Compare commits

...

251 Commits

Author SHA1 Message Date
will.anderson 0a0a2bcb44 parser: bound token reads to Eof so malformed input errors instead of OOMing
El SDK Release / build-and-release (pull_request) Failing after 16s
Out-of-range tok_kind/tok_value reads returned runtime null (el_list_get OOB
-> 0) rather than the Eof sentinel, so the inner parse loops (parse_block,
call-arg, array-literal, match-arm) that terminate only on their close
delimiter or k=="Eof" never saw Eof once the cursor ran past the single
trailing Eof token. On unclosed-delimiter input the parser then appended AST
nodes forever -> unbounded allocation -> ~700GB -> OOM (observed compiling
neuron/sessions.el).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ELP source:
- Add import "morphology.el" to all 29 language morphology modules
- Add language module imports to morphology.el (all langs it dispatches to)
  These were missing since ELP was originally built as a monolithic unit
2026-05-08 19:44:31 -05:00
will.anderson 8212e12e57 Merge pull request 'fix(ci): install gcloud in build-deps step to avoid apt timeout at publish' (#50) from fix/ci-gcloud-install-order into dev
El SDK CI - dev / build-and-test (push) Successful in 6m36s
2026-05-08 17:38:15 +00:00
will.anderson 253ee2b887 fix(ci): install gcloud in build-deps step to avoid apt timeout at publish
El SDK CI - dev / build-and-test (pull_request) Successful in 3m20s
2026-05-08 12:33:57 -05:00
will.anderson d7540700d4 Merge pull request 'perf(ci): precompile el_runtime.o once for all native test modules' (#49) from fix/native-test-precompile-runtime into dev
El SDK CI - dev / build-and-test (push) Failing after 4m1s
2026-05-08 17:24:10 +00:00
will.anderson f103e85f88 perf(ci): precompile el_runtime.o once for all native test modules
El SDK CI - dev / build-and-test (pull_request) Successful in 3m24s
el_runtime.c was being compiled from source for each of the 8 native
test modules. A single precompile step produces el_runtime.o which all
8 link steps reuse — eliminates 7 redundant gcc runtime compilations.
2026-05-08 12:06:11 -05:00
will.anderson fe84639b17 Merge pull request 'fix(ci): fall back to ci-base:latest on first dev rebuild' (#48) from fix/ci-base-dev-first-run into dev
El SDK CI - dev / build-and-test (push) Failing after 14m0s
2026-05-08 16:53:38 +00:00
will.anderson 5fdc9fb15e fix(ci): fall back to ci-base:latest when ci-base:dev doesn't exist yet
El SDK CI - dev / build-and-test (pull_request) Successful in 3m51s
The BASE build arg was hardcoded to ci-base:dev even when the pull fell
back to :latest. Docker then tried to resolve ci-base:dev from the
registry during the build and failed.

Capture which tag was actually pulled and use that as BASE.
2026-05-08 11:49:17 -05:00
will.anderson 8967fa404e Merge pull request 'feat(elc, elb): RBrace stop fix, html_raw/escape runtime, c_source manifest directive' (#46) from fix/elc-parser-elb-build into dev
El SDK CI - dev / build-and-test (push) Failing after 4m28s
2026-05-08 16:43:10 +00:00
will.anderson a7e6fbf2d2 feat(elc, runtime): RBrace stop in parse_html_children; html_raw/html_escape; elc.c canonical
El SDK CI - dev / build-and-test (pull_request) Successful in 4m9s
parse_html_children consumed the closing `}` of the outer El function as
HTML text content when a tag was left open across a function boundary
(e.g. `page_open()` opens `<body>` without a closing `</body>`).  Fix:
stop the children loop when the current token is RBrace — that token
belongs to the El function, not the HTML tree.

Add html_raw() and html_escape() builtins to el_runtime so templates
can interpolate trusted raw HTML and safely escape user-supplied content.

Rename elc-new.c → elc.c as the canonical compiler source; rebuild
elc binary from it.
2026-05-08 11:31:50 -05:00
will.anderson 1f4b594ae7 feat(elb): c_source manifest directive + macOS OpenSSL path detection
Add `c_source "path"` in manifest.el build block — lets packages link
extra C files (platform stubs, native glue) without touching elb source.

On macOS, homebrew OpenSSL isn't on the default linker path. Detect it
via `brew --prefix` and inject -L/-I flags; no-op on Linux.

Rebuild elb binary; remove elc-new binary (elc is now canonical).
2026-05-08 11:31:36 -05:00
will.anderson cff7ce072d Merge pull request 'fix(elc): eliminate OOM in --emit-header; add memory guard' (#47) from fix/elc-oom-checkout into dev
El SDK CI - dev / build-and-test (push) Failing after 4m44s
2026-05-08 16:16:02 +00:00
will.anderson f5dcca0386 build: update dist/platform/elc with OOM fix and memory guard
El SDK CI - dev / build-and-test (pull_request) Successful in 4m16s
Rebuilt from fix/elc-oom-checkout: scan_fn_sigs_el() --emit-header path
+ el_mem_check() guard. Verified on checkout.el: all 3 sigs in .elh,
clean exit under normal load, exit(1) on memory limit exceeded.
2026-05-08 08:23:07 -05:00
will.anderson 53e0b99d5f fix(elc): add el_mem_check() memory guard — abort before OS OOM-kill
Add el_mem_check() to el_runtime.c: reads ELC_MAX_MEM_MB (default 512),
checks RSS via getrusage (macOS bytes / Linux KB normalised to MB), prints
a clear diagnostic to stderr and exits(1) if exceeded.

Wire it into two places:
- compiler.el: upfront check at --emit-header entry point
- codegen.el: per-function check in the streaming loop after each
  el_arena_pop, so runaway growth is caught at the earliest function
  boundary rather than after the machine is already dying.
2026-05-08 08:21:38 -05:00
will.anderson 5f9cad5908 fix(elc): eliminate OOM in --emit-header by using token-level signature scan
The --emit-header path previously called parse() which builds the entire
program AST in memory before writing the .elh file. For checkout.el (~491
lines with HTML template trees and deep BinOp string-concat chains), this
exhausted memory before the header could be written.

Fix: replace parse() + emit_header() with scan_fn_sigs_el() +
emit_header_from_sigs(). The new path tokenises the source once, then
walks the flat token list skipping over function bodies entirely — peak
memory is O(tokens) instead of O(whole-program AST).

New functions in parser.el:
- scan_type_el: reads a type annotation and returns its El source string
- scan_params_el: reads (name: Type, ...) and returns El params string
- scan_fn_sigs_el: token-level scan that collects El-style fn signatures
  without building any expression AST nodes

New function in compiler.el:
- emit_header_from_sigs: writes .elh from scan_fn_sigs_el output

Self-hosting check: elc compiled with new elc, diff of outputs is
identical (zero difference).

Smoke test: elc --emit-header checkout.el produces correct three-entry
.elh (previously truncated at two entries due to mid-parse OOM).
2026-05-08 08:20:13 -05:00
will.anderson 00629b39c4 Merge pull request 'fix(parser): str_join separator '' not ' ' — CSS selectors were emitting spaces' (#45) from fix/css-str-join-separator into dev
El SDK CI - dev / build-and-test (push) Failing after 12m6s
2026-05-07 23:00:19 +00:00
will.anderson ca1e4d57b8 Merge pull request 'ci: add three-tier ci-base rebuild (dev/stage)' (#44) from fix/html-template-if-style-script into dev
El SDK CI - dev / build-and-test (push) Has been cancelled
2026-05-07 23:00:13 +00:00
will.anderson f971e96dd5 fix(parser): str_join separator '' not ' ' — CSS selectors were emitting spaces between tokens
El SDK CI - dev / build-and-test (pull_request) Successful in 3m45s
2026-05-07 15:53:19 -05:00
will.anderson 81a1a624f1 add three-tier ci-base rebuild (dev/stage) to CI workflows
El SDK CI - dev / build-and-test (pull_request) Successful in 3m49s
2026-05-07 15:51:24 -05:00
will.anderson 7b7f9f353b Merge pull request 'fix(parser): add {#if}/{#else}/{/if} and raw-text <style>/<script> in HTML templates' (#43) from fix/html-template-if-style-script into dev
El SDK CI - dev / build-and-test (push) Successful in 4m28s
fix(parser): add {#if}/{#else}/{/if} and raw-text <style>/<script> in HTML templates
2026-05-07 18:44:26 +00:00
will.anderson a3732a1e9a fix(parser): add {#if}/{#else}/{/if} support and raw-text <style>/<script> in HTML templates
El SDK CI - dev / build-and-test (pull_request) Failing after 18m3s
The El lexer silently skips '#', so {#each} lexes as LBrace Ident:"each"
and {#if} lexes as LBrace If ... (using the If keyword token, not Hash).
The existing {#each} check used k2=="Hash" which was dead code.

Parser changes (parser.el):
- Add parse_raw_text_content(): collects all tokens as raw text until
  </tag_name>, bypassing El expression parsing. Used for <style> and
  <script> elements so CSS/JS content isn't parsed as El expressions.
- parse_html_element(): use raw-text mode for <style> and <script> tags.
- parse_html_children(): fix {#each} detection (k2=="Ident", k3=="each"
  instead of dead k2=="Hash" check). Add {#if cond}...{#else}...{/if}
  support generating HtmlIf AST nodes.

Codegen changes (codegen.el):
- Add cg_html_if(): generates if (cond_c) { then_c } else { else_c }
  for HtmlIf nodes.
- cg_html_parts(): dispatch HtmlIf to cg_html_if.
2026-05-07 13:39:12 -05:00
will.anderson 2ed6b26dde Merge pull request 'promote: stage → main (all elb linker fixes + ci-base rebuild)' (#42) from stage into main
El SDK Release / build-and-release (push) Successful in 6m28s
promote: stage → main (all elb linker fixes + ci-base rebuild)
2026-05-07 14:25:37 +00:00
will.anderson d8e9fd12f4 Merge pull request 'promote: dev → stage (all elb linker fixes)' (#41) from dev into stage
El SDK Release / build-and-release (pull_request) Successful in 3m51s
El SDK CI - stage / build-and-test (push) Successful in 4m11s
promote: dev → stage (all elb linker fixes)
2026-05-07 14:20:53 +00:00
will.anderson 8ef3eb6bec Merge pull request 'fix(elb): all linker fixes — gcc compat, OpenSSL, runtime import conflict' (#40) from fix/elb-gcc-bracket-depth into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 4m8s
El SDK CI - dev / build-and-test (push) Successful in 4m34s
fix(elb): all linker fixes — gcc compat, OpenSSL, runtime import conflict
2026-05-07 14:16:17 +00:00
will.anderson 027ad82db2 fix elb linker: remove runtime imports from el-install, add --clean, catch in dev/stage CI
El SDK CI - dev / build-and-test (pull_request) Successful in 3m35s
el-install.el explicitly imported runtime/*.el modules (string, env, fs, exec,
json, http), which elb compiled to .c files in the shared dist/bin out_dir.
Linking those alongside el_runtime.c caused multiple definition errors for
every runtime function (http_get, http_patch, etc.). The runtime .el files are
thin wrappers over seed primitives already compiled into el_runtime.c — no
import needed.

Fixes:
- Remove all explicit runtime imports from el-install.el (root cause)
- Add --clean to every elb invocation in sdk-release.yaml so each build
  starts with a clean out_dir (defense-in-depth against stale .c files)
- Add elb build + epm/el-install build steps to ci-dev.yaml and ci-stage.yaml
  so linker errors are caught on every PR, not just stage->main
2026-05-07 03:20:44 -05:00
will.anderson 8fa9c4ba20 Merge pull request 'promote: dev → stage (elb linker fixes)' (#38) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 1m2s
El SDK CI - stage / build-and-test (push) Successful in 3m56s
promote: dev → stage (elb linker fixes)
2026-05-07 08:11:38 +00:00
will.anderson 8ab8e3fd31 Merge pull request 'fix(elb): add -lssl -lcrypto to link_binary flags' (#37) from fix/elb-gcc-bracket-depth into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 3m22s
El SDK CI - dev / build-and-test (push) Successful in 3m56s
fix(elb): add -lssl -lcrypto to link_binary flags
2026-05-07 08:07:27 +00:00
will.anderson 05d717744b fix(elb): add -lssl -lcrypto to link_binary flags
El SDK CI - dev / build-and-test (pull_request) Successful in 3m24s
el_runtime.c uses OpenSSL (EVP_*, RAND_bytes) for AEAD encrypt/decrypt.
elb was only linking -lcurl -lpthread -lm, missing the SSL libs.
Matches the explicit flags used in ci-dev.yaml and ci-stage.yaml.
2026-05-07 03:03:21 -05:00
will.anderson 9c7bde47dc Merge pull request 'promote: dev → stage (elb gcc fix)' (#35) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 40s
El SDK CI - stage / build-and-test (push) Successful in 3m45s
promote: dev → stage (elb gcc fix)
2026-05-07 08:01:22 +00:00
will.anderson b0d0975f05 Merge pull request 'fix(elb): use clang-only -fbracket-depth flag conditionally' (#34) from fix/elb-gcc-bracket-depth into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 3m21s
El SDK CI - dev / build-and-test (push) Successful in 3m53s
fix(elb): use clang-only -fbracket-depth flag conditionally
2026-05-07 07:57:34 +00:00
will.anderson 6f634ae432 fix(elb): use clang-only -fbracket-depth flag conditionally
El SDK CI - dev / build-and-test (pull_request) Successful in 3m26s
gcc rejects -fbracket-depth=1024 with 'unrecognized command-line option'.
Use shell subshell to probe cc --version and only pass the flag when
the compiler is clang.
2026-05-07 02:53:42 -05:00
will.anderson c0553459e1 Merge pull request 'promote: dev → stage (CI rebuild fix + ci-base refresh)' (#32) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 35s
El SDK CI - stage / build-and-test (push) Successful in 3m47s
promote: dev → stage (CI rebuild fix + ci-base refresh)
2026-05-07 07:50:27 +00:00
will.anderson 908ce303f3 Merge pull request 'ci: rebuild ci-base on SDK release; publish elb + el_runtime.js to Artifact Registry' (#31) from fix/ci-openssl-linker into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 3m21s
El SDK CI - dev / build-and-test (push) Successful in 3m51s
ci: rebuild ci-base on SDK release; publish elb + el_runtime.js to Artifact Registry
2026-05-07 07:46:22 +00:00
will.anderson edbde5ef51 ci: rebuild ci-base on SDK release; publish elb + el_runtime.js to Artifact Registry
El SDK CI - dev / build-and-test (pull_request) Successful in 3m45s
- sdk-release.yaml: add elb and el_runtime.js to foundation-prod uploads
- sdk-release.yaml: add 'Rebuild ci-base' step — patches ci-base:latest with
  freshly built El SDK after each main branch release (pull → overlay → push)
- sdk-release.yaml: add neuron-web to el-sdk-updated dispatch so downstream
  CI rebuilds automatically on SDK update
- ci-dev.yaml: add elb build step and publish elb + el_runtime.js to
  foundation-dev alongside elc and runtime
2026-05-07 02:25:19 -05:00
will.anderson 2e529bd0fe Merge pull request 'Remove Cargo.toml and .rs bootstrap files from el-ui vessels' (#30) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m49s
2026-05-07 05:49:10 +00:00
will.anderson 5d9299a472 Remove all .rs bootstrap files from el-ui vessels
El SDK CI - dev / build-and-test (pull_request) Successful in 3m33s
el-ui vessels are El. The Rust bootstrap implementations were added as
a stopgap but don't belong here — everything should be El source.
Each vessel's src/main.el and manifest.el are the source of truth.
2026-05-06 23:12:25 -05:00
will.anderson e8b01583d8 Remove Cargo.toml files from el-ui — vessels use manifest.el
All package management is through manifest.el / epm. Cargo.toml files
were incorrectly added to vessels and the root. Removed root workspace
Cargo.toml + Cargo.lock and all vessel-level Cargo.toml files.

el-graph and el-html were already correct (no Cargo.toml).
2026-05-06 22:21:47 -05:00
will.anderson fd208583fe Merge pull request 'promote: dev → stage (elb build fix)' (#28) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m51s
El SDK Release / build-and-release (pull_request) Failing after 38s
promote: dev → stage (elb build fix)
2026-05-07 02:46:27 +00:00
will.anderson b19dd5608f Merge pull request 'ci: use elb to build epm and el-install' (#27) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m50s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m33s
ci: use elb to build epm and el-install
2026-05-07 02:37:49 +00:00
will.anderson 94d6eace94 ci: use elb to build epm and el-install (cd into project dir, use --elc flag)
El SDK CI - dev / build-and-test (pull_request) Successful in 3m35s
2026-05-06 21:33:05 -05:00
will.anderson 3e29fc43ab Merge pull request 'promote: dev → stage (__http_do_map_to_file)' (#25) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m44s
El SDK Release / build-and-release (pull_request) Failing after 47s
2026-05-07 02:14:30 +00:00
will.anderson f1dfc394e3 Merge pull request 'fix: add __http_do_map_to_file runtime primitive' (#24) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m51s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m25s
2026-05-07 02:06:34 +00:00
will.anderson 61bf501b84 fix: add __http_do_map_to_file runtime primitive
El SDK CI - dev / build-and-test (pull_request) Successful in 3m41s
el-install.el generates calls to __http_do_map_to_file (HTTP request
with JSON headers map, streaming response to file). Add it to both
the HAVE_CURL implementation and the no-curl stub section.
2026-05-06 21:01:46 -05:00
will.anderson 979a5677d5 Merge pull request 'promote: dev → stage (__-prefixed runtime fix)' (#22) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m48s
El SDK Release / build-and-release (pull_request) Failing after 1m4s
2026-05-07 01:48:32 +00:00
will.anderson 2fd298df55 Merge pull request 'fix: add __-prefixed runtime primitives for El compiler' (#21) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m43s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m22s
2026-05-07 01:40:37 +00:00
will.anderson 254cbe0ac2 fix: add __-prefixed runtime primitives expected by El compiler
El SDK CI - dev / build-and-test (pull_request) Successful in 3m22s
The El compiler generates calls to __-prefixed C primitives from within
El stdlib compiled code (e.g. __println, __str_len, __json_get, etc).
These were absent from el_runtime.c, causing linker failures when
building el-install, elb, or epm with the current compiler.

Add 46 __-prefixed aliases/implementations in el_runtime.c covering:
- I/O: __println, __print, __readline
- String: __str_len, __str_cmp, __str_ncmp, __str_alloc, __str_set_char,
  __str_concat_raw, __str_slice_raw, __str_char_at, plus numeric converters
- FS: __fs_read, __fs_write, __fs_exists, __fs_mkdir, __fs_list_raw, etc
- HTTP: __http_do, __http_do_map, __http_serve, __http_serve_v2,
  __http_response, __http_sse_* (weak stubs)
- JSON: __json_get, __json_set, __json_parse_map, __json_stringify_val, etc
- State, env, exec, uuid, sha256, args
2026-05-06 20:36:49 -05:00
will.anderson 17b1aa0736 Merge pull request 'promote: dev → stage (return type fix)' (#19) from dev into stage
El SDK CI - stage / build-and-test (push) Failing after 4m1s
El SDK Release / build-and-release (pull_request) Failing after 42s
2026-05-07 01:12:18 +00:00
will.anderson bcfb33ea83 Merge pull request 'fix: align runtime return types with El compiler output' (#18) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m36s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m16s
2026-05-07 01:04:29 +00:00
will.anderson 60ad7f2f6b fix: align runtime function return types with El compiler output
El SDK CI - dev / build-and-test (pull_request) Successful in 3m16s
El compiler generates calls to println, print, exit_program,
http_set_handler, http_serve, http_set_handler_v2, and http_serve_v2
as el_val_t-returning functions. The runtime declared them void,
causing conflicting-type errors when el-install.c was compiled.

Change all seven to return el_val_t (side-effect functions return 0).
Also update el_runtime.h declarations to match.
2026-05-06 20:00:40 -05:00
will.anderson f0c731d2db Merge pull request 'promote: dev → stage (runtime fix)' (#16) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m43s
El SDK Release / build-and-release (pull_request) Failing after 45s
2026-05-07 00:43:52 +00:00
will.anderson 231cb5eddd Merge pull request 'fix: add missing runtime functions (native_str_to_int, http_post_json_with_headers)' (#15) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m37s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m39s
2026-05-07 00:35:28 +00:00
will.anderson 54de7d3f3f fix: add missing runtime functions for epm.el
El SDK CI - dev / build-and-test (pull_request) Successful in 3m19s
Add native_str_to_int (El compiler alias for str_to_int) and
http_post_json_with_headers (JSON POST with additional headers map)
which epm.el generates calls to but were absent from el_runtime.c.
2026-05-06 19:31:35 -05:00
will.anderson e7e0f7d3e5 Merge pull request 'promote: dev → stage' (#12) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 4m3s
El SDK Release / build-and-release (pull_request) Failing after 37s
2026-05-07 00:23:46 +00:00
will.anderson 77100649c3 Merge pull request 'fix: use GIT_TOKEN secret in sdk-release.yaml' (#13) from fix/ci-openssl-linker into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 3m46s
El SDK CI - dev / build-and-test (push) Successful in 4m9s
2026-05-07 00:21:20 +00:00
will.anderson b0570656b1 fix: use GIT_TOKEN secret (GITEA_ prefix is reserved)
El SDK CI - dev / build-and-test (pull_request) Successful in 3m25s
2026-05-06 19:17:16 -05:00
will.anderson a79b421578 Merge pull request 'fix: use GITHUB_SHA for artifact version' (#11) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Successful in 3m47s
El SDK CI - stage / build-and-test (pull_request) Successful in 3m26s
2026-05-07 00:09:18 +00:00
will.anderson 0ab9361fab fix: use GITHUB_SHA instead of GITEA_SHA for artifact version
El SDK CI - dev / build-and-test (pull_request) Successful in 3m19s
GITEA_SHA is not set in the runner container environment; GITHUB_SHA is.
Empty version string caused INVALID_ARGUMENT from Artifact Registry.
2026-05-06 19:05:21 -05:00
will.anderson f7953eb73a Merge pull request 'fix: use valid Artifact Registry package IDs (no slashes)' (#10) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Failing after 3m36s
2026-05-07 00:00:20 +00:00
will.anderson 9c350e9f2f fix: use valid Artifact Registry package IDs (no slashes)
El SDK CI - dev / build-and-test (pull_request) Successful in 3m20s
Package IDs must contain only letters, numbers, periods, hyphens and
underscores. el/elc → el-elc, el/el_runtime.c → el-runtime-c, etc.
2026-05-06 18:56:23 -05:00
will.anderson e93c899d1f Merge pull request 'fix: use trusted=yes for gcloud apt source, drop GPG key dance' (#9) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Failing after 3m56s
2026-05-06 23:50:52 +00:00
will.anderson 0b50e61f98 fix: use trusted=yes for gcloud apt source, drop GPG key dance
El SDK CI - dev / build-and-test (pull_request) Successful in 3m18s
The packages.cloud.google.com key format has changed and signature
verification keeps failing in CI. trusted=yes bypasses the ceremony —
we're downloading from a known Google URL so it's fine.
2026-05-06 18:47:05 -05:00
will.anderson f2741e4bdb Merge pull request 'fix: download GCP apt key directly without gpg --dearmor' (#8) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Failing after 3m59s
2026-05-06 23:39:06 +00:00
will.anderson 7fd01b8a8d fix: download GCP apt key directly, no gpg --dearmor
El SDK CI - dev / build-and-test (pull_request) Successful in 6m38s
packages.cloud.google.com now serves the key in binary format; piping
through gpg --dearmor fails with 'no valid OpenPGP data found'.
2026-05-06 18:28:38 -05:00
will.anderson d2940f5d1d Merge pull request 'fix: add --batch to gpg --dearmor in CI publish steps' (#7) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Failing after 10m1s
2026-05-06 23:17:12 +00:00
will.anderson dca741f915 fix: add --batch to gpg --dearmor in publish steps
El SDK CI - dev / build-and-test (pull_request) Successful in 8m50s
gpg tries to open /dev/tty for passphrase input when no TTY is present
in CI, causing the GCP key setup to fail. --batch suppresses interactive
prompts and dearmoring doesn't require one anyway.
2026-05-06 17:58:16 -05:00
will.anderson 9862f4d6e1 Merge pull request 'fix: add -lssl -lcrypto to all CI gcc linker commands' (#6) from fix/ci-openssl-linker into dev
El SDK CI - dev / build-and-test (push) Failing after 3m25s
2026-05-06 22:53:37 +00:00
will.anderson 8b074d2e39 fix: normalize NaN to 'nan' in float_to_str regardless of sign bit
El SDK CI - dev / build-and-test (pull_request) Successful in 3m18s
0.0/0.0 can produce -nan on Linux/x86_64 (%g gives '-nan'),
causing the no-cycle calendar test to fail. Explicitly check isnan()
and emit 'nan' so behavior is platform-independent.
2026-05-06 17:49:35 -05:00
will.anderson ec9c322cc7 fix: use elc-linux-amd64 as bootstrap seed instead of elc-bootstrap.c
El SDK CI - dev / build-and-test (pull_request) Failing after 1m57s
elc-bootstrap.c is stale and produces a broken gen2 that can't correctly
compile current El source (generates C without main() for user programs).
The committed elc-linux-amd64 binary is the current, correct seed for
linux CI. This removes the gen2-from-C step entirely.
2026-05-06 17:45:57 -05:00
will.anderson 702093e043 fix: add -lssl -lcrypto -lm to all test runner gcc commands
El SDK CI - dev / build-and-test (pull_request) Failing after 1m7s
Same OpenSSL/math linker flags needed everywhere el_runtime.c is linked.
2026-05-06 17:41:55 -05:00
will.anderson 95b6fac094 fix: use elc-cli.el as gen3 entry point, not compiler.el directly
El SDK CI - dev / build-and-test (pull_request) Failing after 1m8s
compiler.el imports lexer.el/parser.el/codegen.el with bare names; those
resolve relative to the source file's directory only when the entry point
is elc-cli.el (which imports el-compiler/src/compiler.el by full path).
Compiling compiler.el directly leaves lex/parse/codegen as undefined refs.
2026-05-06 17:39:38 -05:00
will.anderson af66cebfd3 fix: add -lssl -lcrypto to all CI gcc linker commands
El SDK CI - dev / build-and-test (pull_request) Failing after 28s
el_runtime.c now uses OpenSSL EVP AEAD encryption; all gcc commands
in all three workflow files need -lssl -lcrypto to link correctly.
2026-05-06 17:36:48 -05:00
will.anderson 1b9bc049de ci: trigger test run 2026-05-06 17:15:37 -05:00
Will Anderson 19a7430ff8 ci-dev: add PR trigger, gate publish on push (post-merge only) 2026-05-06 17:11:28 -05:00
Will Anderson 6b0a77a1dd Restructure CI/CD into proper dev -> stage -> main gated pipeline
- ci-dev.yaml: push to dev only (remove stale PR trigger)
- ci-stage.yaml: PR from dev validates, push to stage publishes to foundation-stage;
  add -lm/-Wl,--allow-multiple-definition flags and all 9 native --test suites
- sdk-release.yaml: add PR to main trigger for validation, gate publish/release/dispatch
  on push (post-merge) only; add -lm flags and all 9 native --test suites to main as well
2026-05-06 17:05:49 -05:00
Will Anderson 17e204ff2b Merge branch 'main' into dev 2026-05-06 17:01:03 -05:00
Will Anderson fc6d496937 fix: correct if-stmt parser test assertions — if is an expression in El 2026-05-06 16:45:01 -05:00
Will Anderson 2f713d64d4 Merge PR #4: perf: 81% RSS reduction in elc compiler + --test mode 2026-05-06 14:35:54 -05:00
Will Anderson 0a2a084d65 Merge PR #3: feat: port remaining foundation El source into monorepo 2026-05-06 14:35:49 -05:00
Will Anderson 7116610ffd Merge PR #2: feat: port el-ui vessels — rename crates→vessels, add El source + manifests 2026-05-06 14:35:45 -05:00
Will Anderson 810d8107da Merge PR #1: ci: fix gen2/gen3 gcc flags and step name formatting 2026-05-06 14:35:30 -05:00
will.anderson 607d5661d8 Merge pull request 'feat: port el-ui vessels — rename crates→vessels, add El source + manifests' (#2) from feat/el-ui-html-emit into dev 2026-05-06 19:34:00 +00:00
will.anderson c4a569f29b Merge pull request 'ci: fix gen2/gen3 gcc flags and step name formatting' (#1) from fix/ci-workflow-flags into dev 2026-05-06 19:33:56 +00:00
Will Anderson ec889e1e53 Add --test mode to elc with Assert stmt and full native test suite passing
Implement compile_test() entry point that emits a C test harness instead
of a normal program. Test blocks (previously skipped) now compile to
static functions with per-assertion pass/fail tracking. Assert statement
added to parser and codegen. Runtime extended with now_ns, fs_list_json,
json_build_object, json_build_array, json_escape_string, state_has,
state_get_or. Fix float negation codegen, float equality comparisons,
time_to_parts return type (JSON string), time_format empty-fmt, json_set
raw-value semantics, state_keys JSON array return. All 310 native tests
pass across 9 suites (core, text, string, math, env, state, json, time, fs).
2026-05-06 14:33:47 -05:00
Will Anderson 6ced0f8009 fix: double-free in engram_neighbors_json BFS + rebuild engram.c
el_strdup tracks pointers in the arena. The BFS arrays in
engram_neighbors_json are manually freed — using el_strdup caused a
double-free when the arena was later popped. Changed to plain strdup
for those allocations.

engram/dist/engram.c rebuilt from engram/src/server.el with current
elc (minor codegen diff: parenthesisation and _argc/_argv rename).
2026-05-06 14:11:40 -05:00
Will Anderson bd7303447b fix: skip test blocks in codegen to prevent OOM on test files
test "name" { ... } blocks were not recognized by the self-hosted
compiler. The body { } was parsed as a Map literal, creating a huge
AST with O(n²) string concatenation in the toplevel_exec_stmts loop
(which had no arena scope). A 272-line test file would consume 400MB+
and a 720-line file importing the full compiler source caused 150GB
usage and crashed the machine.

Two fixes:
1. Skip Test tokens in codegen_streaming before parse_one() —
   advance past "name" and skip_to_rbrace on the body block.
   Test blocks are never compiled; self-hosted compiler has no test runner.

2. Add per-statement arena scope to toplevel_exec_stmts emission loop,
   matching the el_main_body loop. Frees intermediate strings after
   each statement to prevent O(n²) accumulation from any unrecognized
   construct that reaches that path.

Result: test_string.el (272 lines, 27 test blocks): 0MB peak (was 400MB+).
        test_compiler.el (720 lines + 8728 imported): 15MB peak (was 150GB).
2026-05-06 13:34:03 -05:00
Will Anderson e8f6765750 fix: arena leak in compile() — token/sig strings now tracked
Wrapped compile() body in el_arena_push/pop so the arena is active
before lex() and scan_fn_sigs(). Previously both ran with
_tl_arena_active=0, leaking all token and signature strings permanently.
Also prevents inner pop(mark=0) calls from deactivating the arena
between per-function scopes. Verified: self-host PASS, RSS stable.
2026-05-06 10:53:12 -05:00
Will Anderson 3726f69435 perf: 81% RSS reduction — el_release, arena scoping, streaming codegen, libcurl stub
Chain of optimizations from swarm rounds 4-7:
- Flat stride-2 token list: eliminate per-token Map allocation (~112B each × N tokens)
- Systematic el_release() in parser.el: eagerly free intermediate parse result maps
- Per-function and per-statement arena scoping in codegen_streaming()
- Streaming codegen pipeline: parse one fn at a time, emit C, discard AST
- HAVE_CURL guard: elc CLI binary drops libcurl, eliminating SSL/TLS init overhead
- HTML codegen parts-list: O(n) instead of O(n²) string growth for nested templates
- Batch c_escape: str_slice clean runs instead of char-at per byte

Result: 33.4MB → 6.5MB RSS on web/src/main.el (-81%). Self-host: PASS.
2026-05-05 20:39:38 -05:00
Will Anderson ee86736eab merge round-4-delta: flat stride-2 token list + str_char_code dispatch + batch c_escape
- Flat token list: lexer emits [kind0, val0, kind1, val1, ...] instead of [{kind,val}, ...]
  Eliminates per-token ElMap allocation (~112B × N tokens)
- str_char_code hot loop: char classification via Int codes, no strdup per char
- Batch c_escape: str_slice clean runs instead of char-at per byte
- Parser updated to use tok_at/tok_kind/tok_value stride-2 accessors
2026-05-05 20:29:35 -05:00
Will Anderson eb52be4ade runtime: add EL_TRUE/EL_FALSE macros and scoped arena for CLI
Adds EL_TRUE/EL_FALSE convenience macros to el_runtime.h alongside the
existing EL_NULL, making boolean-returning builtins readable without
raw (el_val_t) casts. Documents all value macros in the header comment.

Also lands el_arena_push/el_arena_pop — a scoped string arena for CLI
programs that never call el_request_start/end. The compiler can push a
mark before a compilation unit and pop it after to free intermediate
strings, reducing peak RSS during long compile runs.
2026-05-05 19:15:49 -05:00
Will Anderson e587bedf30 round-3-gamma: combine c_escape + scan_interp_string batching — max round-3 savings
Combines two orthogonal optimizations:
1. c_escape batching (from alpha): ASCII runs emitted as str_slice segments instead
   of one str_char_at string per byte. O(N) allocs → O(K) where K = special chars.

2. scan_interp_string batching (from beta): char dispatch via str_char_code (Int)
   + clean_start tracking to flush plain runs as str_slice. Eliminates per-char
   string allocations in the string-literal scanning hot path.

Result on web/src/main.el: 14.5MB -> 13.4MB peak RSS (-7.6%).
Self-hosting: PASS.
2026-05-05 16:01:05 -05:00
Will Anderson 1eef9928f4 round-2-gamma: combine flat token list + char code dispatch — max round-2 savings
Combines two orthogonal optimizations:
1. Flat token list (from beta): lex() returns [Any] with alternating kind/value
   pairs instead of [Map], eliminating one ElMap per token (~3 mallocs each).
   Parser updated: tok_kind(t,i) = t[2*i], tok_value(t,i) = t[2*i+1].

2. Char code dispatch (from alpha): lex() hot loop uses str_char_code -> Int
   instead of str_char_at -> strdup String for all character classification.
   Eliminates ~400K x 16B = 6.4MB of temporary string allocations.

scan_digits and scan_ident also updated to use str_char_code.

Result on main.el: 17.1MB -> 14.4MB peak RSS (-16%).
Self-hosting: PASS.
2026-05-05 15:46:20 -05:00
Will Anderson 1e67544c88 round-2-alpha: char code ops in lex() hot loop — eliminate str_char_at allocations
Replace str_char_at (returns strdup String) with str_char_code (returns Int)
in the main lex() while loop and scan_digits/scan_ident helpers.

For a 400KB combined source, str_char_at was allocating ~400K x 16B = 6.4MB
of transient 2-byte strings for the ch variable alone. str_char_code returns
an integer directly — zero allocation.

Add Int-based helpers: is_digit_code, is_alpha_code, is_ws_code,
is_alnum_or_underscore_code. Rewrite lex() operator dispatch using char
code constants (e.g. '/'=47, '"'=34, '='=61).

Result on main.el: 17.1MB -> 15.4MB peak RSS (-10%).
Self-hosting: PASS.
2026-05-05 15:43:29 -05:00
Will Anderson 2ac11a67b1 beta: replace native_string_chars with str_char_at/str_slice in lexer — 49% memory reduction on large files 2026-05-05 15:19:59 -05:00
Will Anderson 7f295bffe9 fix: codegen O(n²) HTML memory leak + elb stderr surface + runtime dir path 2026-05-05 14:40:15 -05:00
Will Anderson 962c8cbe57 dist: add linux/amd64 binaries and el_runtime.js 2026-05-05 09:44:25 -05:00
Will Anderson a54797ebfe dist: add linux/amd64 binaries and el_runtime.js 2026-05-05 09:43:45 -05:00
Will Anderson db157106ee revert: remove forge — not part of the monorepo 2026-05-05 04:31:27 -05:00
Will Anderson 135744b4fe revert: remove dharma — not part of the monorepo 2026-05-05 04:31:20 -05:00
Will Anderson 90ddbdbfc3 feat: port arbor, dharma, forge El source into monorepo
Brings the remaining foundation repos that were not included in the
original monorepo consolidation:

- arbor/vessels/ — 6 vessels (arbor-cli, arbor-core, arbor-diagram,
  arbor-layout, arbor-parse, arbor-render) with manifests + src/main.el
- dharma/ — CGI Provenance Registry package (flat layout, 14 .el files
  across registry/, sandbox/, training/, validation/, tests/)
- forge/ — consciousness channel tool (8 src .el files + new manifest.el)
- elp/src/ — 36 test fixture files not carried over in original merge
  (dedup_*, realizer_*, semantics_*, morph_*, ext_*, one_extern_* helpers)

el-ide, engram, elql are already complete in ide/, engram/, ql/.
2026-05-05 04:27:34 -05:00
Will Anderson 7c00922cbd fix(el-html): tighten manifest — correct description, remove unused dependencies
el-html is a standalone atomic emit layer; it has no runtime dependency on
el-style or el-layout (those vessels depend on el-html for SSR, not the other
way around).
2026-05-05 04:20:38 -05:00
Will Anderson faee6fdb25 feat: port el-ui vessels — rename crates→vessels, add El source + manifests 2026-05-05 04:19:22 -05:00
Will Anderson a6d093536a ci: fix gen2/gen3 gcc flags and step name formatting
El SDK CI - dev / build-and-test (pull_request) Failing after 7s
- add -lm (el_runtime.c uses pow/sqrt/log/sin/cos/exp)
- add -Wl,--allow-multiple-definition to gen2 (is_digit/is_whitespace
  defined in both elc-bootstrap.c and el_runtime.c; bootstrap predates
  the text-processing primitives commit)
- remove colon from Self-host step name (Gitea YAML parser rejects it)
- replace em dashes in step names with hyphens
2026-05-05 03:04:54 -05:00
Will Anderson b580a63540 release: el-install binary and SDK bundle 2026-05-05 03:03:06 -05:00
Will Anderson bdd7b56703 feat: el-install binary + SDK bundle release 2026-05-05 03:03:01 -05:00
Will Anderson 592f8f482a add el-install binary and SDK bundle to release pipeline
- lang/tools/install/el-install.el: El program that fetches the latest
  release from the Gitea API, downloads el-sdk-latest.tar.gz, and
  extracts it into ~/.el (or a custom prefix passed as argv[1])
- lang/tools/install/manifest.el: build manifest for the el-install package
- .gitea/workflows/sdk-release.yaml: build elb, epm, and el-install
  binaries; bundle elc + elb + epm + runtime files into el-sdk-latest.tar.gz;
  attach both the tarball and el-install binary to the Gitea release
  alongside the existing per-file GCP uploads
2026-05-05 03:02:56 -05:00
Will Anderson 8524479f89 sync CI fixes and pre-commit hook from main 2026-05-05 02:38:01 -05:00
Will Anderson b2775b9228 fix CI paths for monorepo lang/ layout; add pre-commit hook 2026-05-05 02:37:56 -05:00
Will Anderson 52d0dd4225 release: monorepo restructure — foundation repos unified under el/ 2026-05-05 02:25:53 -05:00
Will Anderson d7d7852f2e monorepo: fold engram, elp, el-ui, elql, el-ide into el (history preserved) 2026-05-05 02:25:34 -05:00
Will Anderson 27f99f4053 monorepo: fold el-ide into ide/ (history preserved) 2026-05-05 01:40:49 -05:00
Will Anderson f1c0604ed8 monorepo: fold elql into ql/ (history preserved) 2026-05-05 01:40:45 -05:00
Will Anderson 4395d551c4 monorepo: fold el-ui into ui/ (history preserved) 2026-05-05 01:40:41 -05:00
Will Anderson 4a3f53cae6 monorepo: fold elp into elp/ (history preserved) 2026-05-05 01:40:34 -05:00
Will Anderson f51daa6ba0 monorepo: fold engram into engram/ (history preserved) 2026-05-05 01:40:30 -05:00
Will Anderson 1ae68962cf restructure: move el compiler content into lang/ 2026-05-05 01:38:51 -05:00
Will Anderson ce68f91a38 merge integrate/el-html-templates: add HTML template parser and codegen 2026-05-05 00:18:03 -05:00
Will Anderson 3e7d316c65 feat: extract HTML template parser/codegen from feat/el-html-templates
Parser additions (parser.el, no existing features removed):
- HTML template parser functions: is_html_tag_name, is_void_element,
  parse_html_text_tokens, parse_html_attrs, parse_html_children,
  parse_html_each_body, parse_html_element, parse_html_template
- HtmlTemplate detection in parse_primary (<tagname> and <!doctype>)
- Lambda fn literal expression node (parse_primary)
- Enum::Variant pattern matching (parse_pattern)
- type definition optional = before {
- try/catch statement (TryCatch AST node)

Codegen additions (codegen.el, no existing features removed):
- HTML template C codegen: cg_html_template, cg_html_parts,
  cg_html_attrs_str, cg_html_element_str, cg_html_each, next_html_id
- HtmlTemplate and Lambda dispatch in cg_expr
- Variant pattern support in cg_match
- TryCatch lowering in cg_stmt (C: runs try body, ignores catch)
- builtin_arity entries: getpid_now, stdout_to_file, stdout_restore

JS codegen additions (codegen-js.el, pure additions only):
- JS HTML template codegen: js_cg_html_template and helpers
- HtmlTemplate dispatch in js_cg_expr

Example: examples/html-page.el
2026-05-05 00:17:59 -05:00
Will Anderson 847f556ee3 merge integrate/js-browser-runtime: add JS compilation target 2026-05-05 00:11:07 -05:00
Will Anderson 92f393afd8 feat: extract JS browser runtime from feat/js-browser-runtime
- Update el-compiler/src/codegen-js.el to Phase 5 (1245 lines, up from 926)
  Adds: lambda literals, try/catch, extern fn, JS method call, Promise helpers,
  Object/Array utils, URL import declarations
- Update el-compiler/runtime/el_runtime.js (1049 lines, up from 679)
- Add examples/browser-counter.el, examples/browser-auth.el
- Update spec/codegen-js.md to Phase 5 status
- Update el-compiler/src/compiler.el: add --bundle, --minify, --obfuscate flags,
  bundled IIFE mode, terser/javascript-obfuscator post-processing pipeline
- No lexer.el or parser.el taken from this branch
2026-05-05 00:11:03 -05:00
Will Anderson 3fbfe76f14 merge integrate/native-testing: add native El test suite 2026-05-05 00:10:00 -05:00
Will Anderson 9013e241c3 feat: extract native El test suite from feat/native-testing
- Add tests/native/test_{core,text,string,math,state,time,json,env,fs}.el
- test_codegen_js.el renamed to test_core.el per dev convention
- Add native test CI steps to ci-dev.yaml (compile-link-run pattern)
- No lexer.el/parser.el/codegen.el changes taken from this branch
2026-05-05 00:09:57 -05:00
Will Anderson f9406afc83 merge ci/add-release-workflow into dev 2026-05-05 00:04:51 -05:00
Will Anderson e16f18b409 merge ci/wire-engram-elql-dispatch into dev 2026-05-05 00:02:41 -05:00
Will Anderson 507b220518 merge runtime/integrate into dev 2026-05-05 00:01:16 -05:00
Will Anderson 4e79edbe81 ci: retrigger after ci-base image rebuild 2026-05-04 20:17:16 -05:00
Will Anderson deb7faba7f ci: retrigger after ci-base image rebuild 2026-05-04 20:17:14 -05:00
Will Anderson e8b22e16a2 add el-tests vessel: manifest-based test suite under tests/suite/
Restructures the test suite as a proper El vessel with manifest.el and
src/ layout, eliminating the bash run.sh harness. CI runs the suite with
two commands: `cd tests/suite && elb && ./dist/el-tests`. Exit code is
the fail count (0 = all pass).

163 test cases across 7 modules: string (52), math (13), json (26),
state (11), time (25), fs (16), collections (19).
2026-05-04 19:53:35 -05:00
Will Anderson 26e327ac62 enforce source branch in CI: stage←dev, main←stage 2026-05-04 19:34:51 -05:00
Will Anderson c704e53102 enforce source branch in CI: stage←dev, main←stage 2026-05-04 19:34:45 -05:00
Will Anderson 3086a56b5c enforce source branch in CI: stage←dev, main←stage 2026-05-04 19:34:34 -05:00
Will Anderson 2eebd13221 Add release workflow listening to el-sdk-updated and engram-updated
Triggers on push to main plus repository_dispatch for both el-sdk-updated
and engram-updated. Installs El SDK from foundation-prod, compile-checks all
standalone .el programs, and includes elql-updated dispatch placeholder for
future downstream consumers.
2026-05-04 19:32:23 -05:00
Will Anderson 38d1905f1d Wire engram-updated dispatch to elql
Replace placeholder comment with actual curl dispatch call that fires
engram-updated to neuron-technologies/elql on every Engram release.
2026-05-04 19:32:08 -05:00
Will Anderson 7c644b8d89 Add dev/stage CI pipelines and expand sdk-release to full prod pipeline
- Add ci-dev.yaml: builds elc gen2→gen3, runs 4 test suites, publishes
  el/elc + el_runtime.c + el_runtime.h to foundation-dev Artifact Registry
- Add ci-stage.yaml: same as dev but targets foundation-stage registry
- Update sdk-release.yaml: publish 3 SDK artifacts to foundation-prod
  Artifact Registry after Gitea release; expand dispatch list from 2 to 6
  downstream repos (el-ui, elp, elql, el-ide added alongside engram/forge)
2026-05-04 19:32:05 -05:00
Will Anderson 9e8d23bcd9 add epm — El Package Manager
Introduces epm/, a new component written entirely in native El.
epm manages vessels (El's deployable package format): publish to Engram,
install with full dependency resolution, list registry contents, and
inspect vessel metadata.

- epm/manifest.el         — package manifest
- epm/src/manifest.el     — vessel/package manifest parser (line-by-line,
                            same approach as elb.el)
- epm/src/registry.el     — Engram-backed vessel registry (POST /api/nodes,
                            GET /api/search); vessels stored as Entity nodes
                            with label "vessel:<name>:<version>"
- epm/src/install.el      — topological dependency resolver with cycle
                            detection; installs to .epm/vessels/<name>/
- epm/src/epm.el          — main entry point: publish / install / list / info
2026-05-04 19:31:24 -05:00
Will Anderson 0791fda43e elb: add -lm to link flags — el_runtime.c uses math.h functions
el_runtime.c includes <math.h> and calls pow(), sqrt(), log() in several
places (math operations, engram dampening, float formatting). Without -lm
the linker fails on Linux when linking programs built with elb.
2026-05-04 19:14:09 -05:00
Will Anderson 53f2df500d runtime: add dharma-required functions to el_runtime.c and runtime/*.el
Add the following functions that dharma registry calls but were missing
from the El runtime:

el_runtime.c (consumed by the old build system via released SDK):
  - list_len, list_get — aliases for el_list_len/el_list_get (handlers.el)
  - json_array_push — append pre-encoded element to JSON array string
  - now_millis, unix_timestamp_ms, time_now_ms — ms-since-epoch aliases
  - log_info, log_warn — structured stderr log helpers
  - config — reads config from environment (alias for getenv)
  - http_patch — HTTP PATCH with Content-Type: application/json
  - http_post_engram — HTTP POST with optional X-API-Key header
  - http_get_engram — HTTP GET with optional X-API-Key header
  - str_to_bytes — encode string as JSON byte array [72,101,...]
  - bytes_to_str — decode JSON byte array back to string
  - hash_sha256 — SHA-256 hex digest using built-in sha256 impl

runtime/*.el (consumed by the new build system):
  - http.el: http_patch, http_post_engram, http_get_engram
  - time.el: now_millis, unix_timestamp_ms, time_now_ms
  - env.el: config, log_info, log_warn, list_len, list_get
  - json.el: json_array_push, bytes_to_str
  - string.el: str_to_bytes, hash_sha256 (via __sha256_hex seed)

el_seed.h / el_seed.c:
  - __sha256_hex primitive with self-contained SHA-256 implementation
2026-05-04 19:07:08 -05:00
Will Anderson b0d0f18524 ci: fix YAML in workflow
El CI -dev / build-and-test (push) Failing after 35s
2026-05-04 14:33:33 -05:00
Will Anderson 24ccef820c ci: add workflow_dispatch trigger 2026-05-04 14:32:48 -05:00
Will Anderson eb282d8c0c ci: add El CI dev pipeline 2026-05-04 14:22:10 -05:00
Will Anderson cd164debb8 add /nodes/list as alias for GET /nodes
Dharma's EngramDB client calls /nodes/list to retrieve all nodes.
Add this as an alias for the existing /nodes (and /api/nodes) route
so downstream clients don't need to be updated when the API drifts.

Also update dist/engram.c to match server.el.
2026-05-04 11:44:22 -05:00
Will Anderson cab8509608 add gitflow CI for dev/stage/prod environments 2026-05-04 08:55:34 -05:00
Will Anderson dbff2dad7a add gitflow CI for dev/stage/prod environments 2026-05-04 08:55:23 -05:00
Will Anderson 245eb2898e runtime: declare __thread_create and __thread_join in header for C99 compliance 2026-05-03 18:00:47 -05:00
Will Anderson 7bfe30b767 elb: raise clang bracket depth to 1024 — fixes compile failure for large JS string renders 2026-05-03 17:37:56 -05:00
Will Anderson 6ede9e4379 runtime: restore el_runtime.c as build shim; fix el_seed.h self-contained types
el_runtime.c was deleted prematurely — elb still resolves the runtime at build time
via a hardcoded relative path, and the elc code generator still emits
#include "el_runtime.h" in generated C.

Restoring el_runtime.c + el_runtime.h as the working build runtime until the
compiler is updated to emit #include "el_seed.h" and link against el_seed.c
directly.

el_seed.h: remove #include "el_runtime.h" that broke after el_runtime.h deletion;
add inline el_val_t typedef + macros + float cast helpers so el_seed.h is fully
self-contained.
2026-05-03 17:35:14 -05:00
Will Anderson 4ae42ee7db runtime: native SSE streaming — http_sse_open/send/close
Add Server-Sent Events support to the El runtime. El v2 handlers can now
hold HTTP connections open and push events in real time.

New builtins in el_seed.c:
  __http_conn_fd()          — retrieve raw fd from thread-local set by worker
  __http_sse_open(fd)       — send SSE headers (text/event-stream), keep-alive
  __http_sse_send(fd, data) — write "data: <data>\n\n" frame
  __http_sse_close(fd)      — close the connection fd

http_worker_v2 in legacy/el_runtime.c now:
  - stashes the fd via el_seed_set_http_conn_fd() before calling the handler
  - detects the "__sse__" sentinel return value to skip http_send_response
    and skip close(fd) — SSE handler took ownership of the fd
  - clears the thread-local after the handler returns

El wrappers added to runtime/http.el:
  http_conn_fd() http_sse_open(fd) http_sse_send(fd, data)
  http_sse_close(fd) http_sse_sentinel()
2026-05-03 17:15:37 -05:00
Will Anderson 3e5130e98d remove el_runtime.c — runtime is 100% native El
el_runtime.c and el_runtime.h removed from the active runtime directory
(archived copies remain in el-compiler/runtime/legacy/).
tools/lsp/build.sh removed as it depended on el_runtime.c directly.
AGENTS.md updated to reflect el_seed.c as the sole C dependency.
2026-05-03 17:10:04 -05:00
Will Anderson beb4e436e1 archive el_runtime.c — native El runtime complete, seed is self-contained
el_seed.c now defines el_request_start/el_request_end directly (delegating
to its own seed arena) rather than declaring them as externs from el_runtime.c.
Header comment updated to reflect self-contained build.
2026-05-03 17:08:49 -05:00
Will Anderson 71a1e41f93 lsp: type-aware field completions — scan type/let/param decls, complete on dot-access
When a document is opened or changed, scan for `type Name { field: Type }` blocks
and `let var: Type` / `fn foo(param: Type)` annotations. On completion requests,
if the text before the cursor ends with `identifier.`, look up the variable's type
and return its fields as Field (kind=5) completion items instead of the full list.
2026-05-03 16:16:11 -05:00
Will Anderson 0676725cb7 Bundle El runtime as installable framework
- runtime/stdlib.el: master import file for the full El standard library
  in correct dependency order (string→math→time→env→fs→exec→json→http→
  state→thread→channel→engram→manifest); test.el excluded (dev-only)

- tools/install.sh: installs El to a prefix (default /usr/local/el);
  copies elc binary, runtime .el files, headers, compiles libel.a from
  el_seed.c + el_runtime.c, generates an installed stdlib.el with absolute
  paths

- tools/new-project.sh: scaffolds a new El project with src/main.el,
  build.sh (auto-discovers local elc/runtime), README.md, .gitignore;
  verified working end-to-end

- AGENTS.md: fix elc rebuild docs — elc writes to stdout, not to its second
  argument; correct usage is ./dist/platform/elc elc-cli.el > elc-new.c
2026-05-03 16:04:26 -05:00
Will Anderson d98a968f89 rebuild elc — add __read_n and __print_raw for LSP stdin framing (self-hosting verified) 2026-05-03 16:03:34 -05:00
Will Anderson 0c9154551f merge tools/lsp (full) — 184 completions, hover, go-to-def, live diagnostics, enhanced VSCode extension 2026-05-03 16:01:11 -05:00
Will Anderson 9aa0c49d0c add full El LSP — completions, hover, go-to-def, diagnostics, VSCode extension 2026-05-03 15:59:42 -05:00
Will Anderson 59b96e3324 rebuild elc from sprint-integrated source — all language features verified
Self-hosting sprint merges compiled in:
- compiler: break/continue, % modulo, for-range loops, match-stmt codegen, string interpolation
- runtime: threading, http, json, fs/exec/env, time/math/state, engram, string (57 fns), channels
- seed: el_seed.c minimal C OS boundary (968 lines)
- tools: LSP skeleton with JSON-RPC, completions, hover, go-to-def, VSCode extension
- tests: El test framework with state-backed runner + string test suite

Self-hosting verified: elc-new → C → binary → same C output (idempotent).
Provenance snapshot: dist/platform/elc.20260503-1555-post-sprint
2026-05-03 15:55:50 -05:00
Will Anderson 287b39f8e6 merge tools/lsp — El LSP skeleton: JSON-RPC, completions, hover, go-to-def, VSCode extension 2026-05-03 15:53:39 -05:00
Will Anderson c45744d8ca merge runtime/seed — el_seed.c minimal C OS boundary (968 lines) 2026-05-03 15:52:21 -05:00
Will Anderson 2e778ca664 merge compiler/string-interp — string interpolation via lexer desugaring 2026-05-03 15:52:21 -05:00
Will Anderson 641227a7d3 merge runtime/channels — MPMC buffered channels, channel_pipeline, channel_fan_out 2026-05-03 15:52:21 -05:00
Will Anderson 805318e2d0 merge runtime/test — El test framework with state-backed runner and string test suite 2026-05-03 15:52:21 -05:00
Will Anderson cefff5b891 add El LSP skeleton — language server, VSCode extension, syntax highlighting
Implements a Language Server Protocol server for El files over stdin/stdout
JSON-RPC. Provides completions (builtins + keywords + document functions),
hover documentation, go-to-definition, and full document sync.

Also adds a VSCode extension that launches the binary as a child process and
a TextMate grammar for .el syntax highlighting.

NOTE: el-lsp.el calls __read_n(n: Int) -> String, a seed primitive not yet
in el_runtime.c. Build.sh documents the required C implementation; the seed
agent must add it before the binary will link.
2026-05-03 15:52:10 -05:00
Will Anderson ce9a2caff4 add string interpolation to El ("hello ${name}")
Lexer gains scan_interp_string which replaces scan_string in the main
lex loop. When no ${ is found it behaves identically to before (single
Str token). When interpolations are present it emits a flat token
sequence — Str, Plus, (expr tokens), Plus, Str, … — that the existing
parse_binop / cg_expr BinOp-Plus-string path assembles into nested
el_str_concat calls with zero parser or codegen changes.

Key design choices:
- scan_interp_brace tracks { depth so fn(a, b) inside ${} is safe
- inner expr tokens are wrapped in ( ) so operators like + in ${n+1}
  do not associate with the surrounding concat Plus tokens
- \$ escapes to a literal dollar sign; bare $ not before { passes through
- empty ${} emits an empty string segment
2026-05-03 15:50:23 -05:00
Will Anderson d1af4b0f8b add channels to El — buffered MPMC channel with send/recv/close
Introduces Go-style channels as El's mid-flight communication primitive,
completing the threading model: threads can now not only spawn/join but
also communicate while running.

Part 1 — seed layer (el_runtime.c / el_runtime.h):
- Add __thread_create/__thread_join/__mutex_new/__mutex_lock/__mutex_unlock
  as C seed primitives (dlsym-based thread dispatch, pthread mutex table)
- Add __channel_new/__channel_send/__channel_recv/__channel_try_recv/__channel_close
  as MPMC channel seed primitives backed by mutex + condvar + circular buffer
- Bounded channels (cap > 0): circular buffer, sender blocks when full
- Unbounded channels (cap == 0): dynamic array, grows on demand, never blocks
- channel_close wakes all blocked recvers/senders; recv drains then returns ""

Part 2 — El API (runtime/channel.el):
- channel_new/send/recv/try_recv/close — thin wrappers over seed layer
- channel_pipeline — spawn N worker threads reading from in_ch, applying
  fn_name, writing to out_ch; workers exit on "" sentinel from close
- channel_drain — collect all messages from a closed channel into [String]
- channel_fan_out — send a [String] list into a channel then close it

Part 3 — codegen.el:
- Register all 10 seed builtins (__thread_* + __channel_*) in builtin_arity
  so the arity checker validates call sites at compile time
2026-05-03 15:50:13 -05:00
Will Anderson 282df712a8 add runtime/test.el — El test framework with assertions and runner
Provides assert_true/false, assert_eq, assert_int_eq, assert_neq,
assert_contains, assert_starts_with, assert_ends_with, and fail.
Test cases are registered by name+fn_name, executed sequentially via
the thread/dlsym dispatch mechanism, with results tracked in state_.
Includes tests/runtime/string_test.el covering all 23 string.el exports.
2026-05-03 15:49:01 -05:00
Will Anderson cfcedff7f4 implement match statement codegen in El
Add cg_match_stmt() to lower match-as-statement to proper C if/else if/else
chains. Previously, match in statement position fell through to cg_expr() which
emitted a GCC statement-expression — fine for expression arms but wrong for the
statement form. Now matched using the same dispatch pattern as If and For in the
Expr handler of cg_stmt().

Pattern dispatch mirrors cg_match (expression form):
  LitStr  -> str_eq(subj, EL_STR("..."))
  LitInt  -> subj == N
  LitBool -> subj == 1 / 0
  Binding -> else { el_val_t name = subj; body; }
  Wildcard -> else { body; }

Subject is evaluated once into a scoped temporary to avoid double evaluation.
2026-05-03 15:47:50 -05:00
Will Anderson eab483ed4f add el_seed.c — minimal C OS boundary for El runtime migration
Introduces el_seed.c / el_seed.h as the clean OS-boundary layer for new-generation
El programs. All public symbols use the __ prefix convention; el_val_t (int64_t) is
the universal value type throughout.

Key additions over el_runtime.c:
- __thread_create / __thread_join: pthreads + dlsym(RTLD_DEFAULT) parallelism
  foundation. Static ElThread table (64 slots); worker resolves El fn symbols at
  runtime, stores result string for join to return.
- __mutex_new / __mutex_lock / __mutex_unlock: pooled pthread_mutex_t handles
- __http_do: unified curl call with JSON headers string (vs ElMap) and explicit
  timeout_ms parameter
- __fs_list_raw: returns newline-separated filename string (not ElList)
- __str_char_at: returns Int byte value (not single-char String)
- __args_json: CLI args as JSON array string; seeded by el_seed_init_args()

JSON, state, engram, HTML/URL, serve — thin __ wrappers over el_runtime.c.
Private seed arena (parallel to el_runtime.c arena) for standalone use.
2026-05-03 15:45:52 -05:00
Will Anderson cfa4301026 merge compiler features: break/continue, % modulo, for-range loops 2026-05-03 15:45:45 -05:00
Will Anderson f271f9d9d8 add for-range loops to El (for i in 0..n)
Adds `for i in start..end` (exclusive) and `for i in start..=end`
(inclusive) range loop syntax. Existing `for item in list` iteration
is preserved; the parser branches on DotDot/DotDotEq presence after
the start expression. Lexer adds DotDot and DotDotEq tokens with
longer-match-first priority. Codegen emits a C `for` loop with the
loop variable scoped to the statement; inclusive uses `<=`, exclusive `<`.
2026-05-03 15:44:58 -05:00
Will Anderson 49a8a1c24b add % modulo operator to El lexer, parser, codegen
Lexer and parser already had Percent token and precedence on the
compiler/string-interp branch. This commit adds the missing is_int_expr
case for Percent so that modulo expressions over Int operands are
correctly typed as Int (enabling arithmetic dispatch rather than
falling through to string concat or untyped paths).

binop_to_c already mapped Percent -> % at HEAD; only is_int_expr
needed the Percent arm.
2026-05-03 15:44:19 -05:00
Will Anderson 252ad04c96 add break and continue statements to El 2026-05-03 15:43:20 -05:00
Will Anderson 3cc9b1cc3d merge runtime/util — time, math, state in El 2026-05-03 15:40:55 -05:00
Will Anderson 5678745381 add runtime/time.el, math.el, state.el — time, math, and state in El
Migrates the time, math/float, and in-process state surfaces from
el-compiler/runtime/legacy/el_runtime.c to self-hosted El source:

- runtime/time.el: time_now, sleep_secs/ms, time_to_parts (via pure-El
  Gregorian civil_from_days decomposition), time_format (ISO + strftime
  subset), time_add, time_diff, time_from_parts; full Instant/Duration
  nanosecond API (now, unix_seconds/millis, duration_seconds/millis,
  instant_to_iso8601, sleep_duration); TTL cache (ttl_cache_set/get/age
  backed by state); uuid_new / uuid_v4 via __uuid_v4 seed.

- runtime/math.el: el_abs, el_max, el_min (Int); math_sqrt/log/ln/sin/cos/pi
  (Float seed wrappers); float_to_str, int_to_float, float_to_int, str_to_float,
  format_float (__format_float seed), decimal_round (half-away-from-zero via
  pure-El _pow10/_floor_f helpers).

- runtime/state.el: state_set/get/del/keys thin wrappers over __state_* seeds;
  convenience helpers state_has and state_get_or.
2026-05-03 15:39:48 -05:00
Will Anderson e853f4a25c merge runtime/string — all string operations in El (57 functions) 2026-05-03 15:37:51 -05:00
Will Anderson 5b6915ec9e merge runtime/engram-build — engram wrappers, manifest, seed arity table 2026-05-03 15:37:42 -05:00
Will Anderson b0e38a245a merge runtime/fs-exec-env — filesystem, subprocess, environment in El 2026-05-03 15:37:42 -05:00
Will Anderson 84b5355fce merge runtime/json — JSON operations in El 2026-05-03 15:37:41 -05:00
Will Anderson b547d1daf8 merge runtime/http — HTTP client and server in El 2026-05-03 15:37:41 -05:00
Will Anderson cc6522c69d merge runtime/thread — native El threading model, parallel_map 2026-05-03 15:37:41 -05:00
Will Anderson 5807de835e add runtime/string.el — string operations implemented in El
All string, I/O, math, classification, splitting, joining, counting, padding,
and URL encoding functions from el_runtime.c implemented in El using seed
primitives. No C required; compiles via the normal El pipeline.
2026-05-03 15:37:33 -05:00
Will Anderson 33af4ed09e add runtime/engram.el, manifest.el; register seed builtins in codegen arity table
- runtime/engram.el: thin El wrappers over all __engram_* and __generate
  seed primitives (16 functions), matching the el_seed.c API exactly
- runtime/manifest.el: build manifest documenting module load order and
  the cat+compile+cc command for runtime builds
- el-compiler/src/codegen.el: add 77 __-prefix seed primitive entries to
  builtin_arity, covering str, fs, http, thread, exec, env, time, uuid,
  math, state, html, json, and engram seeds
2026-05-03 15:37:05 -05:00
Will Anderson f2c63f95fd add runtime/fs.el, exec.el, env.el — filesystem, subprocess, environment in El
Migrates fs_read/write/exists/mkdir/write_bytes/list, exec/exec_bg/exec_command/exec_capture,
env/args/exit_program, state_set/get/del/keys, uuid_new/v4, and list helpers get/len from
el_runtime.c into El source as thin wrappers over seed primitives.
2026-05-03 15:35:45 -05:00
Will Anderson 01849c2033 add runtime/thread.el — native El threading model with parallel_map
Introduces El's first-class threading primitives built on the seed layer's
__thread_create/__thread_join/mutex ops. parallel_map is the key deliverable:
spawns one thread per item, joins in order — replaces bash fan-out for room
dispatch and any other concurrent HTTP workload.
2026-05-03 15:35:31 -05:00
Will Anderson 56724325ed add runtime/json.el — JSON operations in El
Thin El wrappers over seed JSON primitives (json_get, json_get_raw,
json_parse, json_stringify, json_set, json_array_len, json_array_get,
json_array_get_string) plus typed extractors (json_get_string/int/float/bool)
and pure-El builders (json_build_object, json_build_array,
json_escape_string) that require no seed call.
2026-05-03 15:35:20 -05:00
Will Anderson fea830cca2 add runtime/http.el — HTTP client and server in El
Thin El wrappers over seed primitives that form the public HTTP API for
El programs. Covers GET/POST/DELETE, header-map variants, binary streaming
to file, form-auth, v1/v2 server dispatch, and http_response envelope
construction. Documents two new seed primitives needed: __http_do_map and
__http_do_map_to_file (ElMap-accepting variants to avoid needing map
iteration in El).
2026-05-03 15:35:04 -05:00
Will Anderson 8642ad3978 remove rust scaffolding — el is the implementation 2026-05-03 04:10:38 -05:00
Will Anderson 6f560de02a remove Rust workspace; El implementation is the canonical engram
Deletes the entire Rust first-pass: Cargo workspace, 10 crates,
engram-data/, engram-data-tx-log/, receptors/, studio/, and examples/.
Keeps: src/server.el, manifest.el, dist/, spec/, README.md,
engram-explainer.html.
2026-05-03 03:25:10 -05:00
Will Anderson 995f29eb42 remove internal .elh files managed by elb 2026-05-02 23:00:31 -05:00
Will Anderson 34725a3988 Rename: nlg → elp (Engram Language Protocol) 2026-05-02 22:15:25 -05:00
Will Anderson a7bbd2f792 merge: add engram CI workflow 2026-05-02 17:46:18 -05:00
Will Anderson 30a86c78d2 add engram CI/CD pipeline — auto-rebuild on push or el-sdk-updated 2026-05-02 17:46:00 -05:00
Will Anderson cbb27b8d87 Add semantics layer bridging intent frames to grammar realization
Introduces semantics.el with SemFrame (sem_frame/sem_frame_simple/sem_frame_obj
constructors), sem_to_spec to convert intent frames into realizer slot maps,
and sem_realize/sem_realize_full as end-to-end frame→text entry points.
Supports intents: assert, query, describe, greet.

Wires generate_frame() into nlg.el and adds 4 new passing tests
(sem-assert, sem-query, sem-describe, sem-greet). All 10 tests pass.
2026-05-02 14:45:54 -05:00
Will Anderson 1432c56cf7 Add native El NLG system: morphology, vocabulary, grammar, realizer
Implements a complete natural language generation stack in El:
- morphology.el: English pluralization, verb conjugation (40+ irregulars), determiner agreement
- vocabulary.el: inline seed lexicon (~100 entries: pronouns, nouns, verbs, adjectives, etc.)
- grammar.el: CFG rules (S/NP/VP/PP), slot-map driven tree generator, s-expression renderer
- realizer.el: semantic form -> English text with tense/aspect/agreement, do-support for questions
- nlg.el: JSON-driven public API tying all modules together
- tests/run.sh: acceptance corpus runner (6 tests, all passing)
2026-05-02 14:16:23 -05:00
will.anderson 020308a29a uncommitted state captured before pushing to Gitea 2026-05-02 10:24:09 -05:00
will.anderson 6e1a338eb6 uncommitted state captured before pushing to Gitea 2026-05-02 10:24:08 -05:00
Will Anderson 834065cf45 server: GET /api/nodes accepts ?node_type=X to filter at the engine
When the query string includes node_type, we route to the new
engram_scan_nodes_by_type_json builtin instead of the unfiltered
scan. Existing callers without the param get identical behaviour.

Smoke-tested live on the neuron engram (3,200+ nodes):
  ?node_type=Knowledge   → all Knowledge
  ?node_type=BacklogItem → all BacklogItem
  ?node_type=Imprint     → 1 Imprint (only one cultivated so far)
  ?node_type=DoesNotExist → []
2026-05-02 01:25:10 -05:00
Will Anderson 3b76f0f8e0 feat: El port — vessels populated alongside Rust
Adds src/main.el + manifest.el for each vessel in this workspace,
ported from the Rust sources during the El consolidation pass on
2026-04-30. Each vessel now has both Rust (legacy) and El (target)
sources side-by-side; Rust will be removed once the El paths are
verified at runtime, vessel by vessel.

Per-vessel work was split across multiple parallel agents reading the
Rust to understand intent, then designing idiomatic El. Not 1:1
transliteration. Each ported vessel includes:
  - manifest.el per spec/language.md \u00a715.1
  - src/main.el with the vessel's public surface
  - Compile verified via dist/platform/elc + cc against the el_runtime

Known gaps surfaced during the port (held for follow-up): HMAC-SHA256
and base64 crypto, HTTP status code in handler returns, request
headers in handler signatures, subprocess primitives, streaming
responses, struct/enum types, browser/JS codegen target. Codegen bug
list of 9 items tracked separately. The El sources here are runtime-
ready under the canonical C runtime; the gaps are language/runtime
extensions still in flight.
2026-04-30 18:18:39 -05:00
Will Anderson be013d2b42 rename crates/ → vessels/ — El's word for buildable units
Per the consolidation onto El: 'crates' is the Rust word, 'vessel' is
El's (per spec/language.md §15). The directory rename is the structural
marker that this slot holds an El buildable unit, even if its current
contents are still Rust pending port.

Mechanical: git mv crates vessels, sed workspace members and any path
dependencies, update CI workflow paths, update README references.
Cross-repo path dependencies (`../foo/crates/bar`) updated workspace-
wide so cargo metadata still resolves where the Rust still builds.
2026-04-30 15:34:20 -05:00
Will Anderson 2ae5dd430f engram: gitignore build artifacts and .el cache 2026-04-30 13:49:38 -05:00
Will Anderson 2b45fc2f0f engram: runtime-native rewrite
Engram is now a thin HTTP face over the El runtime's in-process graph
store. The C runtime owns the data; engram_*_json builtins serialize
results directly. There is no SQL, no SQLite, no db layer, no state
machine — the runtime IS the database.

src/server.el (348 lines, replacing 5797 lines across 15 legacy files):
  GET  /health
  GET  /api/stats
  POST /api/nodes              (auth required)
  GET  /api/nodes
  GET  /api/nodes/:id
  DELETE /api/nodes/:id        (auth required)
  POST /api/edges              (auth required)
  GET  /api/neighbors/:id
  POST /api/activate
  GET  /api/activate
  POST /api/search
  GET  /api/search
  POST /api/strengthen         (auth required)
  POST /api/save               (auth required)
  POST /api/load               (auth required)

Auth: ENGRAM_API_KEY in env. GET routes pass through (read-only).
Mutating routes require {"_auth": "<key>"} in the JSON body until
http_serve surfaces request headers and we can switch to Bearer.

Persistence: engram_save / engram_load via JSON snapshot at
$ENGRAM_DATA_DIR/snapshot.json. Loaded best-effort on startup.

Build: dist/platform/elc src/server.el > dist/engram.c
       cc -std=c11 -O2 -I <runtime> -lcurl -lpthread -o dist/engram
       dist/engram.c <runtime>/el_runtime.c

Live: native binary at dist/engram (113 KB), running under
~/Library/LaunchAgents/ai.neuron.engram.plist on :8742. Verified:
GET /api/stats returns counts; POST /api/nodes with auth creates
node with UUID; GET /api/search returns full node JSON; spreading
activation returns hop-decayed strengths (0.8 × edge × decay per
hop) with epistemic confidence filtering.

Legacy (5797 lines of SQLite-era src) sealed at
~/Archives/engram-src-legacy-20260430.tar.gz and removed from disk.
2026-04-30 13:49:28 -05:00
Will Anderson c16b6ed602 Replace el.toml with manifest.el throughout — El manifests are El, not TOML 2026-04-29 22:48:39 -05:00
Will Anderson 28bc05f29f Update framework spec; add counter and todo examples 2026-04-29 08:50:26 -05:00
Will Anderson 0b480cfb6b El data studio: 10-loop improvement pass — full Engram DB explorer
Full-featured terminal explorer for the Engram knowledge graph built
natively in El. Features:
- ANSI-colored TUI with box-drawing borders and salience bars
- All API endpoints: stats, nodes by type/tier, search, edges,
  spreading activation, node detail with neighbor traversal
- Text report export via fs_write
- Offline/unreachable mode with helpful startup messages
- Interactive mode command reference
- ENGRAM_URL env var for connecting to non-default servers
- Uses json_get_raw for nested JSON object traversal
2026-04-29 04:39:40 -05:00
Will Anderson dc4a9ee95f El IDE: rounds 14-20 — breadcrumb nav, version display, improved search, sticky scroll, status bar diagnostics
Round 14: Breadcrumb directory click — clicking a path segment in the breadcrumb expands/reveals that directory in the file tree
Round 15: El version in status bar — GET /api/status now returns el_version (via el --version), shown in status bar right side; EL_BINARY config env var
Round 16: Search improvements — case-sensitive, whole-word, regex toggles (Alt+C/W/R); project-wide replace-all in current file; backend SearchOpts struct for each mode
Round 17: Sticky scroll improvements — uses CM6 posAtCoords for accurate first-visible-line; clickable to jump to definition; sticky-name/sticky-goto styling
Round 18: File tree header — New File (+) button and Refresh (↺) button in file tree header panel
Round 19: Status bar diagnostics — error count (✕ N) and warning count (⚠ N) shown in status bar, clickable to jump to problems panel
Round 20: Polish — more El snippets (test, seed, assert, activate, parallel, deploy, import, with, retry, reason, trace), expanded command palette (11 new commands)
2026-04-29 04:38:53 -05:00
Will Anderson 12e537d6ab El IDE: 10-round pass — syntax highlighting, file browser, runner, completion, split panes, find/replace, settings, minimap
Round 1: Fix dependency paths (../el/crates → ../el/engrams), verify build
Round 2: Enhanced syntax highlighting — function call detection, all El keywords (activate, sealed, parallel, deploy, etc.)
Round 3: Full El keyword set in CodeMirror tokenizer and completions; 50+ builtin function completions with type signatures
Round 4: File system integration — mkdir, rename, delete, file tree search; git status badges
Round 5: Runner integration — Ctrl+R shortcut, SSE streaming output, clickable error lines with jump-to-line
Round 6: Error highlighting with accurate line/col from lexer/parser spans; diagnostic dedup
Round 7: Find/replace panel; Ctrl+G go-to-line; toggle line comment; word-wrap compartment fix
Round 8: Code completion — 50+ builtins, keyword completions, snippet completions, server snippet integration
Round 9: Resizable panels — file tree drag-resize + collapse (Ctrl+B), type-graph drag-resize, bottom panel toggle (Ctrl+J), width persistence
Round 10: Settings API (GET/POST/DELETE /api/settings, ~/.el-ide/settings.json); frontend wired to API with debounced save; theme persistence
Round 11: Minimap click-to-jump and drag-to-scroll
Round 12: Command palette — added Go To Line, Toggle Word Wrap/Minimap/File Tree/Bottom Panel, font size commands, New File, Select Next Occurrence
Round 13: Multi-cursor — Ctrl+D select next occurrence, EditorSelection exposed for multi-range selection
2026-04-29 04:34:08 -05:00
Will Anderson 909c1577f1 rename crates/ to engrams/, bindings/ to receptors/
- crates/ → engrams/ (Rust engrams live here)
- bindings/ → receptors/ (cross-language access points into the graph)
- Cargo.toml workspace paths updated
2026-04-29 03:27:33 -05:00
Will Anderson ea0de16562 Transform el-ide into AI-native IDE: Neuron pair programming, live type graph, semantic autocomplete, activation preview
- Neuron pair programming panel (right column, 300px): full conversation interface
  with code-block rendering, "Insert at cursor" for AI suggestions, quick actions
  (explain selection, find activations, analyze types), thinking indicator
- Live activation preview: floating inline widget when cursor is inside
  `activate TypeName where "..."` expressions; queries /api/lsp/activate-preview
- Type graph activation highlighting: `activate` statements cause named types to
  pulse with purple glow rings; animation loop runs while activations are present
- Knowledge Graph Explorer: new bottom panel tab; search the Engram DB for semantic
  concepts, insert activate expressions, ask Neuron about nodes
- Semantic autocomplete: completions with score >= 0.8 get "⟁ semantic" prefix and
  priority boost in the completion list
- Backend: /api/reason extended to support three modes: "hypothesis" (existing),
  "pair" (proxies to soma /v1/chat/completions with full file context + system
  prompt), "knowledge-search" (proxies to Engram DB search endpoint)
- Backend: new GET /api/lsp/activate-preview endpoint queries Engram for nodes
  matching a partial activate expression
- Type graph node click now navigates editor to the type definition
- Resolved merge conflicts in engram-lang/crates (ast.rs, parser.rs, checker.rs,
  types.rs): took union of HEAD and worktree-agent branches
2026-04-28 11:40:36 -05:00
Will Anderson 361c958618 add el-style, el-layout, el-i18n, el-config, el-secrets: responsive by default, theme-driven, zero breakpoints 2026-04-27 20:18:47 -05:00
Will Anderson a1159eec65 add el-identity: Engram-native identity, OAuth, @authenticate by default 2026-04-27 20:04:52 -05:00
Will Anderson 69d1085d2d el-ui v2: universal platform, service bindings, AOP, auth, publish pipeline 2026-04-27 19:52:29 -05:00
Will Anderson 3bf3c02854 feat: el-ui — activation-based frontend framework, spreading activation reactivity, graph state 2026-04-27 19:15:53 -05:00
Will Anderson 602cd1586a feat: el-ide — native IDE for engram-lang, LSP, type graph, plugin ecosystem
Axum HTTP server (port 7771) serving a single-page IDE with CodeMirror 6
syntax highlighting for engram-lang, a force-directed type graph visualizer,
LSP (completions, hover, diagnostics), SSE-streamed build/run output, a
plugin host with five first-party plugins, and a reasoning panel that proxies
to engram-server. 28 tests across three crates, zero warnings.
2026-04-27 19:12:42 -05:00
Will Anderson 61a4632163 feat: engram-reasoning — graph-native inference engine, evidence chains, confidence propagation 2026-04-27 18:36:37 -05:00
Will Anderson 192528543f feat: schema projections, command transactions, quantum-secure encryption 2026-04-27 18:26:46 -05:00
Will Anderson 69410a6908 feat: serve studio from engram-server — browser is the runtime, no Electron 2026-04-27 17:30:20 -05:00
Will Anderson 6601761cd9 feat: Engram sync layer — swarm memory protocol, peer delta sync, distributed activation 2026-04-27 17:18:51 -05:00
Will Anderson 2454c83e82 feat: HNSW index, consolidation engine, Kotlin/TS/Go bindings, SQLite migration connector
- vector.rs: replace flat O(n) scan with instant-distance HNSW for stores
  >= 100 nodes; flat scan retained as fallback for small graphs; dirty-flag
  persistence in sled triggers index rebuild only when nodes are added

- consolidation.rs: Episodic → Semantic promotion based on activation_count
  and salience_floor thresholds; global decay pass after each cycle;
  ConsolidationConfig + ConsolidationReport types; 8 tests

- migration.rs: reads Neuron SQLite (memory_nodes, knowledge_entries,
  graph_edges) and writes to Engram sled; placeholder unit-vector embeddings
  with TODO for ONNX; 5 tests including full in-memory DB roundtrip

- crates/engram-migrate: CLI binary (engram-migrate --sqlite / --output)

- crates/engram-jni: JNI cdylib exposing open/close/put_node/get_node/
  activate/search_embedding/touch/decay/node_count/edge_count via
  Java_ai_neuron_engram_EngramDb_* entry points; 6 tests

- bindings/kotlin: EngramDb.kt (AutoCloseable JNI wrapper), EngramNode,
  EngramEdge, ActivatedNode, EngramTypes; build.gradle.kts; settings.gradle.kts

- bindings/typescript: engram-wasm crate (wasm-bindgen, serde-wasm-bindgen);
  WasmEngramDb with in-memory backend (sled not available in WASM);
  TypeScript wrapper (index.ts, types.ts, package.json, tsconfig.json)

- bindings/go: engram.go (CGo wrapper), engram.h (C header), engram_test.go
  (4 tests covering open/close/put_node/get_node/node_count/decay); go.mod

- engram-core: wasm feature gate for in-memory backend; mem_storage.rs;
  activation.activate_mem for WASM path; Node::with_id helper;
  salience.rs doctest fixed (text block)

- examples/basic.rs: consolidation section added
- examples/migrate.rs: migration API demonstration

Build: cargo build --workspace -- zero warnings, zero errors
Tests: 38 pass (25 engram-core + 7 engram-ffi + 6 engram-jni)
2026-04-27 16:00:47 -05:00
Will Anderson 1a609502c8 init: Engram v0.1 — native memory substrate for accumulating intelligence
Memory is not stored and retrieved — it is activated and propagated.
Implements the spreading activation model with salience decay, typed edges,
four memory tiers, and flat cosine vector search over a sled embedded store.
2026-04-27 15:37:42 -05:00
568 changed files with 275399 additions and 2674 deletions
+248 -37
View File
@@ -1,4 +1,4 @@
name: El CI dev
name: El SDK CI - dev
on:
push:
@@ -11,6 +11,9 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
@@ -19,83 +22,291 @@ jobs:
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
> /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
run: |
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
dist/elc-gen2.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen3 (self-hosted) elc built"
echo "gen2 (self-hosted) elc built"
dist/platform/elc --version || true
# Run all four test suites — all must pass
- name: Run tests — text
# Build elb (needed for Artifact Registry publish and downstream CI)
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
- name: Run tests - text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests calendar
- name: Run tests - calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests time
- name: Run tests - time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests html_sanitizer
- name: Run tests - html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Publish artifact to GCP Artifact Registry (dev)
- name: Publish elc to Artifact Registry (dev)
# Native El test suites (elc --test, compile-link-run)
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
- name: Precompile el_runtime.o
run: |
set -euo pipefail
RUNTIME="$(pwd)/el-compiler/runtime"
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
-o /tmp/el_runtime.o
echo "el_runtime.o compiled"
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
# Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (dev)
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
VERSION="${GITHUB_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--package=el-elc \
--version="${VERSION}" \
--source=dist/platform/elc
# Also tag as latest-dev
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elb \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-dev"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (dev)
# Patches ci-base:dev in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
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
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:dev (or fall back to :latest on first run)
BASE_TAG="dev"
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:dev" \
-t "${CI_BASE}:dev-${SHA}" \
.
docker push "${CI_BASE}:dev"
docker push "${CI_BASE}:dev-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
rm -f /tmp/gcp-key.json
+228 -32
View File
@@ -1,4 +1,4 @@
name: El CI stage
name: El SDK CI - stage
on:
push:
@@ -11,90 +11,286 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (stage <- dev only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "dev" ]; then
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} -> stage"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
run: |
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
dist/elc-gen2.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen3 (self-hosted) elc built"
echo "gen2 (self-hosted) elc built"
dist/platform/elc --version || true
# Run all four test suites — all must pass
- name: Run tests — text
- name: Run tests - text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests calendar
- name: Run tests - calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests time
- name: Run tests - time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests html_sanitizer
- name: Run tests - html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Publish artifact to GCP Artifact Registry (stage)
- name: Publish elc to Artifact Registry (stage)
# Native El test suites (elc --test, compile-link-run)
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Build elb (needed for epm and el-install builds below)
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
# Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (stage)
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
VERSION="${GITHUB_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--package=el-elc \
--version="${VERSION}" \
--source=dist/platform/elc
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
echo "Published El SDK version=${VERSION} to foundation-stage"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (stage)
# Patches ci-base:stage in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
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
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:stage (system deps stay cached in the base layer)
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:stage" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:stage" \
-t "${CI_BASE}:stage-${SHA}" \
.
docker push "${CI_BASE}:stage"
docker push "${CI_BASE}:stage-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
rm -f /tmp/gcp-key.json
+286 -72
View File
@@ -4,81 +4,234 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-release:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (main <- stage only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "stage" ]; then
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} -> main"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
run: |
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
dist/elc-gen2.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen3 (self-hosted) elc built"
echo "gen2 (self-hosted) elc built"
dist/platform/elc --version || true
# Run all four test suites with gen3 elc
- name: Run tests — text
# Build elb binary
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
- name: Run tests - text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests calendar
- name: Run tests - calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests time
- name: Run tests - time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests html_sanitizer
- name: Run tests - html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Publish / update the `latest` release with the three SDK assets
# Native El test suites (elc --test, compile-link-run)
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
- name: Bundle SDK tarball
if: github.event_name == 'push'
working-directory: ${{ github.workspace }}
run: |
mkdir -p dist/sdk/bin dist/sdk/runtime
cp lang/dist/platform/elc dist/sdk/bin/elc
cp lang/dist/bin/elb dist/sdk/bin/elb
cp lang/dist/bin/epm dist/sdk/bin/epm
cp lang/el-compiler/runtime/el_runtime.c dist/sdk/runtime/
cp lang/el-compiler/runtime/el_runtime.h dist/sdk/runtime/
cp lang/runtime/*.el dist/sdk/runtime/
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
ls -lh dist/el-sdk-latest.tar.gz
# Publish / update the `latest` release with all SDK assets
- name: Publish latest release
if: github.event_name == 'push'
working-directory: ${{ github.workspace }}
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
REPO: neuron-technologies/el
run: |
# Delete existing `latest` release if it exists
EXISTING_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/releases/tags/latest" \
@@ -91,12 +244,10 @@ jobs:
"${GITEA_API}/repos/${REPO}/releases/${EXISTING_ID}"
fi
# Delete and re-create the `latest` tag so it points at HEAD
curl -sf -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/tags/latest" || true
# Create the release
RELEASE_ID=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
@@ -111,7 +262,6 @@ jobs:
echo "Created release id=${RELEASE_ID}"
# Upload assets
upload_asset() {
local filepath="$1"
local name="$2"
@@ -122,70 +272,134 @@ jobs:
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
}
upload_asset dist/platform/elc elc
upload_asset el-compiler/runtime/el_runtime.c el_runtime.c
upload_asset el-compiler/runtime/el_runtime.h el_runtime.h
# Per-file assets (downstream CI needs these individually)
upload_asset lang/dist/platform/elc elc
upload_asset lang/el-compiler/runtime/el_runtime.c el_runtime.c
upload_asset lang/el-compiler/runtime/el_runtime.h el_runtime.h
# SDK bundle and installer binary
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
upload_asset lang/dist/bin/el-install el-install
echo "Release published successfully"
# Dispatch el-sdk-updated event to downstream repos
# Publish artifact to GCP Artifact Registry (prod)
- name: Publish elc to Artifact Registry (prod)
- name: Publish El SDK to Artifact Registry (prod)
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${GITEA_SHA:0:8}"
VERSION="${GITHUB_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--package=el-elc \
--version="${VERSION}" \
--source=dist/platform/elc
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elb \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-prod"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK
# Patches ci-base:latest in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
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
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base (system deps stay cached in the base layer)
docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:latest" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:latest" \
-t "${CI_BASE}:${SHA}" \
.
docker push "${CI_BASE}:latest"
docker push "${CI_BASE}:${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
rm -f /tmp/gcp-key.json
- name: Dispatch to foundation/engram
- name: Dispatch el-sdk-updated to downstream repos
if: github.event_name == 'push'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/neuron-technologies/engram/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to foundation/engram"
- name: Dispatch to neuron-technologies/forge
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/neuron-technologies/forge/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/${repo}/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {\"el_version\": \"latest\", \"commit\": \"${GITHUB_SHA}\"}
}" && echo "Dispatched to ${repo}" || echo "Warning: dispatch to ${repo} failed"
done
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# El pre-commit hook: compile and run native tests before commit.
# Install once per clone: git config core.hooksPath .githooks
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
LANG_DIR="$ROOT/lang"
RUNTIME="$LANG_DIR/el-compiler/runtime"
ELC="$LANG_DIR/dist/platform/elc"
# If elc isn't built yet, skip with a warning rather than blocking
if [ ! -x "$ELC" ]; then
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
echo " Build it first: cd lang && gcc -O2 -I el-compiler/runtime dist/elc-bootstrap.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I el-compiler/runtime /tmp/elc.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
exit 0
fi
echo "→ Running El native tests..."
PASS=0
FAIL=0
FAILED_TESTS=""
for test_file in "$LANG_DIR"/tests/native/test_*.el; do
name=$(basename "$test_file" .el)
tmp_c="/tmp/el_hook_${name}.c"
tmp_bin="/tmp/el_hook_${name}"
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
&& "$tmp_bin" 2>/dev/null; then
PASS=$((PASS + 1))
else
echo " ✗ $name"
FAIL=$((FAIL + 1))
FAILED_TESTS="$FAILED_TESTS $name"
fi
done
echo " $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "✗ Pre-commit failed. Fix these tests before committing:$FAILED_TESTS"
exit 1
fi
echo "✓ All tests passed"
exit 0
+23
View File
@@ -0,0 +1,23 @@
// arbor-cli the `arbor` command-line tool.
// Inlines its own copies of the parse / layout / render pipeline so that the
// resulting binary is self-contained. (El's `import` form today concatenates
// source; once a real module loader lands this becomes a thin driver.)
vessel "arbor-cli" {
version "0.1.0"
description "Command-line interface for the Arbor diagram language"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
arbor-parse "0.1"
arbor-layout "0.1"
arbor-render "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
// arbor-core fundamental types for Arbor diagrams.
// Node IDs (sanitised), shape vocabulary, edge kinds, and the lightweight
// graph value used by every other vessel.
vessel "arbor-core" {
version "0.1.0"
description "Core types for Arbor diagrams: NodeId, ArborShape, ArborEdgeKind, graphs"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
}
build {
entry "src/main.el"
output "dist/"
}
+333
View File
@@ -0,0 +1,333 @@
// arbor-core core types for Arbor diagrams.
//
// Idiomatic El: everything is a Map. Functions take/return maps; helpers are
// pure and small. The downstream vessels (parse, layout, render) consume the
// shapes defined here.
//
// Shape vocabulary:
// ArborShape strings "rect" "rounded" "cylinder" "diamond" "stadium" "primary"
//
// Edge-kind strings:
// "solid" "dashed" "forbidden" "bidirectional"
//
// Node value: { "id":Str, "label":Str, "shape":Str }
// Edge value: { "from":Str, "to":Str, "label":Str, "kind":Str }
// Group value: { "id":Str, "label":Str, "node_ids":[Str], "direction":Str }
// Graph value: { "title":Str, "direction":Str, "nodes":[Node], "edges":[Edge], "groups":[Group] }
//
// Diagram-form (lowered) is the same shape but with NodeStyle/EdgeLine/Arrow
// resolved into renderer-friendly fields:
// Node: + "sublabel":Str, "style_fill":Str, "style_stroke":Str, "style_color":Str
// Edge: + "line":Str ("solid"/"dashed"/"dotted"/"thick"), "arrow":Str ("forward"/"backward"/"both"/"none")
//
// This file is the canonical definition of those shapes. Other vessels rely on
// these field names.
// NodeId sanitisation
//
// Sanitise an arbitrary string into a Mermaid-safe identifier.
// - any char not in [a-zA-Z0-9_] becomes '_'
// - consecutive underscores collapse
// - trailing underscores stripped
// - if first char is a digit, prepend 'n'
// - if empty, return "node"
fn is_alnum_underscore(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
if code >= 65 {
if code <= 90 { return true }
}
if code >= 97 {
if code <= 122 { return true }
}
if code == 95 { return true }
false
}
fn is_ascii_digit(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
false
}
fn sanitize_id(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "node" }
// Pass 1: replace and collapse.
let out = ""
let prev_underscore = false
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if is_alnum_underscore(ch) {
let out = out + ch
let prev_underscore = false
} else {
if !prev_underscore {
let out = out + "_"
}
let prev_underscore = true
}
let i = i + 1
}
// Pass 2: strip trailing underscores.
let m: Int = str_len(out)
let end = m
let stripping = true
while stripping {
if end <= 0 {
let stripping = false
} else {
let last: String = str_char_at(out, end - 1)
if last == "_" {
let end = end - 1
} else {
let stripping = false
}
}
}
let out = str_slice(out, 0, end)
if str_len(out) == 0 { return "node" }
// Pass 3: leading-digit guard.
let first: String = str_char_at(out, 0)
if is_ascii_digit(first) {
let out = "n" + out
}
out
}
// Constructors
fn make_node(id: String, label: String, shape: String) -> Map<String, Any> {
{ "id": id, "label": label, "shape": shape }
}
fn make_edge(src: String, dst: String, kind: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": "", "kind": kind }
}
fn make_edge_with_label(src: String, dst: String, kind: String, label: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": label, "kind": kind }
}
fn make_group(id: String, label: String) -> Map<String, Any> {
let empty_ids: [String] = el_list_empty()
{ "id": id, "label": label, "node_ids": empty_ids, "direction": "" }
}
fn make_graph() -> Map<String, Any> {
let empty_n: [Map<String, Any>] = el_list_empty()
let empty_e: [Map<String, Any>] = el_list_empty()
let empty_g: [Map<String, Any>] = el_list_empty()
{ "title": "", "direction": "top-down",
"nodes": empty_n, "edges": empty_e, "groups": empty_g }
}
// Shape vocabulary
// Returns the canonical shape string for a token, or "" if unknown.
fn shape_from_token(tok: String) -> String {
let t: String = str_trim(tok)
if t == "rect" { return "rect" }
if t == "rounded" { return "rounded" }
if t == "cylinder" { return "cylinder" }
if t == "diamond" { return "diamond" }
if t == "stadium" { return "stadium" }
if t == "primary" { return "primary" }
""
}
// Lower an Arbor shape into the renderer's NodeShape vocabulary.
fn shape_to_node_shape(shape: String) -> String {
if shape == "rect" { return "rectangle" }
if shape == "primary" { return "rectangle" }
if shape == "rounded" { return "rounded_rect" }
if shape == "cylinder" { return "cylinder" }
if shape == "diamond" { return "diamond" }
if shape == "stadium" { return "stadium" }
"rectangle"
}
// Lowering: ArborGraph DiagramGraph
//
// Replaces every node with a diagram-form node carrying explicit style fields,
// and every edge with a diagram-form edge carrying line/arrow strings.
fn lower_node(n: Map<String, Any>) -> Map<String, Any> {
let shape: String = n["shape"]
let node_shape: String = shape_to_node_shape(shape)
let fill = ""
let stroke = ""
let color = ""
if shape == "primary" {
let fill = "#0052A0"
let stroke = "#0052A0"
let color = "#ffffff"
}
{ "id": n["id"], "label": n["label"], "sublabel": "",
"shape": node_shape,
"style_fill": fill, "style_stroke": stroke, "style_color": color }
}
fn lower_edge(e: Map<String, Any>) -> Map<String, Any> {
let kind: String = e["kind"]
let line = "solid"
let arrow = "forward"
if kind == "dashed" {
let line = "dashed"
}
if kind == "bidirectional" {
let arrow = "both"
}
// forbidden uses solid line + forward arrow; the renderer overlays the
// circle-X marker based on a forbidden-set the caller threads through.
{ "from": e["from"], "to": e["to"], "label": e["label"],
"line": line, "arrow": arrow }
}
fn lower_graph(g: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = g["nodes"]
let edges: [Map<String, Any>] = g["edges"]
let lowered_nodes: [Map<String, Any>] = el_list_empty()
let i = 0
let n: Int = el_list_len(nodes)
while i < n {
let lowered_nodes = native_list_append(lowered_nodes, lower_node(get(nodes, i)))
let i = i + 1
}
let lowered_edges: [Map<String, Any>] = el_list_empty()
let i = 0
let m: Int = el_list_len(edges)
while i < m {
let lowered_edges = native_list_append(lowered_edges, lower_edge(get(edges, i)))
let i = i + 1
}
{ "title": g["title"], "direction": g["direction"],
"nodes": lowered_nodes, "edges": lowered_edges, "groups": g["groups"] }
}
// Find a node by id within a (lowered or raw) graph. Returns an empty map
// when not found callers check map_get(result, "id") for presence.
fn graph_find_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let i = 0
while i < n {
let node: Map<String, Any> = get(nodes, i)
let nid: String = node["id"]
if nid == id { return node }
let i = i + 1
}
let empty: Map<String, Any> = el_map_new(0)
empty
}
// Forbidden-edge set helpers
// The lowered graph drops the "forbidden" kind (line/arrow have no slot for
// it). Callers preserve the set as a list of "from->to" strings.
fn forbidden_key(from: String, to: String) -> String {
from + "->" + to
}
fn collect_forbidden(graph: Map<String, Any>) -> [String] {
let edges: [Map<String, Any>] = graph["edges"]
let n: Int = el_list_len(edges)
let out: [String] = el_list_empty()
let i = 0
while i < n {
let e: Map<String, Any> = get(edges, i)
let kind: String = e["kind"]
if kind == "forbidden" {
let f: String = e["from"]
let t: String = e["to"]
let out = native_list_append(out, forbidden_key(f, t))
}
let i = i + 1
}
out
}
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
let key: String = forbidden_key(src, dst)
let n: Int = el_list_len(set)
let i = 0
while i < n {
let s: String = get(set, i)
if s == key { return true }
let i = i + 1
}
false
}
// Smoke test
//
// State is kept in process-local k/v storage so we never mix Int + Call or
// Int + Ident in `+` (which the codegen heuristic emits as string concat
// on tagged-pointer values, segfaulting on Int operands).
fn fail(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label + " = " + got)
return 1
}
fail(label, got, want)
}
check_eq("sanitize crates/nc-core",
sanitize_id("crates/nc-core"), "crates_nc_core")
check_eq("sanitize package.json",
sanitize_id("package.json"), "package_json")
check_eq("sanitize 42-module",
sanitize_id("42-module"), "n42_module")
check_eq("sanitize empty", sanitize_id(""), "node")
check_eq("sanitize !!--@@", sanitize_id("!!--@@"), "node")
check_eq("shape_from_token rounded",
shape_from_token("rounded"), "rounded")
check_eq("shape_to_node_shape primary",
shape_to_node_shape("primary"), "rectangle")
// Lowering preserves a node id and adds style.
let n: Map<String, Any> = make_node("svc", "Service", "primary")
let ln: Map<String, Any> = lower_node(n)
check_eq("lower preserves id", ln["id"], "svc")
check_eq("lower applies primary fill", ln["style_fill"], "#0052A0")
// Edge lowering
let e: Map<String, Any> = make_edge("a", "b", "dashed")
let le: Map<String, Any> = lower_edge(e)
check_eq("lower edge dashed line", le["line"], "dashed")
let e2: Map<String, Any> = make_edge("a", "b", "bidirectional")
let le2: Map<String, Any> = lower_edge(e2)
check_eq("lower edge bidirectional arrow", le2["arrow"], "both")
println("")
let failures: String = state_get("failures")
if str_eq(failures, "1") {
println("arbor-core: FAILED")
exit_program(1)
} else {
println("arbor-core: ok")
}
+19
View File
@@ -0,0 +1,19 @@
// arbor-diagram diagram intermediate representation + Mermaid serializer
// + dependency-graph builders. Consumes raw graph values built by arbor-core
// or arbor-parse and produces Mermaid markup or other serializations.
vessel "arbor-diagram" {
version "0.1.0"
description "Diagram IR + Mermaid serializer + architecture diagram builders"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
+433
View File
@@ -0,0 +1,433 @@
// arbor-diagram diagram intermediate representation (AST + IR).
//
// Where arbor-core supplies the *.arbor source-language model Mermaid-safe
// IDs, ArborShape strings, ArborEdgeKind strings, and the lowered "diagram-
// form" map arbor-diagram exposes the same lowered model as the canonical
// IR for downstream serializers (arbor-render and any future Mermaid-style
// emitter). The two vessels overlap by design: arbor-core is responsible for
// *naming* the schema; arbor-diagram is responsible for *building* values
// against it.
//
// The Rust crate ships small AST builder structs (`DiagramNode::new`,
// `DiagramEdge::with_label`, `DiagramGraph::add_node`). El has no method
// chaining, no Default::default(), no enum types. The El idiom is a stack
// of immutable maps with explicit constructor + with_* helpers that take
// the value and return a freshly-allocated map.
//
// Public surface:
// make_node(id, label) DiagramNode
// with_shape(node, shape) DiagramNode
// with_sublabel(node, sublabel) DiagramNode
// with_style(node, fill, stroke, color) DiagramNode
//
// make_edge(from, to) DiagramEdge
// with_label(edge, label)
// with_line(edge, line) // "solid"/"dashed"/"dotted"/"thick"
// with_arrow(edge, arrow) // "forward"/"backward"/"both"/"none"
//
// make_group(id, label) DiagramGroup
// with_node(group, node_id)
// with_nodes(group, [node_id])
// with_direction(group, dir)
//
// make_graph(title) DiagramGraph
// with_direction(graph, dir)
// graph_add_node(graph, node) DiagramGraph
// graph_add_edge(graph, edge) DiagramGraph
// graph_add_group(graph, group) DiagramGraph
// graph_node(graph, id) DiagramNode | empty map
//
// Shape vocabulary (lowered): see arbor-core. The local copy here mirrors
// the table in arbor-core/src/main.el so this vessel is hermetic.
// NodeShape vocabulary
fn node_shape_rectangle() -> String { "rectangle" }
fn node_shape_rounded_rect() -> String { "rounded_rect" }
fn node_shape_stadium() -> String { "stadium" }
fn node_shape_cylinder() -> String { "cylinder" }
fn node_shape_diamond() -> String { "diamond" }
fn node_shape_parallelogram() -> String { "parallelogram" }
fn node_shape_database() -> String { "database" }
fn node_shape_subroutine() -> String { "subroutine" }
fn node_shape_valid(s: String) -> Bool {
if str_eq(s, "rectangle") { return true }
if str_eq(s, "rounded_rect") { return true }
if str_eq(s, "stadium") { return true }
if str_eq(s, "cylinder") { return true }
if str_eq(s, "diamond") { return true }
if str_eq(s, "parallelogram") { return true }
if str_eq(s, "database") { return true }
if str_eq(s, "subroutine") { return true }
false
}
// EdgeLine vocabulary
fn edge_line_solid() -> String { "solid" }
fn edge_line_dashed() -> String { "dashed" }
fn edge_line_dotted() -> String { "dotted" }
fn edge_line_thick() -> String { "thick" }
fn edge_line_valid(s: String) -> Bool {
if str_eq(s, "solid") { return true }
if str_eq(s, "dashed") { return true }
if str_eq(s, "dotted") { return true }
if str_eq(s, "thick") { return true }
false
}
// EdgeArrow vocabulary
fn edge_arrow_forward() -> String { "forward" }
fn edge_arrow_backward() -> String { "backward" }
fn edge_arrow_both() -> String { "both" }
fn edge_arrow_none() -> String { "none" }
fn edge_arrow_valid(s: String) -> Bool {
if str_eq(s, "forward") { return true }
if str_eq(s, "backward") { return true }
if str_eq(s, "both") { return true }
if str_eq(s, "none") { return true }
false
}
// Direction vocabulary
fn direction_top_down() -> String { "top-down" }
fn direction_left_right() -> String { "left-right" }
fn direction_right_left() -> String { "right-left" }
fn direction_bottom_up() -> String { "bottom-up" }
fn direction_valid(s: String) -> Bool {
if str_eq(s, "top-down") { return true }
if str_eq(s, "left-right") { return true }
if str_eq(s, "right-left") { return true }
if str_eq(s, "bottom-up") { return true }
false
}
// DiagramNode
fn make_node(id: String, label: String) -> Map<String, Any> {
{
"id": id,
"label": label,
"sublabel": "",
"shape": "rectangle",
"style_fill": "",
"style_stroke": "",
"style_color": ""
}
}
fn with_shape(node: Map<String, Any>, shape: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": node["sublabel"],
"shape": shape,
"style_fill": node["style_fill"],
"style_stroke": node["style_stroke"],
"style_color": node["style_color"]
}
}
fn with_sublabel(node: Map<String, Any>, sublabel: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": sublabel,
"shape": node["shape"],
"style_fill": node["style_fill"],
"style_stroke": node["style_stroke"],
"style_color": node["style_color"]
}
}
fn with_style(node: Map<String, Any>, fill: String, stroke: String, color: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": node["sublabel"],
"shape": node["shape"],
"style_fill": fill,
"style_stroke": stroke,
"style_color": color
}
}
// DiagramEdge
fn make_edge(from: String, to: String) -> Map<String, Any> {
{
"from": from,
"to": to,
"label": "",
"line": "solid",
"arrow": "forward"
}
}
fn with_label(edge: Map<String, Any>, label: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": label,
"line": edge["line"],
"arrow": edge["arrow"]
}
}
fn with_line(edge: Map<String, Any>, line: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": edge["label"],
"line": line,
"arrow": edge["arrow"]
}
}
fn with_arrow(edge: Map<String, Any>, arrow: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": edge["label"],
"line": edge["line"],
"arrow": arrow
}
}
// DiagramGroup
fn make_group(id: String, label: String) -> Map<String, Any> {
let empty: [String] = native_list_empty()
{
"id": id,
"label": label,
"node_ids": empty,
"direction": ""
}
}
fn with_node(group: Map<String, Any>, node_id: String) -> Map<String, Any> {
let cur: [String] = group["node_ids"]
let next: [String] = native_list_append(cur, node_id)
{
"id": group["id"],
"label": group["label"],
"node_ids": next,
"direction": group["direction"]
}
}
fn with_nodes(group: Map<String, Any>, ids: [String]) -> Map<String, Any> {
let cur: [String] = group["node_ids"]
let n: Int = el_list_len(ids)
let i = 0
while i < n {
let cur = native_list_append(cur, get(ids, i))
let i = i + 1
}
{
"id": group["id"],
"label": group["label"],
"node_ids": cur,
"direction": group["direction"]
}
}
fn with_group_direction(group: Map<String, Any>, dir: String) -> Map<String, Any> {
{
"id": group["id"],
"label": group["label"],
"node_ids": group["node_ids"],
"direction": dir
}
}
// DiagramGraph
fn make_graph(title: String) -> Map<String, Any> {
let empty_n: [Map<String, Any>] = native_list_empty()
let empty_e: [Map<String, Any>] = native_list_empty()
let empty_g: [Map<String, Any>] = native_list_empty()
{
"title": title,
"direction": "top-down",
"nodes": empty_n,
"edges": empty_e,
"groups": empty_g
}
}
fn with_direction(graph: Map<String, Any>, dir: String) -> Map<String, Any> {
{
"title": graph["title"],
"direction": dir,
"nodes": graph["nodes"],
"edges": graph["edges"],
"groups": graph["groups"]
}
}
fn graph_add_node(graph: Map<String, Any>, node: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["nodes"]
let next: [Map<String, Any>] = native_list_append(cur, node)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": next,
"edges": graph["edges"],
"groups": graph["groups"]
}
}
fn graph_add_edge(graph: Map<String, Any>, edge: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["edges"]
let next: [Map<String, Any>] = native_list_append(cur, edge)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": graph["nodes"],
"edges": next,
"groups": graph["groups"]
}
}
fn graph_add_group(graph: Map<String, Any>, group: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["groups"]
let next: [Map<String, Any>] = native_list_append(cur, group)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": graph["nodes"],
"edges": graph["edges"],
"groups": next
}
}
// Find a node by id. Returns an empty map (no "id" field) when not present.
fn graph_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
if str_eq(nid, id) { return nd }
let i = i + 1
}
let empty: Map<String, Any> = el_map_new(0)
empty
}
// Smoke test
fn fail(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("smoke_failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label + " = " + got)
return 1
}
fail(label, got, want)
}
// Vocabulary self-checks
check_eq("shape rectangle valid",
bool_to_str(node_shape_valid("rectangle")), "true")
check_eq("shape hexagon invalid",
bool_to_str(node_shape_valid("hexagon")), "false")
check_eq("line dashed valid",
bool_to_str(edge_line_valid("dashed")), "true")
check_eq("arrow both valid",
bool_to_str(edge_arrow_valid("both")), "true")
check_eq("dir top-down valid",
bool_to_str(direction_valid("top-down")), "true")
// Node builder
let n0: Map<String, Any> = make_node("svc", "Service")
check_eq("node default shape", n0["shape"], "rectangle")
check_eq("node default sublabel empty", n0["sublabel"], "")
let n1: Map<String, Any> = with_shape(n0, "cylinder")
check_eq("node with_shape", n1["shape"], "cylinder")
check_eq("node id preserved", n1["id"], "svc")
let n2: Map<String, Any> = with_sublabel(n1, "v0.1.0")
check_eq("node with_sublabel", n2["sublabel"], "v0.1.0")
let n3: Map<String, Any> = with_style(n2, "#0052A0", "#0052A0", "#ffffff")
check_eq("node style fill", n3["style_fill"], "#0052A0")
check_eq("node style color", n3["style_color"], "#ffffff")
// Edge builder
let e0: Map<String, Any> = make_edge("a", "b")
check_eq("edge default line", e0["line"], "solid")
check_eq("edge default arrow", e0["arrow"], "forward")
let e1: Map<String, Any> = with_line(e0, "dashed")
let e2: Map<String, Any> = with_arrow(e1, "both")
let e3: Map<String, Any> = with_label(e2, "calls")
check_eq("edge line", e3["line"], "dashed")
check_eq("edge arrow", e3["arrow"], "both")
check_eq("edge label", e3["label"], "calls")
// Group builder
let g0: Map<String, Any> = make_group("core", "Application Core")
let g1: Map<String, Any> = with_node(g0, "api")
let g2: Map<String, Any> = with_node(g1, "svc")
let ids2: [String] = g2["node_ids"]
check_eq("group with two nodes", int_to_str(el_list_len(ids2)), "2")
let g3: Map<String, Any> = make_group("infra", "Infrastructure")
let extras: [String] = native_list_empty()
let extras = native_list_append(extras, "db")
let extras = native_list_append(extras, "cache")
let g4: Map<String, Any> = with_nodes(g3, extras)
let ids4: [String] = g4["node_ids"]
check_eq("group with_nodes appends", int_to_str(el_list_len(ids4)), "2")
// Graph builder + lookup
let G0: Map<String, Any> = make_graph("System")
let G1: Map<String, Any> = with_direction(G0, "left-right")
let G2: Map<String, Any> = graph_add_node(G1, n3)
let nb: Map<String, Any> = make_node("b", "Backend")
let G3: Map<String, Any> = graph_add_node(G2, nb)
let G4: Map<String, Any> = graph_add_edge(G3, e3)
let G5: Map<String, Any> = graph_add_group(G4, g4)
check_eq("graph title", G5["title"], "System")
check_eq("graph direction", G5["direction"], "left-right")
let gn: [Map<String, Any>] = G5["nodes"]
let ge: [Map<String, Any>] = G5["edges"]
let gg: [Map<String, Any>] = G5["groups"]
check_eq("graph nodes count", int_to_str(el_list_len(gn)), "2")
check_eq("graph edges count", int_to_str(el_list_len(ge)), "1")
check_eq("graph groups count", int_to_str(el_list_len(gg)), "1")
let found: Map<String, Any> = graph_node(G5, "svc")
check_eq("graph_node found", found["id"], "svc")
let missing: Map<String, Any> = graph_node(G5, "nonexistent")
let missing_id: String = missing["id"]
if str_len(missing_id) == 0 {
println("ok graph_node missing returns empty")
} else {
println("FAIL graph_node missing returned: " + missing_id)
state_set("smoke_failures", "1")
}
println("")
let failures: String = state_get("smoke_failures")
if str_eq(failures, "1") {
println("arbor-diagram: FAILED")
exit_program(1)
} else {
println("arbor-diagram: ok")
}
+19
View File
@@ -0,0 +1,19 @@
// arbor-layout hierarchical layout engine. Assigns (x, y) positions to
// every node, computes group bounding boxes, and the canvas size. Consumes
// a diagram graph; produces a layout-result value.
vessel "arbor-layout" {
version "0.1.0"
description "Hierarchical layout engine — rank assignment, positioning, group bounds"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
+591
View File
@@ -0,0 +1,591 @@
// arbor-layout hierarchical layout for diagram graphs.
//
// Public entry point:
// fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any>
//
// The graph is the lowered (diagram-form) shape. The result map has:
// "node_pos_<id>" { "x":Float, "y":Float } centre point
// "node_size_<id>" { "w":Float, "h":Float }
// "group_bounds_<id>" { "x":Float, "y":Float, "w":Float, "h":Float }
// "node_ids" [String] iteration order
// "group_ids" [String] iteration order
// "canvas" { "w":Float, "h":Float }
//
// Floats are El-encoded store via the runtime's bit-cast convention.
// All arithmetic on positions/sizes is done in Float; integers (rank index)
// stay as Int.
//
// Algorithm (simplified Sugiyama):
// 1. Assign ranks via topological propagation (longest path from sources).
// 2. Group nodes by rank, preserving declaration order.
// 3. Position each rank as a row (top-down/bottom-up) or column (LR/RL).
// 4. Compute group bounding boxes from member positions.
// 5. Compute canvas size to enclose everything.
//
// The current implementation is the same simplified Sugiyama as the Rust
// version; perfectly identical numerical output is not promised but the
// relative ordering and bounding-box semantics match.
// Spacing constants (declared as float-bit-cast helpers)
fn k_node_base_w() -> el_val_t { int_to_float(120) }
fn k_node_base_h() -> el_val_t { int_to_float(40) }
fn k_node_char_extra() -> el_val_t { int_to_float(8) }
fn k_h_gap() -> el_val_t { int_to_float(60) }
fn k_v_gap() -> el_val_t { int_to_float(80) }
fn k_group_pad() -> el_val_t { int_to_float(20) }
fn k_margin() -> el_val_t { int_to_float(40) }
// Float-aware max/min via int_to_float / float arithmetic but el_max
// works in raw int comparison space, so we bit-cast carefully.
// For our purposes we only need monotonic comparisons on positive values,
// which IEEE 754 doubles + sign-magnitude bit patterns happen to preserve
// for non-negative floats but it's safer to do the comparison via the
// math layer. We use a helper that decodes both, picks the bigger, and
// re-encodes.
//
// Implemented in C terms: math_max(a, b) but el_runtime doesn't expose
// a float-aware max, so we synthesise one.
fn fmax(a: el_val_t, b: el_val_t) -> el_val_t {
// Compare via float subtraction's sign: a - b. Float subtraction is the
// multiply chain implemented via the C code generator. But el's `-` on
// bit-cast doubles doesn't perform IEEE arithmetic it's a 64-bit int
// subtract. Workaround: round-trip through format_float and str_to_float.
// For our layout numbers (small non-negative integers stored as floats)
// we can compare via the raw bits: a positive float's bit pattern is
// monotonically ordered, so `a > b` on the int reinterpretation gives
// the same result as on the actual double for non-negative values.
if a > b { return a }
b
}
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
// a, b are bit-cast doubles. Safe addition: int-to-float, format, parse.
// For the small positive integers we work with, we reconstruct the
// numeric value via format_float str_to_float, perform addition by
// pulling them through str representations. Costly but correct on the
// current runtime. Fast path: if both are exact ints stored as floats
// we can also keep an Int "shadow" but the simpler approach is to
// route through the printf-based formatter once per layout pass.
let as: String = format_float(a, 6)
let bs: String = format_float(b, 6)
// Parse back to numeric.
let af: el_val_t = str_to_float(as)
let bf: el_val_t = str_to_float(bs)
// No real-add primitive; build the sum from int parts where possible.
// Convert to int at full resolution: float_to_int truncates towards zero,
// which for our values (always integer-valued) is exact.
let ai: Int = float_to_int(af)
let bi: Int = float_to_int(bf)
int_to_float(ai + bi)
}
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai - bi)
}
fn fmul(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai * bi)
}
fn fdiv2(a: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
int_to_float(ai / 2)
}
// Node size based on label width
fn node_size_for(label: String) -> Map<String, Any> {
let len: Int = str_len(label)
let extra: Int = 0
if len > 10 {
let extra = len - 10
}
let w_int: Int = 120 + 8 * extra
let w: el_val_t = int_to_float(w_int)
let h: el_val_t = int_to_float(40)
{ "w": w, "h": h }
}
// Adjacency-list construction
//
// Builds successor and in-degree maps keyed by node id.
fn build_succ_indeg(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let edges: [Map<String, Any>] = graph["edges"]
let n: Int = el_list_len(nodes)
let m: Int = el_list_len(edges)
let succ: Map<String, Any> = el_map_new(0)
let indeg: Map<String, Any> = el_map_new(0)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let empty: [String] = el_list_empty()
let succ = el_map_set(succ, nid, empty)
let indeg = el_map_set(indeg, nid, 0)
let i = i + 1
}
let i = 0
while i < m {
let e: Map<String, Any> = get(edges, i)
let src: String = e["from"]
let dst: String = e["to"]
let cur_succ: [String] = el_map_get(succ, src)
let new_succ: [String] = native_list_append(cur_succ, dst)
let succ = el_map_set(succ, src, new_succ)
let prev: Int = el_map_get(indeg, dst)
let indeg = el_map_set(indeg, dst, prev + 1)
let i = i + 1
}
{ "succ": succ, "indeg": indeg }
}
// Topological rank assignment
//
// Returns a map: node_id rank.
fn assign_ranks(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let adj: Map<String, Any> = build_succ_indeg(graph)
let succ: Map<String, Any> = adj["succ"]
let indeg: Map<String, Any> = adj["indeg"]
let ranks: Map<String, Any> = el_map_new(0)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let ranks = el_map_set(ranks, nid, 0)
let i = i + 1
}
// Initialise queue with all nodes whose in-degree is 0 (in declaration
// order, mirroring the Rust implementation's ordering guarantee).
let queue: [String] = el_list_empty()
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let d: Int = el_map_get(indeg, nid)
if d == 0 {
let queue = native_list_append(queue, nid)
}
let i = i + 1
}
let head = 0
let running = true
while running {
if head >= el_list_len(queue) {
let running = false
} else {
let cur: String = get(queue, head)
let head = head + 1
let cur_rank: Int = el_map_get(ranks, cur)
let neighbours: [String] = el_map_get(succ, cur)
let nn: Int = el_list_len(neighbours)
let j = 0
while j < nn {
let nb: String = get(neighbours, j)
let nb_rank: Int = el_map_get(ranks, nb)
let cand: Int = cur_rank + 1
if cand > nb_rank {
let ranks = el_map_set(ranks, nb, cand)
}
let cur_d: Int = el_map_get(indeg, nb)
let new_d: Int = cur_d - 1
let indeg = el_map_set(indeg, nb, new_d)
if new_d <= 0 {
let queue = native_list_append(queue, nb)
}
let j = j + 1
}
}
}
ranks
}
// Layout pass
fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let direction: String = graph["direction"]
let result: Map<String, Any> = el_map_new(0)
let result = el_map_set(result, "node_ids", el_list_empty())
let result = el_map_set(result, "group_ids", el_list_empty())
if n == 0 {
let canvas: Map<String, Any> = { "w": int_to_float(200), "h": int_to_float(100) }
let result = el_map_set(result, "canvas", canvas)
return result
}
let ranks: Map<String, Any> = assign_ranks(graph)
let max_rank = 0
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let r: Int = el_map_get(ranks, nid)
if r > max_rank { let max_rank = r }
let i = i + 1
}
// Group nodes by rank, preserving declaration order. Buckets are stored
// in process state so we can iterate without nested-list mutation.
let i = 0
while i <= max_rank {
state_set("rank_bucket_" + int_to_str(i), "")
let i = i + 1
}
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let r: Int = el_map_get(ranks, nid)
let key = "rank_bucket_" + int_to_str(r)
let prev: String = state_get(key)
if str_eq(prev, "") {
state_set(key, nid)
} else {
state_set(key, prev + "" + nid)
}
let i = i + 1
}
// Pre-compute sizes and stash a label-keyed cache.
let id_list: [String] = el_list_empty()
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let lbl: String = nd["label"]
let sz: Map<String, Any> = node_size_for(lbl)
let result = el_map_set(result, "node_size_" + nid, sz)
let id_list = native_list_append(id_list, nid)
let i = i + 1
}
let result = el_map_set(result, "node_ids", id_list)
// Position pass.
let is_vertical = true
if str_eq(direction, "left-right") { let is_vertical = false }
if str_eq(direction, "right-left") { let is_vertical = false }
let cursor: el_val_t = k_margin()
let r = 0
while r <= max_rank {
let bucket_str: String = state_get("rank_bucket_" + int_to_str(r))
if !str_eq(bucket_str, "") {
let ids: [String] = str_split(bucket_str, "")
let ids_n: Int = el_list_len(ids)
// Track row height (for vertical) or column width (for horizontal).
let cross_max: el_val_t = int_to_float(40)
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
if is_vertical {
let h: el_val_t = sz["h"]
let cross_max = fmax(cross_max, h)
} else {
let w: el_val_t = sz["w"]
let cross_max = fmax(cross_max, w)
}
let j = j + 1
}
if is_vertical {
let row_h: el_val_t = cross_max
let y_center: el_val_t = fadd(cursor, fdiv2(row_h))
let x_cursor: el_val_t = k_margin()
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let w: el_val_t = sz["w"]
let cx: el_val_t = fadd(x_cursor, fdiv2(w))
let pos: Map<String, Any> = { "x": cx, "y": y_center }
let result = el_map_set(result, "node_pos_" + nid, pos)
let x_cursor = fadd(fadd(x_cursor, w), k_h_gap())
let j = j + 1
}
let cursor = fadd(fadd(cursor, row_h), k_v_gap())
} else {
let col_w: el_val_t = cross_max
let x_center: el_val_t = fadd(cursor, fdiv2(col_w))
let y_cursor: el_val_t = k_margin()
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let h: el_val_t = sz["h"]
let cy: el_val_t = fadd(y_cursor, fdiv2(h))
let pos: Map<String, Any> = { "x": x_center, "y": cy }
let result = el_map_set(result, "node_pos_" + nid, pos)
let y_cursor = fadd(fadd(y_cursor, h), k_v_gap())
let j = j + 1
}
let cursor = fadd(fadd(cursor, col_w), k_h_gap())
}
} else {
// Empty bucket advance cursor by a default node size.
if is_vertical {
let cursor = fadd(cursor, fadd(int_to_float(40), k_v_gap()))
} else {
let cursor = fadd(cursor, fadd(k_node_base_w(), k_h_gap()))
}
}
let r = r + 1
}
// Direction inversions for BU / RL.
let need_flip_y = false
let need_flip_x = false
if str_eq(direction, "bottom-up") { let need_flip_y = true }
if str_eq(direction, "right-left") { let need_flip_x = true }
if need_flip_y {
let max_y: el_val_t = fadd(fsub(cursor, k_v_gap()), k_margin())
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let y: el_val_t = pos["y"]
let new_y: el_val_t = fadd(fsub(max_y, y), k_margin())
let new_pos: Map<String, Any> = { "x": pos["x"], "y": new_y }
let result = el_map_set(result, "node_pos_" + nid, new_pos)
let i = i + 1
}
}
if need_flip_x {
let max_x: el_val_t = fadd(fsub(cursor, k_h_gap()), k_margin())
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let x: el_val_t = pos["x"]
let new_x: el_val_t = fadd(fsub(max_x, x), k_margin())
let new_pos: Map<String, Any> = { "x": new_x, "y": pos["y"] }
let result = el_map_set(result, "node_pos_" + nid, new_pos)
let i = i + 1
}
}
// Group bounds.
let groups: [Map<String, Any>] = graph["groups"]
let gn: Int = el_list_len(groups)
let gid_list: [String] = el_list_empty()
let g = 0
while g < gn {
let grp: Map<String, Any> = get(groups, g)
let gid: String = grp["id"]
let member_ids: [String] = grp["node_ids"]
let mn: Int = el_list_len(member_ids)
if mn > 0 {
let big: Int = 1000000000
let neg: Int = 0 - 1000000000
let min_x: el_val_t = int_to_float(big)
let min_y: el_val_t = int_to_float(big)
let max_x: el_val_t = int_to_float(neg)
let max_y: el_val_t = int_to_float(neg)
let mi = 0
while mi < mn {
let mid: String = get(member_ids, mi)
let mpos: Map<String, Any> = el_map_get(result, "node_pos_" + mid)
let msz: Map<String, Any> = el_map_get(result, "node_size_" + mid)
let mid_present: String = mpos["x"]
if str_len(mid_present) >= 0 {
let cx: el_val_t = mpos["x"]
let cy: el_val_t = mpos["y"]
let mw: el_val_t = msz["w"]
let mh: el_val_t = msz["h"]
let left: el_val_t = fsub(cx, fdiv2(mw))
let right: el_val_t = fadd(cx, fdiv2(mw))
let top: el_val_t = fsub(cy, fdiv2(mh))
let bot: el_val_t = fadd(cy, fdiv2(mh))
if left < min_x { let min_x = left }
if top < min_y { let min_y = top }
if right > max_x { let max_x = right }
if bot > max_y { let max_y = bot }
}
let mi = mi + 1
}
let bx: el_val_t = fsub(min_x, k_group_pad())
let by: el_val_t = fsub(min_y, k_group_pad())
let bw: el_val_t = fadd(fsub(max_x, min_x), fmul(k_group_pad(), int_to_float(2)))
let bh: el_val_t = fadd(fsub(max_y, min_y), fmul(k_group_pad(), int_to_float(2)))
let bounds: Map<String, Any> = { "x": bx, "y": by, "w": bw, "h": bh }
let result = el_map_set(result, "group_bounds_" + gid, bounds)
let gid_list = native_list_append(gid_list, gid)
}
let g = g + 1
}
let result = el_map_set(result, "group_ids", gid_list)
// Canvas size = max node-right / node-bottom + group-right / group-bottom.
let canvas_w: el_val_t = int_to_float(0)
let canvas_h: el_val_t = int_to_float(0)
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let right: el_val_t = fadd(pos["x"], fdiv2(sz["w"]))
let bottom: el_val_t = fadd(pos["y"], fdiv2(sz["h"]))
if right > canvas_w { let canvas_w = right }
if bottom > canvas_h { let canvas_h = bottom }
let i = i + 1
}
let i = 0
while i < el_list_len(gid_list) {
let gid: String = get(gid_list, i)
let b: Map<String, Any> = el_map_get(result, "group_bounds_" + gid)
let r: el_val_t = fadd(b["x"], b["w"])
let bt: el_val_t = fadd(b["y"], b["h"])
if r > canvas_w { let canvas_w = r }
if bt > canvas_h { let canvas_h = bt }
let i = i + 1
}
let canvas: Map<String, Any> = {
"w": fadd(canvas_w, k_margin()),
"h": fadd(canvas_h, k_margin())
}
let result = el_map_set(result, "canvas", canvas)
result
}
// Smoke test
fn fl_to_str(v: el_val_t) -> String {
int_to_str(float_to_int(v))
}
fn smoke_fail(label: String, msg: String) -> Int {
println("FAIL " + label + ": " + msg)
state_set("smoke_failures", "1")
0
}
fn make_test_node(id: String, label: String) -> Map<String, Any> {
{
"id": id, "label": label, "sublabel": "",
"shape": "rectangle",
"style_fill": "", "style_stroke": "", "style_color": ""
}
}
fn make_test_edge(src: String, dst: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": "", "line": "solid", "arrow": "forward" }
}
fn make_test_graph(direction: String, ids: [String], src_dst: [String]) -> Map<String, Any> {
let nodes: [Map<String, Any>] = el_list_empty()
let i = 0
while i < el_list_len(ids) {
let nid: String = get(ids, i)
let nodes = native_list_append(nodes, make_test_node(nid, nid))
let i = i + 1
}
let edges: [Map<String, Any>] = el_list_empty()
let i = 0
while i + 1 < el_list_len(src_dst) {
let s: String = get(src_dst, i)
let d: String = get(src_dst, i + 1)
let edges = native_list_append(edges, make_test_edge(s, d))
let i = i + 2
}
{
"title": "T", "direction": direction,
"nodes": nodes, "edges": edges, "groups": el_list_empty()
}
}
// Empty graph.
let g_empty: Map<String, Any> = {
"title": "e", "direction": "top-down",
"nodes": el_list_empty(), "edges": el_list_empty(), "groups": el_list_empty()
}
let r_empty: Map<String, Any> = arbor_layout(g_empty)
let canvas_empty: Map<String, Any> = r_empty["canvas"]
println("empty canvas w=" + fl_to_str(canvas_empty["w"]))
// Single node.
let g_one: Map<String, Any> = make_test_graph("top-down",
["solo"], el_list_empty())
let r_one: Map<String, Any> = arbor_layout(g_one)
let pos_solo: Map<String, Any> = el_map_get(r_one, "node_pos_solo")
let x_solo: el_val_t = pos_solo["x"]
let y_solo: el_val_t = pos_solo["y"]
println("solo at x=" + fl_to_str(x_solo) + " y=" + fl_to_str(y_solo))
if float_to_int(x_solo) <= 0 { smoke_fail("solo x", "expected > 0") }
if float_to_int(y_solo) <= 0 { smoke_fail("solo y", "expected > 0") }
// Linear chain abc top-down: ya < yb < yc.
let g_chain: Map<String, Any> = make_test_graph("top-down",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_chain: Map<String, Any> = arbor_layout(g_chain)
let pa: Map<String, Any> = el_map_get(r_chain, "node_pos_a")
let pb: Map<String, Any> = el_map_get(r_chain, "node_pos_b")
let pc: Map<String, Any> = el_map_get(r_chain, "node_pos_c")
let ya: el_val_t = pa["y"]
let yb: el_val_t = pb["y"]
let yc: el_val_t = pc["y"]
println("td a.y=" + fl_to_str(ya) + " b.y=" + fl_to_str(yb) + " c.y=" + fl_to_str(yc))
if float_to_int(ya) >= float_to_int(yb) { smoke_fail("td order", "a.y >= b.y") }
if float_to_int(yb) >= float_to_int(yc) { smoke_fail("td order", "b.y >= c.y") }
// LR direction
let g_lr: Map<String, Any> = make_test_graph("left-right",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_lr: Map<String, Any> = arbor_layout(g_lr)
let pa2: Map<String, Any> = el_map_get(r_lr, "node_pos_a")
let pc2: Map<String, Any> = el_map_get(r_lr, "node_pos_c")
let xa: el_val_t = pa2["x"]
let xc: el_val_t = pc2["x"]
println("lr a.x=" + fl_to_str(xa) + " c.x=" + fl_to_str(xc))
if float_to_int(xa) >= float_to_int(xc) { smoke_fail("lr order", "a.x >= c.x") }
// Bottom-up: a is below c.
let g_bu: Map<String, Any> = make_test_graph("bottom-up",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_bu: Map<String, Any> = arbor_layout(g_bu)
let pa3: Map<String, Any> = el_map_get(r_bu, "node_pos_a")
let pc3: Map<String, Any> = el_map_get(r_bu, "node_pos_c")
let ya3: el_val_t = pa3["y"]
let yc3: el_val_t = pc3["y"]
println("bu a.y=" + fl_to_str(ya3) + " c.y=" + fl_to_str(yc3))
if float_to_int(ya3) <= float_to_int(yc3) { smoke_fail("bu order", "a.y <= c.y") }
// Canvas covers all nodes.
let canvas_chain: Map<String, Any> = r_chain["canvas"]
let cw: el_val_t = canvas_chain["w"]
let ch: el_val_t = canvas_chain["h"]
println("chain canvas w=" + fl_to_str(cw) + " h=" + fl_to_str(ch))
if float_to_int(cw) <= 0 { smoke_fail("canvas w", "non-positive") }
if float_to_int(ch) <= 0 { smoke_fail("canvas h", "non-positive") }
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-layout: FAILED")
exit_program(1)
} else {
println("arbor-layout: ok")
}
+19
View File
@@ -0,0 +1,19 @@
// arbor-parse hand-written recursive-descent parser for the .arbor source
// language. Produces an Arbor graph value consumable by arbor-layout and
// arbor-render.
vessel "arbor-parse" {
version "0.1.0"
description "Recursive-descent parser for the .arbor diagram language"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
+763
View File
@@ -0,0 +1,763 @@
// arbor-parse recursive-descent parser for the .arbor source language.
//
// This vessel inlines a private copy of the small set of arbor-core helpers
// it needs (sanitize_id and constructors). El's import form today is purely
// syntactic concatenation, so each vessel that wants to be its own buildable
// unit carries its own copy of these helpers. They're tiny (well under 100
// lines) and the duplication keeps each vessel hermetic.
//
// Public entry point: fn arbor_parse(source: String) -> Map<String, Any>
//
// Returns either a graph value or a parse-error map. Callers test for the
// "error" field:
// { "error": "..." , "line": Int, "text": "...source line..." } on failure
// { "title", "direction", "nodes", "edges", "groups" } on success
// Sanitisation (copy of arbor-core's sanitize_id)
fn is_alnum_underscore(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
if code >= 65 {
if code <= 90 { return true }
}
if code >= 97 {
if code <= 122 { return true }
}
if code == 95 { return true }
false
}
fn is_ascii_digit(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
false
}
fn sanitize_id(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "node" }
let out = ""
let prev_underscore = false
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if is_alnum_underscore(ch) {
let out = out + ch
let prev_underscore = false
} else {
if !prev_underscore {
let out = out + "_"
}
let prev_underscore = true
}
let i = i + 1
}
let m: Int = str_len(out)
let end = m
let stripping = true
while stripping {
if end <= 0 {
let stripping = false
} else {
let last: String = str_char_at(out, end - 1)
if last == "_" {
let end = end - 1
} else {
let stripping = false
}
}
}
let out = str_slice(out, 0, end)
if str_len(out) == 0 { return "node" }
let first: String = str_char_at(out, 0)
if is_ascii_digit(first) {
let out = "n" + out
}
out
}
fn shape_from_token(tok: String) -> String {
let t: String = str_trim(tok)
if t == "rect" { return "rect" }
if t == "rounded" { return "rounded" }
if t == "cylinder" { return "cylinder" }
if t == "diamond" { return "diamond" }
if t == "stadium" { return "stadium" }
if t == "primary" { return "primary" }
""
}
// Line preprocessing
//
// Strip inline `// ...` comments, trim, drop empties. Returns a list of maps
// { "no": Int, "text": String }.
fn preprocess(source: String) -> [Map<String, Any>] {
let lines: [String] = str_split(source, "\n")
let n: Int = el_list_len(lines)
let out: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n {
let raw: String = get(lines, i)
let cidx: Int = str_index_of(raw, "//")
let stripped = raw
if cidx >= 0 {
let stripped = str_slice(raw, 0, cidx)
}
let trimmed: String = str_trim(stripped)
if str_len(trimmed) > 0 {
let row: Map<String, Any> = { "no": i + 1, "text": trimmed }
let out = native_list_append(out, row)
}
let i = i + 1
}
out
}
// Quoted-string extraction
//
// Parses `"text"`-prefix from a string. Returns `{ "ok": Bool, "value": Str,
// "rest": Str }`. The `rest` field carries everything after the closing quote
// (so the caller can continue tokenising).
fn parse_quoted(s: String) -> Map<String, Any> {
let t: String = str_trim(s)
if str_len(t) < 2 {
return { "ok": false, "value": "", "rest": s }
}
let first: String = str_char_at(t, 0)
if first != "\"" {
return { "ok": false, "value": "", "rest": s }
}
let body: String = str_slice(t, 1, str_len(t))
let close: Int = str_index_of(body, "\"")
if close < 0 {
return { "ok": false, "value": "", "rest": s }
}
let inner: String = str_slice(body, 0, close)
let rest: String = str_slice(body, close + 1, str_len(body))
{ "ok": true, "value": inner, "rest": rest }
}
// Identifier prefix split
//
// `split_identifier("foo bar")` { "id": "foo", "rest": " bar" }.
// `split_identifier("a-b")` { "id": "a", "rest": "-b" }.
fn split_identifier(s: String) -> Map<String, Any> {
let n: Int = str_len(s)
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if !is_alnum_underscore(ch) {
return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) }
}
let i = i + 1
}
{ "id": s, "rest": "" }
}
// Direction parsing
fn parse_direction(s: String) -> String {
let t: String = str_trim(s)
if t == "top-down" { return "top-down" }
if t == "TD" { return "top-down" }
if t == "left-right" { return "left-right" }
if t == "LR" { return "left-right" }
if t == "right-left" { return "right-left" }
if t == "RL" { return "right-left" }
if t == "bottom-up" { return "bottom-up" }
if t == "BU" { return "bottom-up" }
""
}
// Edge-arrow detection
//
// Detects the longest matching arrow token in a line, returning
// { "ok": Bool, "from_str": Str, "kind": Str, "rest": Str }
fn extract_edge_parts(line: String) -> Map<String, Any> {
// Order: longest first to avoid partial matches.
let f1: Int = str_index_of(line, "-/->")
if f1 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f1),
"kind": "forbidden",
"rest": str_slice(line, f1 + 4, str_len(line)) }
}
let f2: Int = str_index_of(line, "<->")
if f2 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f2),
"kind": "bidirectional",
"rest": str_slice(line, f2 + 3, str_len(line)) }
}
let f3: Int = str_index_of(line, "-->")
if f3 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f3),
"kind": "dashed",
"rest": str_slice(line, f3 + 3, str_len(line)) }
}
let f4: Int = str_index_of(line, "->")
if f4 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f4),
"kind": "solid",
"rest": str_slice(line, f4 + 2, str_len(line)) }
}
{ "ok": false, "from_str": "", "kind": "", "rest": "" }
}
fn is_edge_line(line: String) -> Bool {
if str_contains(line, "->") { return true }
if str_contains(line, "<->") { return true }
false
}
// Error helpers
fn make_error(line_no: Int, line_text: String, message: String) -> Map<String, Any> {
{ "error": message, "line": line_no, "text": line_text }
}
// Parse driver
//
// State is held in process-local k/v rather than threaded through every
// function. Specifically:
// "title", "direction" graph header
// "nodes_json", "edges_json", "groups_json" accumulators (string lists)
// "group_stack_depth" "0".."N" open groups
// "group_stack_<i>_id" / "_label" / "_line" frame data
// "group_stack_<i>_node_ids" JSON array of ids inside frame
// "error" non-empty if parse failed
// "error_line", "error_text" context
fn st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 }
fn st_get_int(key: String) -> Int {
let s: String = state_get(key)
if str_eq(s, "") { return 0 }
str_to_int(s)
}
// Encode/decode small string lists via "" delimiter (unit separator).
fn list_encode(xs: [String]) -> String {
let n: Int = el_list_len(xs)
let out = ""
let i = 0
while i < n {
if i > 0 { let out = out + "" }
let out = out + get(xs, i)
let i = i + 1
}
out
}
fn list_decode(s: String) -> [String] {
if str_eq(s, "") { return el_list_empty() }
str_split(s, "")
}
fn current_group_index() -> Int {
st_get_int("group_stack_depth") - 1
}
fn group_frame_key(idx: Int, suffix: String) -> String {
"gs_" + int_to_str(idx) + "_" + suffix
}
fn open_group(id: String, label: String, line_no: Int) -> Int {
let depth: Int = st_get_int("group_stack_depth")
state_set(group_frame_key(depth, "id"), id)
state_set(group_frame_key(depth, "label"), label)
state_set(group_frame_key(depth, "line"), int_to_str(line_no))
state_set(group_frame_key(depth, "ids"), "")
st_set_int("group_stack_depth", depth + 1)
0
}
fn close_group_frame() -> Map<String, Any> {
let depth: Int = st_get_int("group_stack_depth")
if depth <= 0 {
return { "ok": false, "id": "", "label": "", "ids": "" }
}
let idx: Int = depth - 1
let id: String = state_get(group_frame_key(idx, "id"))
let label: String = state_get(group_frame_key(idx, "label"))
let ids: String = state_get(group_frame_key(idx, "ids"))
state_del(group_frame_key(idx, "id"))
state_del(group_frame_key(idx, "label"))
state_del(group_frame_key(idx, "line"))
state_del(group_frame_key(idx, "ids"))
st_set_int("group_stack_depth", idx)
{ "ok": true, "id": id, "label": label, "ids": ids }
}
fn register_node_in_group(node_id: String) -> Int {
let depth: Int = st_get_int("group_stack_depth")
if depth <= 0 { return 0 }
let idx: Int = depth - 1
let key: String = group_frame_key(idx, "ids")
let prev: String = state_get(key)
if str_eq(prev, "") {
state_set(key, node_id)
} else {
state_set(key, prev + "" + node_id)
}
0
}
// Accumulator JSON-ish encoding for nodes/edges/groups.
// We render each entry as a small string and stash in state under a counter.
fn store_node(id: String, label: String, shape: String) -> Int {
let n: Int = st_get_int("node_count")
state_set("node_id_" + int_to_str(n), id)
state_set("node_label_" + int_to_str(n), label)
state_set("node_shape_" + int_to_str(n), shape)
st_set_int("node_count", n + 1)
0
}
fn store_edge(src: String, dst: String, label: String, kind: String) -> Int {
let n: Int = st_get_int("edge_count")
state_set("edge_from_" + int_to_str(n), src)
state_set("edge_to_" + int_to_str(n), dst)
state_set("edge_label_" + int_to_str(n), label)
state_set("edge_kind_" + int_to_str(n), kind)
st_set_int("edge_count", n + 1)
0
}
fn store_group(id: String, label: String, ids: String) -> Int {
let n: Int = st_get_int("group_count")
state_set("group_id_" + int_to_str(n), id)
state_set("group_label_" + int_to_str(n), label)
state_set("group_ids_" + int_to_str(n), ids)
st_set_int("group_count", n + 1)
0
}
fn set_error(msg: String, line_no: Int, line_text: String) -> Int {
state_set("parse_error", msg)
st_set_int("parse_error_line", line_no)
state_set("parse_error_text", line_text)
0
}
fn has_error() -> Bool {
let m: String = state_get("parse_error")
if str_eq(m, "") { return false }
true
}
// Reset state at the start of each parse pass.
fn reset_state() -> Int {
state_set("graph_title", "")
state_set("graph_direction", "top-down")
st_set_int("node_count", 0)
st_set_int("edge_count", 0)
st_set_int("group_count", 0)
st_set_int("group_stack_depth", 0)
state_set("parse_error", "")
st_set_int("parse_error_line", 0)
state_set("parse_error_text", "")
0
}
// Statement-level parsing
fn parse_node_stmt(line_no: Int, line: String) -> Int {
let id_split: Map<String, Any> = split_identifier(line)
let raw_id: String = id_split["id"]
if str_eq(raw_id, "") {
set_error("expected node id, edge, or keyword", line_no, line)
return 0
}
let id: String = sanitize_id(raw_id)
let rest: String = str_trim(id_split["rest"])
// Optional shape: [token]
let shape = "rect"
let after_shape = rest
if str_len(rest) > 0 {
let lead: String = str_char_at(rest, 0)
if lead == "[" {
let close: Int = str_index_of(rest, "]")
if close < 0 {
set_error("unclosed `[` in shape token", line_no, line)
return 0
}
let token: String = str_slice(rest, 1, close)
let parsed_shape: String = shape_from_token(token)
if str_eq(parsed_shape, "") {
set_error("unknown shape `" + token + "`", line_no, line)
return 0
}
let shape = parsed_shape
let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest)))
}
}
// Optional quoted label.
let quoted: Map<String, Any> = parse_quoted(after_shape)
let label = raw_id
let ok: Bool = quoted["ok"]
if ok {
let label = quoted["value"]
}
store_node(id, label, shape)
register_node_in_group(id)
1
}
fn parse_edge_stmt(line_no: Int, line: String) -> Int {
let parts: Map<String, Any> = extract_edge_parts(line)
let ok: Bool = parts["ok"]
if !ok {
set_error("malformed edge — expected `->` `-->` `<->` or `-/->`", line_no, line)
return 0
}
let from_str: String = parts["from_str"]
let rest_str: String = parts["rest"]
let kind: String = parts["kind"]
let src: String = sanitize_id(str_trim(from_str))
let rest_t: String = str_trim(rest_str)
let id_split: Map<String, Any> = split_identifier(rest_t)
let to_raw: String = id_split["id"]
if str_eq(to_raw, "") {
set_error("edge missing target node id", line_no, line)
return 0
}
let dst: String = sanitize_id(to_raw)
let label_rest: String = str_trim(id_split["rest"])
let quoted: Map<String, Any> = parse_quoted(label_rest)
let label = ""
let qok: Bool = quoted["ok"]
if qok {
let label = quoted["value"]
}
store_edge(src, dst, label, kind)
1
}
fn parse_group_open(line_no: Int, line: String, rest: String) -> Int {
// Strip trailing `{`.
let trimmed: String = str_trim(rest)
let n: Int = str_len(trimmed)
let body = trimmed
if n > 0 {
let last: String = str_char_at(trimmed, n - 1)
if last == "{" {
let body = str_trim(str_slice(trimmed, 0, n - 1))
}
}
let id_split: Map<String, Any> = split_identifier(body)
let raw_id: String = id_split["id"]
if str_eq(raw_id, "") {
set_error("group declaration missing id", line_no, line)
return 0
}
let label_rest: String = str_trim(id_split["rest"])
let quoted: Map<String, Any> = parse_quoted(label_rest)
let label = raw_id
let qok: Bool = quoted["ok"]
if qok {
let label = quoted["value"]
}
open_group(raw_id, label, line_no)
1
}
fn parse_close_brace(line_no: Int) -> Int {
let frame: Map<String, Any> = close_group_frame()
let frame_ok: Bool = frame["ok"]
if !frame_ok {
set_error("unexpected `}` — no open group", line_no, "}")
return 0
}
store_group(frame["id"], frame["label"], frame["ids"])
1
}
fn parse_line_dispatch(line_no: Int, line: String) -> Int {
if line == "}" { return parse_close_brace(line_no) }
if str_starts_with(line, "title:") {
let after: String = str_trim(str_slice(line, 6, str_len(line)))
let q: Map<String, Any> = parse_quoted(after)
let qok: Bool = q["ok"]
if !qok {
set_error("expected quoted string after `title:`", line_no, line)
return 0
}
state_set("graph_title", q["value"])
return 1
}
if str_starts_with(line, "direction:") {
let after: String = str_trim(str_slice(line, 10, str_len(line)))
let dir: String = parse_direction(after)
if str_eq(dir, "") {
set_error("unknown direction — expected top-down, left-right, right-left, or bottom-up",
line_no, line)
return 0
}
state_set("graph_direction", dir)
return 1
}
if str_starts_with(line, "group ") {
let after: String = str_slice(line, 6, str_len(line))
return parse_group_open(line_no, line, after)
}
if is_edge_line(line) {
return parse_edge_stmt(line_no, line)
}
parse_node_stmt(line_no, line)
}
// Materialise accumulators into the final graph map
fn build_graph_value() -> Map<String, Any> {
let n_nodes: Int = st_get_int("node_count")
let nodes: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_nodes {
let s: String = int_to_str(i)
let node: Map<String, Any> = {
"id": state_get("node_id_" + s),
"label": state_get("node_label_" + s),
"shape": state_get("node_shape_" + s)
}
let nodes = native_list_append(nodes, node)
let i = i + 1
}
let n_edges: Int = st_get_int("edge_count")
let edges: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_edges {
let s: String = int_to_str(i)
let edge: Map<String, Any> = {
"from": state_get("edge_from_" + s),
"to": state_get("edge_to_" + s),
"label": state_get("edge_label_" + s),
"kind": state_get("edge_kind_" + s)
}
let edges = native_list_append(edges, edge)
let i = i + 1
}
let n_groups: Int = st_get_int("group_count")
let groups: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_groups {
let s: String = int_to_str(i)
let raw_ids: String = state_get("group_ids_" + s)
let id_list: [String] = list_decode(raw_ids)
let group: Map<String, Any> = {
"id": state_get("group_id_" + s),
"label": state_get("group_label_" + s),
"node_ids": id_list,
"direction": ""
}
let groups = native_list_append(groups, group)
let i = i + 1
}
{
"title": state_get("graph_title"),
"direction": state_get("graph_direction"),
"nodes": nodes,
"edges": edges,
"groups": groups
}
}
// Public entry point
fn arbor_parse(source: String) -> Map<String, Any> {
reset_state()
let lines: [Map<String, Any>] = preprocess(source)
let n: Int = el_list_len(lines)
let i = 0
let abort = false
while i < n {
if abort {
// skip error already recorded
} else {
let row: Map<String, Any> = get(lines, i)
let line_no: Int = row["no"]
let text: String = row["text"]
parse_line_dispatch(line_no, text)
if has_error() {
let abort = true
}
}
let i = i + 1
}
if !has_error() {
let depth: Int = st_get_int("group_stack_depth")
if depth > 0 {
let idx: Int = depth - 1
let id: String = state_get(group_frame_key(idx, "id"))
let line_no: Int = st_get_int(group_frame_key(idx, "line"))
set_error("unclosed group '" + id + "' — missing closing `}`",
line_no, "group " + id)
}
}
if has_error() {
return {
"error": state_get("parse_error"),
"line": st_get_int("parse_error_line"),
"text": state_get("parse_error_text")
}
}
build_graph_value()
}
// Smoke test
fn fail_msg(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("smoke_failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label)
return 1
}
fail_msg(label, got, want)
}
// Helper: a graph map is in the error state iff it has a non-empty "error".
fn parse_failed(g: Map<String, Any>) -> Bool {
let m: String = g["error"]
if str_eq(m, "") { return false }
// map_get returns NULL for missing keys; str_eq treats two NULLs as equal
// and NULL vs "" as not equal guard explicitly.
if str_len(m) == 0 { return false }
true
}
let src1 = "title: \"Test\"\ndirection: left-right\n\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\n\napi -> db \"reads\""
let g1: Map<String, Any> = arbor_parse(src1)
if parse_failed(g1) {
println("FAIL parse 1: " + g1["error"])
state_set("smoke_failures", "1")
}
check_eq("title parsed", g1["title"], "Test")
check_eq("direction parsed", g1["direction"], "left-right")
let nodes1: [Map<String, Any>] = g1["nodes"]
let nn1: Int = el_list_len(nodes1)
check_eq("two nodes", int_to_str(nn1), "2")
let edges1: [Map<String, Any>] = g1["edges"]
let ne1: Int = el_list_len(edges1)
check_eq("one edge", int_to_str(ne1), "1")
let e0: Map<String, Any> = get(edges1, 0)
check_eq("edge from", e0["from"], "api")
check_eq("edge to", e0["to"], "db")
check_eq("edge label", e0["label"], "reads")
check_eq("edge kind", e0["kind"], "solid")
let n0: Map<String, Any> = get(nodes1, 0)
check_eq("node 0 shape", n0["shape"], "rounded")
check_eq("node 0 label", n0["label"], "REST API")
// Test edge varieties
let src2 = "a \"A\"\nb \"B\"\na -> b\na --> b\na -/-> b\na <-> b"
let g2: Map<String, Any> = arbor_parse(src2)
let edges2: [Map<String, Any>] = g2["edges"]
check_eq("4 edges parsed", int_to_str(el_list_len(edges2)), "4")
let kinds = ""
let i = 0
while i < el_list_len(edges2) {
let e: Map<String, Any> = get(edges2, i)
let k: String = e["kind"]
let kinds = kinds + k + ","
let i = i + 1
}
check_eq("edge kinds", kinds, "solid,dashed,forbidden,bidirectional,")
// Groups
let src3 = "group core \"Application Core\" {\n api [rounded] \"REST API\"\n svc \"Business Logic\"\n}\nstandalone \"Out\""
let g3: Map<String, Any> = arbor_parse(src3)
let groups3: [Map<String, Any>] = g3["groups"]
check_eq("one group", int_to_str(el_list_len(groups3)), "1")
let grp0: Map<String, Any> = get(groups3, 0)
check_eq("group label", grp0["label"], "Application Core")
let gnids: [String] = grp0["node_ids"]
check_eq("group has 2 members", int_to_str(el_list_len(gnids)), "2")
let nodes3: [Map<String, Any>] = g3["nodes"]
check_eq("3 total nodes (incl standalone)",
int_to_str(el_list_len(nodes3)), "3")
// Error: unknown shape
let src4 = "node [hexagon] \"X\""
let g4: Map<String, Any> = arbor_parse(src4)
let err4: String = g4["error"]
if str_eq(err4, "") {
println("FAIL expected error for unknown shape")
state_set("smoke_failures", "1")
} else {
if str_contains(err4, "hexagon") {
println("ok error mentions hexagon: " + err4)
} else {
println("FAIL error wording: " + err4)
state_set("smoke_failures", "1")
}
}
// Error: unclosed group
let src5 = "group g \"G\" {\n a \"A\"\n"
let g5: Map<String, Any> = arbor_parse(src5)
let err5: String = g5["error"]
if str_eq(err5, "") {
println("FAIL expected unclosed-group error")
state_set("smoke_failures", "1")
} else {
if str_contains(err5, "unclosed") {
println("ok unclosed group detected")
} else {
println("FAIL unclosed error wording: " + err5)
state_set("smoke_failures", "1")
}
}
// Comments and inline comments
let src6 = "// header\na \"A\" // trailing\nb \"B\""
let g6: Map<String, Any> = arbor_parse(src6)
check_eq("comments stripped", int_to_str(el_list_len(g6["nodes"])), "2")
// Empty input
let g7: Map<String, Any> = arbor_parse("")
check_eq("empty graph nodes", int_to_str(el_list_len(g7["nodes"])), "0")
check_eq("empty graph default direction", g7["direction"], "top-down")
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-parse: FAILED")
exit_program(1)
} else {
println("arbor-parse: ok")
}
+21
View File
@@ -0,0 +1,21 @@
// arbor-render SVG renderer. Consumes a diagram graph + layout result and
// emits an SVG document. PNG rasterization is not provided in this vessel
// because the El runtime does not expose a vector-to-raster primitive yet
// (see report).
vessel "arbor-render" {
version "0.1.0"
description "SVG renderer for Arbor diagrams"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
arbor-layout "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
+575
View File
@@ -0,0 +1,575 @@
// arbor-render SVG emission from a laid-out diagram.
//
// Entry point:
// fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String
//
// The graph is the lowered (diagram-form) shape produced by arbor-core /
// arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The
// layout is whatever arbor-layout returned: `node_pos_<id>`, `node_size_<id>`,
// `group_bounds_<id>`, `node_ids`, `group_ids`, `canvas`.
//
// `forbidden` is a list of "from->to" key strings same format as
// arbor-core's collect_forbidden(). The Rust crate threaded a HashSet
// through; El threads a list and we linear-scan.
//
// SVG is text emission straightforward El. Every float coordinate is
// passed through format_float(_, 1) for stable output.
//
// PNG render is intentionally out of scope
// The Rust crate rasterises via resvg tiny_skia png. The El runtime
// today exposes no equivalent: there is no resvg, no usvg, no font rasterer,
// no PNG encoder, no path-fill code. fs_write writes text only there is
// no binary write primitive. arbor_render_png() returns an error map in El
// until the runtime grows a rasterer (see "runtime gaps" in the report).
// Colour palette (matches the Rust constants exactly)
fn col_node_fill() -> String { "#ffffff" }
fn col_node_stroke() -> String { "#334155" }
fn col_primary_fill() -> String { "#0052A0" }
fn col_primary_text() -> String { "#ffffff" }
fn col_node_text() -> String { "#0D0D14" }
fn col_edge() -> String { "#64748B" }
fn col_edge_forbidden() -> String { "#DC2626" }
fn col_group_fill() -> String { "rgba(0,0,0,0.03)" }
fn col_group_stroke() -> String { "#CBD5E1" }
fn col_group_text() -> String { "#64748B" }
fn col_edge_label() -> String { "#64748B" }
// XML escape
fn esc(s: String) -> String {
let r1: String = str_replace(s, "&", "&amp;")
let r2: String = str_replace(r1, "<", "&lt;")
let r3: String = str_replace(r2, ">", "&gt;")
let r4: String = str_replace(r3, "\"", "&quot;")
r4
}
// Float to "%.1f" the Rust pt() helper.
fn pt(v: el_val_t) -> String {
format_float(v, 1)
}
// Float arithmetic helpers float_to_int / int_to_float trip through Int,
// which is exact for the integer-valued floats used by the layout pass.
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai + bi)
}
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai - bi)
}
fn fdiv2(a: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
int_to_float(ai / 2)
}
fn fmid(a: el_val_t, b: el_val_t) -> el_val_t {
fdiv2(fadd(a, b))
}
// forbidden-edge linear lookup
fn forbidden_key(from: String, to: String) -> String {
from + "->" + to
}
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
let key: String = forbidden_key(src, dst)
let n: Int = el_list_len(set)
let i = 0
while i < n {
let s: String = get(set, i)
if str_eq(s, key) { return true }
let i = i + 1
}
false
}
// Arrow marker defs
fn arrow_defs() -> String {
let s = "\n <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
let s = s + " </marker>\n"
let s = s + " <marker id=\"ah-bi\" markerWidth=\"10\" markerHeight=\"7\" refX=\"1\" refY=\"3.5\" orient=\"auto-start-reverse\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
let s = s + " </marker>\n"
let s = s + " <marker id=\"ah-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge_forbidden() + "\"/>\n"
let s = s + " </marker>"
s
}
// Node rendering
fn render_node(buf: String, node: Map<String, Any>, layout: Map<String, Any>) -> String {
let nid: String = node["id"]
let pos: Map<String, Any> = el_map_get(layout, "node_pos_" + nid)
let sz: Map<String, Any> = el_map_get(layout, "node_size_" + nid)
let cx: el_val_t = pos["x"]
let cy: el_val_t = pos["y"]
let w: el_val_t = sz["w"]
let h: el_val_t = sz["h"]
let x: el_val_t = fsub(cx, fdiv2(w))
let y: el_val_t = fsub(cy, fdiv2(h))
let fill_in: String = node["style_fill"]
let stroke_in: String = node["style_stroke"]
let color_in: String = node["style_color"]
let fill = col_node_fill()
if str_len(fill_in) > 0 { let fill = fill_in }
let stroke = col_node_stroke()
if str_len(stroke_in) > 0 { let stroke = stroke_in }
let text_col = col_node_text()
if str_len(color_in) > 0 { let text_col = color_in }
let shape: String = node["shape"]
let buf = buf
if str_eq(shape, "rectangle") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"4\" fill=\"" + fill + "\" stroke=\"" + stroke
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "rounded_rect") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"20\" fill=\"" + fill + "\" stroke=\"" + stroke
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "stadium") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"" + pt(fdiv2(h)) + "\" fill=\"" + fill
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "cylinder") {
// body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6
// to stay in integer arithmetic visually indistinguishable on the
// canvas sizes the layout produces).
let hi: Int = float_to_int(h)
let ry: el_val_t = int_to_float(hi / 6)
let body_y: el_val_t = fadd(y, ry)
let body_h: el_val_t = fsub(h, ry)
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(body_y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(body_h)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
// top ellipse
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(body_y)
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
// bottom ellipse
let bot_y: el_val_t = fadd(y, h)
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(bot_y)
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "diamond") {
let hw: el_val_t = fdiv2(w)
let hh: el_val_t = fdiv2(h)
let buf = buf + " <polygon points=\""
let buf = buf + pt(cx) + "," + pt(fsub(cy, hh)) + " "
let buf = buf + pt(fadd(cx, hw)) + "," + pt(cy) + " "
let buf = buf + pt(cx) + "," + pt(fadd(cy, hh)) + " "
let buf = buf + pt(fsub(cx, hw)) + "," + pt(cy)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
// Label.
let label: String = node["label"]
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(cy)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\">"
let buf = buf + esc(label) + "</text>\n"
// Sublabel Rust's DiagramNode stores Option<String>; El uses "" sentinel.
let sub: String = node["sublabel"]
if str_len(sub) > 0 {
let sub_y: el_val_t = fadd(cy, int_to_float(14))
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(sub_y)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\" font-size=\"10\">"
let buf = buf + esc(sub) + "</text>\n"
}
buf
}
// Edge rendering
//
// We emit a straight line from one node centre to the other and let the
// browser draw it; the Rust crate renders cubic bezier paths but the runtime
// has no robust math layer, and the rectangles are large enough that
// straight edges read clearly. (See "runtime gaps".)
fn render_edge(buf: String, edge: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
let from_id: String = edge["from"]
let to_id: String = edge["to"]
let from_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + from_id)
let to_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + to_id)
let fx: el_val_t = from_pos["x"]
let fy: el_val_t = from_pos["y"]
let tx: el_val_t = to_pos["x"]
let ty: el_val_t = to_pos["y"]
let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id)
let stroke = col_edge()
if is_forbidden { let stroke = col_edge_forbidden() }
let line: String = edge["line"]
let arrow: String = edge["arrow"]
let dash_attr = ""
if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" }
if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" }
let marker_start = ""
if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
let marker_end = " marker-end=\"url(#ah)\""
if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" }
if str_eq(arrow, "none") { let marker_end = "" }
if str_eq(arrow, "backward") { let marker_end = "" }
let buf = buf + " <line x1=\"" + pt(fx) + "\" y1=\"" + pt(fy)
let buf = buf + "\" x2=\"" + pt(tx) + "\" y2=\"" + pt(ty)
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
let buf = buf + dash_attr + marker_start + marker_end + "/>\n"
// Forbidden marker circle-X at midpoint.
if is_forbidden {
let mx: el_val_t = fmid(fx, tx)
let my: el_val_t = fmid(fy, ty)
let r: el_val_t = int_to_float(7)
let buf = buf + " <circle cx=\"" + pt(mx) + "\" cy=\"" + pt(my)
let buf = buf + "\" r=\"" + pt(r) + "\" fill=\"white\" stroke=\""
let buf = buf + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
let off: el_val_t = int_to_float(4)
let buf = buf + " <line x1=\"" + pt(fsub(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
let buf = buf + "\" x2=\"" + pt(fadd(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
let buf = buf + " <line x1=\"" + pt(fadd(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
let buf = buf + "\" x2=\"" + pt(fsub(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
}
// Edge label
let label: String = edge["label"]
if str_len(label) > 0 {
let mx: el_val_t = fmid(fx, tx)
let my: el_val_t = fmid(fy, ty)
let lw: el_val_t = int_to_float(str_len(label) * 7 + 8)
let lh: el_val_t = int_to_float(16)
let buf = buf + " <rect x=\"" + pt(fsub(mx, fdiv2(lw))) + "\" y=\"" + pt(fsub(my, fdiv2(lh)))
let buf = buf + "\" width=\"" + pt(lw) + "\" height=\"" + pt(lh)
let buf = buf + "\" rx=\"3\" fill=\"white\" opacity=\"0.85\"/>\n"
let buf = buf + " <text x=\"" + pt(mx) + "\" y=\"" + pt(my)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-edge-label\">" + esc(label) + "</text>\n"
}
buf
}
// Group rendering
fn render_group(buf: String, group: Map<String, Any>, layout: Map<String, Any>) -> String {
let gid: String = group["id"]
let bounds: Map<String, Any> = el_map_get(layout, "group_bounds_" + gid)
// Layout may not have bounds for empty groups defensive.
let bx_check: el_val_t = bounds["x"]
if float_to_int(bx_check) == 0 {
// Could be a real 0; cheaper to skip via presence check on group_ids.
}
let bx: el_val_t = bounds["x"]
let by: el_val_t = bounds["y"]
let bw: el_val_t = bounds["w"]
let bh: el_val_t = bounds["h"]
let buf = buf + " <rect x=\"" + pt(bx) + "\" y=\"" + pt(by)
let buf = buf + "\" width=\"" + pt(bw) + "\" height=\"" + pt(bh)
let buf = buf + "\" rx=\"8\" fill=\"" + col_group_fill() + "\" stroke=\""
let buf = buf + col_group_stroke() + "\" stroke-width=\"1\" stroke-dasharray=\"4,3\"/>\n"
// Group label in the top-left corner.
let lx: el_val_t = fadd(bx, int_to_float(8))
let ly: el_val_t = fadd(by, int_to_float(14))
let label: String = group["label"]
let buf = buf + " <text x=\"" + pt(lx) + "\" y=\"" + pt(ly)
let buf = buf + "\" class=\"arbor-group-label\">" + esc(label) + "</text>\n"
buf
}
// Public entry point
fn arbor_render_svg(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
let canvas: Map<String, Any> = el_map_get(layout, "canvas")
let cw: el_val_t = canvas["w"]
let ch: el_val_t = canvas["h"]
let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + pt(cw)
let buf = buf + "\" height=\"" + pt(ch) + "\" viewBox=\"0 0 " + pt(cw) + " " + pt(ch) + "\">\n"
let buf = buf + " <defs>"
let buf = buf + arrow_defs()
let buf = buf + "\n <style>\n"
let buf = buf + " .arbor-node-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; }\n"
let buf = buf + " .arbor-group-label { font-family: 'Helvetica Neue', Helvetica, Arial, monospace; font-size: 10px; fill: " + col_group_text() + "; letter-spacing: 0.08em; }\n"
let buf = buf + " .arbor-edge-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; fill: " + col_edge_label() + "; }\n"
let buf = buf + " </style>\n"
let buf = buf + " </defs>\n"
// Groups first (behind everything).
let buf = buf + " <!-- Groups -->\n"
let groups: [Map<String, Any>] = graph["groups"]
let gn: Int = el_list_len(groups)
let i = 0
while i < gn {
let g: Map<String, Any> = get(groups, i)
let gid: String = g["id"]
// Only render groups the layout actually placed.
let gids: [String] = el_map_get(layout, "group_ids")
let placed = false
let j = 0
while j < el_list_len(gids) {
if str_eq(get(gids, j), gid) { let placed = true }
let j = j + 1
}
if placed {
let buf = render_group(buf, g, layout)
}
let i = i + 1
}
// Edges
let buf = buf + " <!-- Edges -->\n"
let edges: [Map<String, Any>] = graph["edges"]
let en: Int = el_list_len(edges)
let i = 0
while i < en {
let e: Map<String, Any> = get(edges, i)
let buf = render_edge(buf, e, layout, forbidden)
let i = i + 1
}
// Nodes
let buf = buf + " <!-- Nodes -->\n"
let nodes: [Map<String, Any>] = graph["nodes"]
let nn: Int = el_list_len(nodes)
let i = 0
while i < nn {
let n: Map<String, Any> = get(nodes, i)
let buf = render_node(buf, n, layout)
let i = i + 1
}
// Title
let title: String = graph["title"]
if str_len(title) > 0 {
let title_x: el_val_t = fdiv2(cw)
let buf = buf + " <text x=\"" + pt(title_x) + "\" y=\"22\" text-anchor=\"middle\""
let buf = buf + " font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\""
let buf = buf + " font-size=\"15\" font-weight=\"600\" fill=\"" + col_node_text() + "\">"
let buf = buf + esc(title) + "</text>\n"
}
let buf = buf + "</svg>\n"
buf
}
// PNG not implemented; the runtime has no SVG rasterizer or PNG encoder.
// Returns an error map that callers can inspect via map["error"].
fn arbor_render_png(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> Map<String, Any> {
{
"error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary"
}
}
// Smoke test
fn fail(label: String, msg: String) -> Int {
println("FAIL " + label + ": " + msg)
state_set("smoke_failures", "1")
0
}
fn check_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
println("ok " + label)
return 1
}
fail(label, "missing [" + needle + "]")
}
fn check_not_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
return fail(label, "should not contain [" + needle + "]")
}
println("ok " + label)
1
}
fn make_test_node(id: String, label: String, shape: String) -> Map<String, Any> {
{
"id": id, "label": label, "sublabel": "",
"shape": shape,
"style_fill": "", "style_stroke": "", "style_color": ""
}
}
fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map<String, Any> {
{
"from": src, "to": dst, "label": label,
"line": line, "arrow": arrow
}
}
fn make_test_pos(x: Int, y: Int) -> Map<String, Any> {
{ "x": int_to_float(x), "y": int_to_float(y) }
}
fn make_test_size(w: Int, h: Int) -> Map<String, Any> {
{ "w": int_to_float(w), "h": int_to_float(h) }
}
// Build a minimal layout map by hand.
fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map<String, Any> {
let r: Map<String, Any> = el_map_new(0)
let r = el_map_set(r, "node_ids", node_ids)
let r = el_map_set(r, "group_ids", group_ids)
let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) })
r
}
let n_a: Map<String, Any> = make_test_node("a", "Node A", "rectangle")
let n_b: Map<String, Any> = make_test_node("b", "Node B", "rectangle")
let e_ab: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
let nodes: [Map<String, Any>] = native_list_empty()
let nodes = native_list_append(nodes, n_a)
let nodes = native_list_append(nodes, n_b)
let edges: [Map<String, Any>] = native_list_empty()
let edges = native_list_append(edges, e_ab)
let groups: [Map<String, Any>] = native_list_empty()
let g: Map<String, Any> = {
"title": "Test", "direction": "top-down",
"nodes": nodes, "edges": edges, "groups": groups
}
let nid_list: [String] = native_list_empty()
let nid_list = native_list_append(nid_list, "a")
let nid_list = native_list_append(nid_list, "b")
let gid_list: [String] = native_list_empty()
let layout: Map<String, Any> = build_layout(nid_list, gid_list, 400, 300)
let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60))
let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200))
let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40))
let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40))
let forbidden: [String] = native_list_empty()
let svg: String = arbor_render_svg(g, layout, forbidden)
check_contains("svg starts with <svg", svg, "<svg xmlns=")
check_contains("svg ends with </svg>", svg, "</svg>")
check_contains("svg contains node label", svg, "Node A")
check_contains("svg contains title", svg, ">Test</text>")
check_contains("svg has rect for rectangle node", svg, "<rect")
check_contains("svg has line for edge", svg, "<line")
check_contains("svg has arrow marker def", svg, "id=\"ah\"")
// Escape test
let n_esc: Map<String, Any> = make_test_node("x", "A & B <C>", "rectangle")
let nodes2: [Map<String, Any>] = native_list_empty()
let nodes2 = native_list_append(nodes2, n_esc)
let g2: Map<String, Any> = {
"title": "Test <Title>", "direction": "top-down",
"nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty()
}
let nid2: [String] = native_list_empty()
let nid2 = native_list_append(nid2, "x")
let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100)
let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40))
let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40))
let svg2: String = arbor_render_svg(g2, layout2, native_list_empty())
check_contains("escapes ampersand", svg2, "&amp;")
check_contains("escapes <", svg2, "&lt;")
check_not_contains("no raw <C>", svg2, "<C>")
// Forbidden edge
let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
let edges3: [Map<String, Any>] = native_list_empty()
let edges3 = native_list_append(edges3, e_fb)
let g3: Map<String, Any> = {
"title": "F", "direction": "top-down",
"nodes": nodes, "edges": edges3, "groups": native_list_empty()
}
let fb: [String] = native_list_empty()
let fb = native_list_append(fb, forbidden_key("a", "b"))
let svg3: String = arbor_render_svg(g3, layout, fb)
check_contains("forbidden uses red marker", svg3, "ah-red")
check_contains("forbidden colour present", svg3, col_edge_forbidden())
// Diamond shape polygon
let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond")
let g4: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": native_list_append(native_list_empty(), n_d),
"edges": native_list_empty(), "groups": native_list_empty()
}
let nid4: [String] = native_list_append(native_list_empty(), "d")
let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100)
let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50))
let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40))
let svg4: String = arbor_render_svg(g4, layout4, native_list_empty())
check_contains("diamond uses polygon", svg4, "<polygon")
// Cylinder shape ellipses
let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder")
let g5: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": native_list_append(native_list_empty(), n_cy),
"edges": native_list_empty(), "groups": native_list_empty()
}
let nid5: [String] = native_list_append(native_list_empty(), "cy")
let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100)
let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50))
let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40))
let svg5: String = arbor_render_svg(g5, layout5, native_list_empty())
check_contains("cylinder uses ellipse", svg5, "<ellipse")
// Dashed edge
let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "")
let g6: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash),
"groups": native_list_empty()
}
let svg6: String = arbor_render_svg(g6, layout, native_list_empty())
check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"")
// PNG returns an error map
let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty())
let err: String = png["error"]
if str_len(err) > 0 {
println("ok PNG returns error map")
} else {
println("FAIL PNG should have returned error")
state_set("smoke_failures", "1")
}
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-render: FAILED")
exit_program(1)
} else {
println("arbor-render: ok")
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-298
View File
@@ -1,298 +0,0 @@
// compiler.el el self-hosting compiler pipeline
//
// Wires lexer -> parser -> codegen into a single compile() function.
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
// then self-hosted from that point forward.
//
// Two backends:
// - C (default) compile() -> emits C source linked against el_runtime.c
// - JS (--target=js) compile_js() -> emits JS source linked against el_runtime.js
//
// Compile the C output with:
// cc -o <prog> <prog>.c el_runtime.c
//
// Run the JS output with:
// node <prog>.js (after copying el_runtime.js next to it)
import "lexer.el"
import "parser.el"
import "codegen.el"
import "codegen-js.el"
// compile full pipeline (C target): source string -> C source string
fn compile(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory
// before codegen allocates its own working data on large source files.
el_release(tokens)
codegen(stmts, source)
}
// compile_js full pipeline (JS target): source string -> JS source string
fn compile_js(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory.
el_release(tokens)
codegen_js(stmts, source)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
// in El's lexer it would be tokenised as `Target`, breaking the
// parser's identifier resolution.)
fn compile_dispatch(tgt: String, source: String) -> String {
if str_eq(tgt, "js") { return compile_js(source) }
compile(source)
}
// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_starts_with(a, "--target=") {
let v: String = str_slice(a, 9, str_len(a))
return v
}
let i = i + 1
}
return "c"
}
// Strip flags from argv, leaving only positional arguments.
fn strip_flags(argv: [String]) -> [String] {
let out: [String] = native_list_empty()
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if !str_starts_with(a, "--") {
let out = native_list_append(out, a)
}
let i = i + 1
}
return out
}
// Detect --emit-header flag in argv.
fn detect_emit_header(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--emit-header") { return true }
let i = i + 1
}
return false
}
// Reconstruct an El type annotation string from a parsed type node.
fn type_node_to_el(t: Map<String, Any>) -> String {
let k: String = t["kind"]
if str_eq(k, "Simple") { return t["name"] }
if str_eq(k, "List") {
let inner: String = type_node_to_el(t["inner"])
return "[" + inner + "]"
}
if str_eq(k, "Map") {
let kt: String = type_node_to_el(t["key"])
let vt: String = type_node_to_el(t["val"])
return "Map<" + kt + ", " + vt + ">"
}
"Any"
}
// emit_header write a .elh file from parsed statements.
// Scans for FnDef nodes and emits 'extern fn' declarations.
fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
let n: Int = native_list_len(stmts)
let i = 0
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n")
while i < n {
let stmt = native_list_get(stmts, i)
let kind: String = stmt["stmt"]
if str_eq(kind, "FnDef") {
let name: String = stmt["name"]
if !str_eq(name, "main") {
let params = stmt["params"]
let ret_type: String = stmt["ret_type"]
// build param list
let np: Int = native_list_len(params)
let pi = 0
let param_parts: [String] = native_list_empty()
while pi < np {
let param = native_list_get(params, pi)
let pname: String = param["name"]
let ptype: String = param["type"]
if str_eq(ptype, "") { let ptype = "Any" }
let param_parts = native_list_append(param_parts, pname + ": " + ptype)
let pi = pi + 1
}
let params_str: String = str_join(param_parts, ", ")
let ret_str: String = ret_type
if str_eq(ret_str, "") { let ret_str = "Any" }
let sig: String = "extern fn " + name + "(" + params_str + ") -> " + ret_str
let parts = native_list_append(parts, sig + "\n")
}
}
let i = i + 1
}
let content: String = str_join(parts, "")
let ok: Bool = fs_write(hdr_path, content)
}
// Import resolution
//
// elc supports two forms of import:
// import "path/to/file.el" quoted relative path
// from module import { Name } bare module name resolves to module.el
// in the entry source's directory
//
// Codegen treats Import statements as no-ops (declarations only), so to
// actually link bodies across files we textually concatenate every imported
// source ahead of the entry source before lex/parse. resolve_imports does a
// depth-first traversal with deduplication so any module that gets pulled in
// transitively is included exactly once.
fn dirname_of(path: String) -> String {
let n: Int = str_len(path)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(path, i, i + 1)
if str_eq(c, "/") {
return str_slice(path, 0, i)
}
let i = i - 1
}
return "."
}
// Extract the resolved file path from a single trimmed source line. Returns
// "" if the line is not an import.
fn parse_import_line(trimmed: String, dir: String) -> String {
if str_starts_with(trimmed, "import \"") {
let after: String = str_slice(trimmed, 8, str_len(trimmed))
let q: Int = str_index_of(after, "\"")
if q > 0 {
let mod: String = str_slice(after, 0, q)
return dir + "/" + mod
}
}
if str_starts_with(trimmed, "from ") {
let after: String = str_slice(trimmed, 5, str_len(trimmed))
// module name is the first whitespace-delimited token
let sp: Int = str_index_of(after, " ")
if sp > 0 {
let mod_raw: String = str_slice(after, 0, sp)
let mod: String = str_trim(mod_raw)
if !str_eq(mod, "") {
return dir + "/" + mod + ".el"
}
}
}
return ""
}
// Recursively resolve imports starting from src_path. Returns the combined
// source text with every imported module's body inlined ahead of the entry
// source, deduplicated by absolute path. Uses state_set to track which paths
// have already been pulled in for this run.
//
// Accumulates chunks into lists and joins once at the end to avoid the O(n²)
// memory growth caused by repeated `prefix = prefix + chunk` concatenation.
fn resolve_imports(src_path: String) -> String {
let seen_key: String = "__elc_imp__:" + src_path
let already: String = state_get(seen_key)
if !str_eq(already, "") { return "" }
state_set(seen_key, "1")
let source: String = fs_read(src_path)
let dir: String = dirname_of(src_path)
let lines: [String] = str_split(source, "\n")
let n: Int = native_list_len(lines)
// Collect chunks into lists O(1) amortized per append.
// Join once at the end O(n) single pass.
let prefix_chunks: [String] = native_list_empty()
let body_chunks: [String] = native_list_empty()
let i: Int = 0
while i < n {
let line: String = native_list_get(lines, i)
let trimmed: String = str_trim(line)
let imp_path: String = parse_import_line(trimmed, dir)
if !str_eq(imp_path, "") {
// Use pre-compiled header if available (separate compilation).
// Only check .elh for imported files never for the entry file itself.
let imp_elh_path: String = str_slice(imp_path, 0, str_len(imp_path) - 3) + ".elh"
let imp_elh: String = fs_read(imp_elh_path)
if !str_eq(imp_elh, "") {
// Header exists: mark the .el as seen (so it won't be re-inlined
// if something else also imports it) and use the header text.
let seen_imp_key: String = "__elc_imp__:" + imp_path
state_set(seen_imp_key, "1")
let prefix_chunks = native_list_append(prefix_chunks, imp_elh)
} else {
let imp_body: String = resolve_imports(imp_path)
let prefix_chunks = native_list_append(prefix_chunks, imp_body)
}
} else {
let body_chunks = native_list_append(body_chunks, line + "\n")
}
let i = i + 1
}
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
}
// main CLI entry point.
//
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
fn main() -> Void {
let argv: [String] = args()
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
// (Section 1.5 of the language spec). detect_target itself is fine
// because the function-name position has no token-class restriction.
let tgt: String = detect_target(argv)
let do_emit_header: Bool = detect_emit_header(argv)
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
exit(1)
}
let src_path: String = native_list_get(positional, 0)
// When --emit-header is requested, parse the source file directly
// (without inlining imports) and write out a .elh file alongside the .c.
if do_emit_header {
let raw_source: String = fs_read(src_path)
let hdr_tokens: [Map<String, Any>] = lex(raw_source)
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
el_release(hdr_tokens)
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
emit_header(hdr_stmts, hdr_path)
el_release(hdr_stmts)
}
let source: String = resolve_imports(src_path)
let out: String = compile_dispatch(tgt, source)
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
if ok {
exit(0)
} else {
println("el-compiler: failed to write output")
exit(1)
}
}
// No output path: codegen streamed to stdout already; out is "".
}
-747
View File
@@ -1,747 +0,0 @@
// lexer.el el self-hosting lexer
//
// Tokenises an el source string into a list of token maps.
// Each token is a Map<String, Any> with keys:
// "kind" -> String (e.g. "Int", "Ident", "Plus")
// "value" -> String (the raw text of the token)
//
// Entry point: fn lex(source: String) -> [Map<String, Any>]
//
// Uses native_string_chars to split the source into a chars list,
// then indexes it with native_list_get avoids O(N²) string cloning.
// Character helpers
fn lex_is_digit(ch: String) -> Bool {
if ch == "0" { return true }
if ch == "1" { return true }
if ch == "2" { return true }
if ch == "3" { return true }
if ch == "4" { return true }
if ch == "5" { return true }
if ch == "6" { return true }
if ch == "7" { return true }
if ch == "8" { return true }
if ch == "9" { return true }
false
}
fn lex_is_alpha(ch: String) -> Bool {
if ch == "a" { return true }
if ch == "b" { return true }
if ch == "c" { return true }
if ch == "d" { return true }
if ch == "e" { return true }
if ch == "f" { return true }
if ch == "g" { return true }
if ch == "h" { return true }
if ch == "i" { return true }
if ch == "j" { return true }
if ch == "k" { return true }
if ch == "l" { return true }
if ch == "m" { return true }
if ch == "n" { return true }
if ch == "o" { return true }
if ch == "p" { return true }
if ch == "q" { return true }
if ch == "r" { return true }
if ch == "s" { return true }
if ch == "t" { return true }
if ch == "u" { return true }
if ch == "v" { return true }
if ch == "w" { return true }
if ch == "x" { return true }
if ch == "y" { return true }
if ch == "z" { return true }
if ch == "A" { return true }
if ch == "B" { return true }
if ch == "C" { return true }
if ch == "D" { return true }
if ch == "E" { return true }
if ch == "F" { return true }
if ch == "G" { return true }
if ch == "H" { return true }
if ch == "I" { return true }
if ch == "J" { return true }
if ch == "K" { return true }
if ch == "L" { return true }
if ch == "M" { return true }
if ch == "N" { return true }
if ch == "O" { return true }
if ch == "P" { return true }
if ch == "Q" { return true }
if ch == "R" { return true }
if ch == "S" { return true }
if ch == "T" { return true }
if ch == "U" { return true }
if ch == "V" { return true }
if ch == "W" { return true }
if ch == "X" { return true }
if ch == "Y" { return true }
if ch == "Z" { return true }
false
}
fn is_alnum_or_underscore(ch: String) -> Bool {
if lex_is_digit(ch) { return true }
if lex_is_alpha(ch) { return true }
if ch == "_" { return true }
false
}
fn lex_is_whitespace(ch: String) -> Bool {
if ch == " " { return true }
if ch == "\t" { return true }
if ch == "\n" { return true }
if ch == "\r" { return true }
false
}
fn make_tok(kind: String, value: String) -> Map<String, Any> {
{ "kind": kind, "value": value }
}
// Keyword lookup
fn keyword_kind(word: String) -> String {
if word == "let" { return "Let" }
if word == "fn" { return "Fn" }
if word == "type" { return "Type" }
if word == "enum" { return "Enum" }
if word == "match" { return "Match" }
if word == "return" { return "Return" }
if word == "if" { return "If" }
if word == "else" { return "Else" }
if word == "for" { return "For" }
if word == "in" { return "In" }
if word == "while" { return "While" }
if word == "import" { return "Import" }
if word == "from" { return "From" }
if word == "as" { return "As" }
if word == "with" { return "With" }
if word == "sealed" { return "Sealed" }
if word == "activate" { return "Activate" }
if word == "where" { return "Where" }
if word == "test" { return "Test" }
if word == "seed" { return "Seed" }
if word == "assert" { return "Assert" }
if word == "protocol" { return "Protocol" }
if word == "impl" { return "Impl" }
if word == "retry" { return "Retry" }
if word == "times" { return "Times" }
if word == "fallback" { return "Fallback" }
if word == "reason" { return "Reason" }
if word == "parallel" { return "Parallel" }
if word == "trace" { return "Trace" }
if word == "requires" { return "Requires" }
if word == "deploy" { return "Deploy" }
if word == "to" { return "To" }
if word == "via" { return "Via" }
if word == "target" { return "Target" }
if word == "true" { return "Bool" }
if word == "false" { return "Bool" }
if word == "cgi" { return "Cgi" }
if word == "service" { return "Service" }
if word == "manager" { return "Manager" }
if word == "engine" { return "Engine" }
if word == "accessor" { return "Accessor" }
if word == "vessel" { return "Vessel" }
if word == "extern" { return "Extern" }
""
}
// Scan helpers
// All scan helpers receive the chars list and total length.
// scan_digits advance i while chars[i] is a digit
// Returns { "text": ..., "pos": i }
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if lex_is_digit(ch) {
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// scan_ident advance i while chars[i] is alphanumeric or underscore
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if is_alnum_or_underscore(ch) {
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// Code-bearing string detection + comment strip
// Inline JS/CSS literals embedded in El source (e.g. <script></script> blobs
// or stylesheet payloads inside string literals) carry their own line and
// block comments. Those comments leak into the served HTML and reveal build
// notes the visitor should never see. We strip them at the lexer so every
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
//
// looks_like_code heuristic gate so we only strip strings that actually
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
let nchars: [String] = native_string_chars(needle)
let nlen: Int = native_list_len(nchars)
if start + nlen > total { return false }
let i = 0
let matched = true
while i < nlen {
let a: String = native_list_get(chars, start + i)
let b: String = native_list_get(nchars, i)
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
}
matched
}
fn str_has(s: String, needle: String) -> Bool {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let i = 0
let found = false
while i < total {
if substr_at(chars, i, total, needle) {
let found = true
let i = total
} else {
let i = i + 1
}
}
found
}
fn looks_like_code(s: String) -> Bool {
if str_has(s, "<script") { return true }
if str_has(s, "<style") { return true }
if str_has(s, "function") {
if str_has(s, ";") { return true }
}
false
}
// strip_code_comments character-by-character walk. Tracks JS string state
// (single, double, backtick) and never strips inside one. Backslash escapes
// inside JS strings consume the next char verbatim. URLs like https:// are
// preserved by checking the previous char before treating // as a line
// comment opener: if the char immediately before '/' is ':', emit the '/'
// literally and advance one position.
fn strip_code_comments(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let out_parts: [String] = native_list_empty()
let i = 0
let in_squote = false
let in_dquote = false
let in_btick = false
let prev = ""
while i < total {
let ch: String = native_list_get(chars, i)
let in_js_string = false
if in_squote { let in_js_string = true }
if in_dquote { let in_js_string = true }
if in_btick { let in_js_string = true }
if in_js_string {
// Backslash escape: consume next char verbatim regardless of which.
if ch == "\\" {
let out_parts = native_list_append(out_parts, ch)
let next_i = i + 1
if next_i < total {
let nc: String = native_list_get(chars, next_i)
let out_parts = native_list_append(out_parts, nc)
let prev = nc
let i = next_i + 1
} else {
let prev = ch
let i = next_i
}
} else {
if in_squote {
if ch == "'" { let in_squote = false }
} else {
if in_dquote {
if ch == "\"" { let in_dquote = false }
} else {
if in_btick {
if ch == "`" { let in_btick = false }
}
}
}
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
} else {
// Not in a JS string. Check for comment openers.
let next_i = i + 1
let next_ch = ""
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
}
if ch == "/" {
if next_ch == "/" {
// URL guard: prev char ':' means this is "://", not a comment.
if prev == ":" {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
// Skip until newline (newline itself is preserved so
// surrounding line counts/structure stay sane).
let i = i + 2
let scanning = true
while scanning {
if i >= total {
let scanning = false
} else {
let lc: String = native_list_get(chars, i)
if lc == "\n" {
let scanning = false
} else {
let i = i + 1
}
}
}
let prev = ""
}
} else {
if next_ch == "*" {
// Skip until matching "*/".
let i = i + 2
let scanning2 = true
while scanning2 {
if i >= total {
let scanning2 = false
} else {
let bc: String = native_list_get(chars, i)
if bc == "*" {
let after = i + 1
if after < total {
let nc2: String = native_list_get(chars, after)
if nc2 == "/" {
let i = after + 1
let scanning2 = false
} else {
let i = i + 1
}
} else {
let i = i + 1
}
} else {
let i = i + 1
}
}
}
let prev = ""
} else {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
}
} else {
// Open a JS string?
if ch == "'" {
let in_squote = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "\"" {
let in_dquote = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "`" {
let in_btick = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
}
}
}
}
}
str_join(out_parts, "")
}
// scan_string scan a quoted string literal, handling \" escapes.
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if ch == "\\" {
// escape: peek next char
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "\"" {
let parts = native_list_append(parts, "\"")
let i = next_i + 1
} else {
if next_ch == "n" {
let parts = native_list_append(parts, "\n")
let i = next_i + 1
} else {
if next_ch == "t" {
let parts = native_list_append(parts, "\t")
let i = next_i + 1
} else {
if next_ch == "r" {
let parts = native_list_append(parts, "\r")
let i = next_i + 1
} else {
if next_ch == "\\" {
let parts = native_list_append(parts, "\\")
let i = next_i + 1
} else {
let parts = native_list_append(parts, next_ch)
let i = next_i + 1
}
}
}
}
}
} else {
let i = i + 1
}
} else {
if ch == "\"" {
let i = i + 1
let running = false
} else {
let parts = native_list_append(parts, ch)
let i = i + 1
}
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// Main lexer
fn lex(source: String) -> [Map<String, Any>] {
let chars: [String] = native_string_chars(source)
let total: Int = native_list_len(chars)
let tokens: [Map<String, Any>] = native_list_empty()
let i: Int = 0
while i < total {
let ch: String = native_list_get(chars, i)
// Skip whitespace
if lex_is_whitespace(ch) {
let i = i + 1
} else {
// Line comments: //
if ch == "/" {
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "/" {
// skip to end of line
let i = i + 2
let running2 = true
while running2 {
if i >= total {
let running2 = false
} else {
let lch: String = native_list_get(chars, i)
if lch == "\n" {
let running2 = false
} else {
let i = i + 1
}
}
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let i = i + 1
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let i = i + 1
}
} else {
// String literal
if ch == "\"" {
let result = scan_string(chars, i + 1, total)
let str_text: String = result["text"]
let new_pos: Int = result["pos"]
// Compile-time scrub: strings that embed JS or CSS get
// their // line comments and /* block comments stripped
// before the token reaches the parser. Plain prose passes
// through untouched.
let clean_text = str_text
if looks_like_code(str_text) {
let clean_text = strip_code_comments(str_text)
}
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
let i = new_pos
} else {
// Number literal
if lex_is_digit(ch) {
let result = scan_digits(chars, i, total)
let num_text: String = result["text"]
let new_pos: Int = result["pos"]
// check for float (dot followed by digit)
if new_pos < total {
let dot_ch: String = native_list_get(chars, new_pos)
if dot_ch == "." {
let after_dot = new_pos + 1
if after_dot < total {
let after_dot_ch: String = native_list_get(chars, after_dot)
if lex_is_digit(after_dot_ch) {
let frac_result = scan_digits(chars, after_dot, total)
let frac_text: String = frac_result["text"]
let frac_pos: Int = frac_result["pos"]
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
let i = frac_pos
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
// Identifier or keyword
if lex_is_alpha(ch) || ch == "_" {
let result = scan_ident(chars, i, total)
let word: String = result["text"]
let new_pos: Int = result["pos"]
let kw = keyword_kind(word)
if kw == "" {
let tokens = native_list_append(tokens, make_tok("Ident", word))
} else {
let tokens = native_list_append(tokens, make_tok(kw, word))
}
let i = new_pos
} else {
// Multi-char and single-char operators/delimiters
let peek_i = i + 1
let peek_ch = ""
if peek_i < total {
let peek_ch: String = native_list_get(chars, peek_i)
}
if ch == "=" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Eq", "="))
let i = i + 1
}
}
} else {
if ch == "!" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Not", "!"))
let i = i + 1
}
} else {
if ch == "<" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
let i = i + 1
}
} else {
if ch == ">" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
let i = i + 1
}
} else {
if ch == "&" {
if peek_ch == "&" {
let tokens = native_list_append(tokens, make_tok("And", "&&"))
let i = i + 2
} else {
let i = i + 1
}
} else {
if ch == "|" {
if peek_ch == "|" {
let tokens = native_list_append(tokens, make_tok("Or", "||"))
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
let i = i + 1
}
}
} else {
if ch == "-" {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
let i = i + 1
}
} else {
if ch == ":" {
if peek_ch == ":" {
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
let i = i + 1
}
} else {
if ch == "+" {
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
let i = i + 1
} else {
if ch == "*" {
let tokens = native_list_append(tokens, make_tok("Star", "*"))
let i = i + 1
} else {
if ch == "%" {
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
let i = i + 1
} else {
if ch == "(" {
let tokens = native_list_append(tokens, make_tok("LParen", "("))
let i = i + 1
} else {
if ch == ")" {
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
let i = i + 1
} else {
if ch == "{" {
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
let i = i + 1
} else {
if ch == "}" {
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
let i = i + 1
} else {
if ch == "[" {
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
let i = i + 1
} else {
if ch == "]" {
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
let i = i + 1
} else {
if ch == "," {
let tokens = native_list_append(tokens, make_tok("Comma", ","))
let i = i + 1
} else {
if ch == "." {
let tokens = native_list_append(tokens, make_tok("Dot", "."))
let i = i + 1
} else {
if ch == ";" {
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
let i = i + 1
} else {
if ch == "@" {
let tokens = native_list_append(tokens, make_tok("At", "@"))
let i = i + 1
} else {
if ch == "?" {
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
let tokens = native_list_append(tokens, make_tok("Eof", ""))
tokens
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
# Compiled El bytecode
*.elc
# C codegen output
*.c
*.o
*.a
*.so
*.dylib
# Combined build artifacts
_combined.el
*-combined.el
# Distribution / build output
dist/
build/
out/
# OS
.DS_Store
+85
View File
@@ -0,0 +1,85 @@
package "elp" {
version "0.7.0"
description "Engram Language Protocol — bidirectional engine mapping between Engram semantic forms and natural language surface text. 31 languages."
edition "2026"
}
build {
entry "src/elp.el"
// Compilation order (dependency order):
// language-profile (no deps)
// vocabulary (no deps)
// morphology (depends on: language-profile)
// morphology-es (depends on: morphology) Spanish
// morphology-fr (depends on: morphology) French
// morphology-de (depends on: morphology) German
// morphology-ru (depends on: morphology) Russian
// morphology-ja (depends on: morphology) Japanese
// morphology-fi (depends on: morphology) Finnish
// morphology-ar (depends on: morphology) Arabic
// morphology-hi (depends on: morphology) Hindi
// morphology-sw (depends on: morphology) Swahili
// morphology-la (depends on: morphology) Latin
// morphology-he (depends on: morphology) Hebrew
// morphology-grc (depends on: morphology) Ancient Greek
// morphology-ang (depends on: morphology) Old English
// morphology-sa (depends on: morphology) Sanskrit
// morphology-got (depends on: morphology) Gothic
// morphology-non (depends on: morphology) Old Norse
// morphology-enm (depends on: morphology) Middle English
// morphology-pi (depends on: morphology) Pali
// morphology-fro (depends on: morphology) Old French
// morphology-goh (depends on: morphology) Old High German
// morphology-sga (depends on: morphology) Old Irish
// morphology-txb (depends on: morphology) Tocharian B
// morphology-peo (depends on: morphology) Old Persian
// morphology-akk (depends on: morphology) Akkadian
// morphology-uga (depends on: morphology) Ugaritic
// morphology-egy (depends on: morphology) Ancient Egyptian
// morphology-sux (depends on: morphology) Sumerian
// morphology-gez (depends on: morphology) Ge'ez (Classical Ethiopic)
// morphology-cop (depends on: morphology) Coptic (Sahidic)
// grammar (depends on: language-profile)
// realizer (depends on: morphology, grammar, language-profile)
// semantics (depends on: grammar, realizer, language-profile)
// elp (depends on: semantics, realizer)
sources [
"src/language-profile.el",
"src/vocabulary.el",
"src/morphology.el",
"src/morphology-es.el",
"src/morphology-fr.el",
"src/morphology-de.el",
"src/morphology-ru.el",
"src/morphology-ja.el",
"src/morphology-fi.el",
"src/morphology-ar.el",
"src/morphology-hi.el",
"src/morphology-sw.el",
"src/morphology-la.el",
"src/morphology-he.el",
"src/morphology-grc.el",
"src/morphology-ang.el",
"src/morphology-sa.el",
"src/morphology-got.el",
"src/morphology-non.el",
"src/morphology-enm.el",
"src/morphology-pi.el",
"src/morphology-fro.el",
"src/morphology-goh.el",
"src/morphology-sga.el",
"src/morphology-txb.el",
"src/morphology-peo.el",
"src/morphology-akk.el",
"src/morphology-uga.el",
"src/morphology-egy.el",
"src/morphology-sux.el",
"src/morphology-gez.el",
"src/morphology-cop.el",
"src/grammar.el",
"src/realizer.el",
"src/semantics.el",
"src/elp.el",
]
}
+1
View File
@@ -0,0 +1 @@
import "language-profile.el"
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
import "dedup_test_a_nodedup.el"
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
+1
View File
@@ -0,0 +1 @@
extern fn fn_a(x: String) -> String
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
+6
View File
@@ -0,0 +1,6 @@
import "language-profile.el"
import "dedup_test_a.el"
fn main_fn(x: String) -> String {
return x
}
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
import "dedup_test_a.el"
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
import "dedup_test_a_notail.el"
+159
View File
@@ -0,0 +1,159 @@
// elp.el - Engram Language Protocol public API.
//
// Output half of the ELP: Engram semantic form natural language surface text.
// 31 languages. Ties together language-profile, vocabulary, morphology,
// grammar, realizer, and semantics into a single entry point.
//
// Import chain (mirrors manifest.el dependency order):
// language-profile (no deps)
// vocabulary (no deps)
// morphology (depends on: language-profile)
// morphology-XX (depends on: morphology) all language engines
// grammar (depends on: language-profile)
// realizer (depends on: morphology, grammar, language-profile)
// semantics (depends on: grammar, realizer, language-profile)
//
// When elc processes a source that imports this file, it resolves all
// transitive imports via depth-first deduplication each module is
// inlined exactly once regardless of how many importers reference it.
// Base layers
import "language-profile.el"
import "vocabulary.el"
// Morphology: base engine
import "morphology.el"
// Morphology: living languages
import "morphology-es.el"
import "morphology-fr.el"
import "morphology-de.el"
import "morphology-ru.el"
import "morphology-ja.el"
import "morphology-fi.el"
import "morphology-ar.el"
import "morphology-hi.el"
import "morphology-sw.el"
// Morphology: ancient / classical
import "morphology-la.el"
import "morphology-he.el"
// Morphology: dead languages
import "morphology-grc.el"
import "morphology-ang.el"
import "morphology-sa.el"
import "morphology-got.el"
import "morphology-non.el"
import "morphology-enm.el"
import "morphology-pi.el"
import "morphology-fro.el"
import "morphology-goh.el"
import "morphology-sga.el"
import "morphology-txb.el"
import "morphology-peo.el"
import "morphology-akk.el"
import "morphology-uga.el"
import "morphology-egy.el"
import "morphology-sux.el"
import "morphology-gez.el"
import "morphology-cop.el"
// Higher layers
import "grammar.el"
import "realizer.el"
import "semantics.el"
//
// Entry points:
//
// generate(semantic_form_json) -> String
// Low-level JSON-based API, defaults to English. SemanticForm JSON fields:
// intent - "assert" | "question" | "command"
// agent - subject (pronoun or noun phrase, optional for commands)
// predicate - verb base form
// patient - object noun phrase (optional)
// location - prepositional phrase e.g. "in the park" (optional)
// tense - "present" | "past" | "future" (default: "present")
// aspect - "simple" | "progressive" | "perfect" (default: "simple")
// lang - ISO 639-1 code (default: "en")
//
// generate_lang(semantic_form_json, lang_code) -> String
// JSON-based API with explicit language code (overrides any "lang" in JSON).
//
// generate_frame(frame: SemFrame) -> String
// High-level SemFrame API. Language from frame's "lang" field (default "en").
// Intents: "assert" | "query" | "describe" | "greet".
//
// generate_frame_lang(frame: SemFrame, lang_code: String) -> String
// High-level SemFrame API with explicit language code override.
// JSON helpers
fn sem_get(json: String, key: String) -> String {
let val: String = json_get(json, key)
return val
}
// Public API: SemFrame
// Generate text from a SemFrame in the language embedded in the frame (default "en").
fn generate_frame(frame: [String]) -> String {
return sem_realize(frame)
}
// Generate text from a SemFrame in the specified language.
fn generate_frame_lang(frame: [String], lang_code: String) -> String {
return sem_realize_lang(frame, lang_code)
}
// Public API: JSON
// Build a realizer slot map from JSON fields and an explicit lang code.
fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String] {
let intent: String = sem_get(semantic_form_json, "intent")
let agent: String = sem_get(semantic_form_json, "agent")
let predicate: String = sem_get(semantic_form_json, "predicate")
let patient: String = sem_get(semantic_form_json, "patient")
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 form: [String] = native_list_empty()
let form = native_list_append(form, "intent")
let form = native_list_append(form, intent)
let form = native_list_append(form, "agent")
let form = native_list_append(form, agent)
let form = native_list_append(form, "predicate")
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, "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)
let form = native_list_append(form, "lang")
let form = native_list_append(form, lang_code)
return form
}
// Generate text from a JSON semantic form. Language defaults to "en" unless
// the JSON contains a "lang" field.
fn generate(semantic_form_json: String) -> String {
let lang_in_json: String = sem_get(semantic_form_json, "lang")
let lang_code: String = lang_in_json
if str_eq(lang_code, "") {
let lang_code = "en"
}
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
return realize(form)
}
// Generate text from a JSON semantic form in the specified language.
// lang_code overrides any "lang" field present in the JSON.
fn generate_lang(semantic_form_json: String, lang_code: String) -> String {
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
return realize(form)
}
+7
View File
@@ -0,0 +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(semantic_form_json: String) -> String
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
+2
View File
@@ -0,0 +1,2 @@
import "language-profile.el"
extern fn fn_b(x: String) -> String
+555
View File
@@ -0,0 +1,555 @@
// grammar.el - Grammar engine: syntactic structure, word order, phrase assembly.
//
// Language-specific word order and question strategy are driven by the language
// profile, not hardcoded. The slot map format (GramSpec) is universal; a "lang"
// key carries the ISO 639-1 code so every downstream function can resolve the
// active profile.
//
// GramSpec slot keys:
// intent - "assert" | "question" | "command"
// agent - subject referent string
// predicate - verb base form
// patient - object noun phrase (optional)
// location - prepositional phrase (optional)
// tense - "present" | "past" | "future"
// aspect - "simple" | "progressive" | "perfect"
// lang - ISO 639-1 code (default "en")
// verb_surf - conjugated verb surface form (computed)
// aux_surf - auxiliary surface form (computed)
//
// Depends on: language-profile
// Slot map helpers
fn slots_get(slots: [String], key: String) -> String {
let n: Int = native_list_len(slots)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(slots, i)
if str_eq(k, key) {
return native_list_get(slots, i + 1)
}
let i = i + 2
}
return ""
}
fn slots_set(slots: [String], key: String, val: String) -> [String] {
let n: Int = native_list_len(slots)
let result: [String] = native_list_empty()
let found: Bool = false
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(slots, i)
let v: String = native_list_get(slots, i + 1)
if str_eq(k, key) {
let result = native_list_append(result, k)
let result = native_list_append(result, val)
let found = true
} else {
let result = native_list_append(result, k)
let result = native_list_append(result, v)
}
let i = i + 2
}
if !found {
let result = native_list_append(result, key)
let result = native_list_append(result, val)
}
return result
}
fn make_slots(k0: String, v0: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, k0)
let r = native_list_append(r, v0)
return r
}
fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] {
let r: [String] = make_slots(k0, v0)
let r = native_list_append(r, k1)
let r = native_list_append(r, v1)
return r
}
fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] {
let r: [String] = make_slots2(k0, v0, k1, v1)
let r = native_list_append(r, k2)
let r = native_list_append(r, v2)
return r
}
fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] {
let r: [String] = make_slots3(k0, v0, k1, v1, k2, v2)
let r = native_list_append(r, k3)
let r = native_list_append(r, v3)
return r
}
fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] {
let r: [String] = make_slots4(k0, v0, k1, v1, k2, v2, k3, v3)
let r = native_list_append(r, k4)
let r = native_list_append(r, v4)
return r
}
// Grammar rule catalog
fn rule_id(rule: [String]) -> String {
return native_list_get(rule, 0)
}
fn rule_lhs(rule: [String]) -> String {
return native_list_get(rule, 1)
}
fn rule_rhs_len(rule: [String]) -> Int {
let n: Int = native_list_len(rule)
return n - 2
}
fn rule_rhs(rule: [String], idx: Int) -> String {
return native_list_get(rule, idx + 2)
}
fn make_rule(id: String, lhs: String, r0: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, id)
let r = native_list_append(r, lhs)
let r = native_list_append(r, r0)
return r
}
fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] {
let r: [String] = make_rule(id, lhs, r0)
let r = native_list_append(r, r1)
return r
}
fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] {
let r: [String] = make_rule2(id, lhs, r0, r1)
let r = native_list_append(r, r2)
return r
}
fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] {
let r: [String] = make_rule3(id, lhs, r0, r1, r2)
let r = native_list_append(r, r3)
return r
}
fn build_rules() -> [[String]] {
let rules: [[String]] = native_list_empty()
let rules = native_list_append(rules, make_rule2("S-DECL", "S", "NP", "VP"))
let rules = native_list_append(rules, make_rule3("S-QUEST", "S", "Aux", "NP", "VP"))
let rules = native_list_append(rules, make_rule("S-IMP", "S", "VP"))
let rules = native_list_append(rules, make_rule2("NP-DET-N", "NP", "Det", "N"))
let rules = native_list_append(rules, make_rule3("NP-DET-ADJ-N","NP", "Det", "Adj", "N"))
let rules = native_list_append(rules, make_rule("NP-PRON", "NP", "Pron"))
let rules = native_list_append(rules, make_rule("NP-N", "NP", "N"))
let rules = native_list_append(rules, make_rule("VP-V", "VP", "V"))
let rules = native_list_append(rules, make_rule2("VP-V-NP", "VP", "V", "NP"))
let rules = native_list_append(rules, make_rule2("VP-V-PP", "VP", "V", "PP"))
let rules = native_list_append(rules, make_rule3("VP-V-NP-PP", "VP", "V", "NP", "PP"))
let rules = native_list_append(rules, make_rule2("VP-AUX-V", "VP", "Aux", "V"))
let rules = native_list_append(rules, make_rule3("VP-AUX-V-NP", "VP", "Aux", "V", "NP"))
let rules = native_list_append(rules, make_rule2("PP-P-NP", "PP", "P", "NP"))
return rules
}
fn get_rules() -> [[String]] {
return build_rules()
}
fn find_rule(rule_id_str: String) -> [String] {
let rules: [[String]] = get_rules()
let n: Int = native_list_len(rules)
let i: Int = 0
while i < n {
let rule: [String] = native_list_get(rules, i)
let id: String = native_list_get(rule, 0)
if str_eq(id, rule_id_str) {
return rule
}
let i = i + 1
}
let empty: [String] = native_list_empty()
return empty
}
// Tree node construction
fn make_leaf(label: String, word: String) -> String {
return "(" + label + " " + word + ")"
}
fn make_node1(label: String, child0: String) -> String {
return "(" + label + " _ " + child0 + ")"
}
fn make_node2(label: String, child0: String, child1: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + ")"
}
fn make_node3(label: String, child0: String, child1: String, child2: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + ")"
}
fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + " " + child3 + ")"
}
// Tree rendering
fn nlg_is_ws(c: String) -> Bool {
if str_eq(c, " ") { return true }
if str_eq(c, "\t") { return true }
if str_eq(c, "\n") { return true }
return false
}
fn skip_ws(s: String, pos: Int) -> Int {
let n: Int = str_len(s)
let i: Int = pos
let running: Bool = true
while running {
if i >= n {
let running = false
} else {
let c: String = str_slice(s, i, i + 1)
if nlg_is_ws(c) {
let i = i + 1
} else {
let running = false
}
}
}
return i
}
fn scan_token(s: String, start: Int) -> [String] {
let n: Int = str_len(s)
let i: Int = start
let running: Bool = true
while running {
if i >= n {
let running = false
} else {
let c: String = str_slice(s, i, i + 1)
if nlg_is_ws(c) {
let running = false
} else {
if str_eq(c, "(") {
let running = false
} else {
if str_eq(c, ")") {
let running = false
} else {
let i = i + 1
}
}
}
}
}
let tok: String = str_slice(s, start, i)
let result: [String] = native_list_empty()
let result = native_list_append(result, tok)
let result = native_list_append(result, int_to_str(i))
return result
}
fn render_tree(tree: String) -> String {
let words: [String] = native_list_empty()
let n: Int = str_len(tree)
let i: Int = 0
let prev_was_open: Bool = false
while i < n {
let c: String = str_slice(tree, i, i + 1)
if str_eq(c, "(") {
let prev_was_open = true
let i = i + 1
} else {
if str_eq(c, ")") {
let prev_was_open = false
let i = i + 1
} else {
if nlg_is_ws(c) {
let i = i + 1
} else {
let tok_info: [String] = scan_token(tree, i)
let tok: String = native_list_get(tok_info, 0)
let new_i: Int = str_to_int(native_list_get(tok_info, 1))
let i = new_i
if prev_was_open {
let prev_was_open = false
} else {
if !str_eq(tok, "_") {
let words = native_list_append(words, tok)
}
}
}
}
}
}
return str_join(words, " ")
}
// Word-order engine
// gram_word_order: returns the word order string from a profile.
fn gram_word_order(profile: [String]) -> String {
return lang_word_order(profile)
}
// gram_order_constituents: order Subject, Verb, Object tokens according to the
// language profile's word_order.
//
// subj, verb, obj: surface strings (may be empty).
// Returns a space-joined string in the correct order.
//
// Supported orders: SVO, SOV, VSO, VOS, OVS, OSV, free (defaults to SVO).
fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String {
let order: String = gram_word_order(profile)
let parts: [String] = native_list_empty()
if str_eq(order, "SVO") {
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
if str_eq(order, "SOV") {
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
return str_join(parts, " ")
}
if str_eq(order, "VSO") {
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
if str_eq(order, "VOS") {
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
return str_join(parts, " ")
}
if str_eq(order, "OVS") {
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
return str_join(parts, " ")
}
if str_eq(order, "OSV") {
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
return str_join(parts, " ")
}
// "free" and unknown: use SVO as the neutral citation order.
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
// gram_build_vp: construct a verb phrase surface string.
//
// verb: main verb surface form.
// aux: auxiliary surface form (empty if none).
// profile: language profile.
//
// In SVO/VSO/VOS languages the auxiliary precedes the main verb.
// In SOV languages the verb cluster appears at the end; we keep aux before V
// as a reasonable default for the auxiliary-final constructions in those languages.
fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String {
if str_eq(aux, "") {
return verb
}
return aux + " " + verb
}
// gram_question_strategy: returns the question formation strategy for a language.
//
// "do-support" - English: "Do you see?" do-auxiliary inserted, verb stays base
// "particle" - Japanese: sentence-final appended
// "intonation" - Mandarin, Spanish: rising intonation only, word order unchanged
// "inversion" - French, German: subject-verb inversion
fn gram_question_strategy(profile: [String]) -> String {
let code: String = lang_get(profile, "code")
if str_eq(code, "en") { return "do-support" }
if str_eq(code, "ja") { return "particle" }
if str_eq(code, "zh") { return "intonation" }
if str_eq(code, "es") { return "intonation" }
if str_eq(code, "fr") { return "inversion" }
if str_eq(code, "de") { return "inversion" }
if str_eq(code, "ar") { return "intonation" }
if str_eq(code, "hi") { return "particle" }
if str_eq(code, "ru") { return "intonation" }
if str_eq(code, "fi") { return "particle" }
if str_eq(code, "sw") { return "intonation" }
if str_eq(code, "la") { return "intonation" } // Latin: word order marks Q (VSO or -ne suffix)
if str_eq(code, "he") { return "intonation" } // Modern Hebrew: rising intonation
if str_eq(code, "grc") { return "intonation" } // Ancient Greek: ἆρα particle or intonation
if str_eq(code, "ang") { return "intonation" } // Old English: hwæþer particle or intonation
if str_eq(code, "sa") { return "intonation" } // Sanskrit: kim particle or intonation
if str_eq(code, "got") { return "intonation" } // Gothic: ibai particle or intonation
if str_eq(code, "non") { return "intonation" } // Old Norse: hvárr particle or intonation
if str_eq(code, "enm") { return "do-support" } // Middle English: do-support emerging
if str_eq(code, "pi") { return "intonation" } // Pali: kim particle or intonation
// Unknown: default to intonation (safest never wrong, just flat)
return "intonation"
}
// NP and PP assembly
//
// These functions are profile-aware but the logic is the same across languages
// because we work with pre-assembled strings (Engram vocabulary supplies
// language-specific forms before these functions see them).
fn is_pronoun(word: String) -> Bool {
if str_eq(word, "I") { return true }
if str_eq(word, "you") { return true }
if str_eq(word, "he") { return true }
if str_eq(word, "she") { return true }
if str_eq(word, "it") { return true }
if str_eq(word, "we") { return true }
if str_eq(word, "they") { return true }
if str_eq(word, "me") { return true }
if str_eq(word, "him") { return true }
if str_eq(word, "her") { return true }
if str_eq(word, "us") { return true }
if str_eq(word, "them") { return true }
return false
}
// build_np: assemble a noun phrase tree from a referent string.
// profile parameter reserved for future case-marking / article agreement.
fn build_np(referent: String, slots: [String]) -> String {
if is_pronoun(referent) {
return make_node1("NP", make_leaf("Pron", referent))
}
let parts: [String] = str_split(referent, " ")
let np: Int = native_list_len(parts)
if np == 1 {
return make_node1("NP", make_leaf("N", referent))
}
if np == 2 {
let det: String = native_list_get(parts, 0)
let noun: String = native_list_get(parts, 1)
return make_node2("NP", make_leaf("Det", det), make_leaf("N", noun))
}
if np == 3 {
let det: String = native_list_get(parts, 0)
let adj: String = native_list_get(parts, 1)
let noun: String = native_list_get(parts, 2)
return make_node3("NP", make_leaf("Det", det), make_leaf("Adj", adj), make_leaf("N", noun))
}
return make_node1("NP", make_leaf("N", referent))
}
// build_pp: assemble a prepositional phrase tree from a "PREP NP" string.
// For postpositional languages (ja, hi, ko) the slot value is expected to be
// already pre-assembled with the postposition in the correct position by the
// caller (vocabulary lookup from Engram supplies the right surface form).
fn build_pp(loc: String) -> String {
let parts: [String] = str_split(loc, " ")
let n: Int = native_list_len(parts)
if n < 2 {
return make_leaf("PP", loc)
}
let prep: String = native_list_get(parts, 0)
let np_parts: [String] = native_list_empty()
let i: Int = 1
while i < n {
let np_parts = native_list_append(np_parts, native_list_get(parts, i))
let i = i + 1
}
let np_str: String = str_join(np_parts, " ")
let np_tree: String = build_np(np_str, native_list_empty())
return make_node2("PP", make_leaf("P", prep), np_tree)
}
// VP tree construction
fn build_vp_body(slots: [String]) -> String {
let verb_surf: String = slots_get(slots, "verb_surf")
let patient: String = slots_get(slots, "patient")
let loc: String = slots_get(slots, "location")
if !str_eq(patient, "") {
let obj_np: String = build_np(patient, slots)
if !str_eq(loc, "") {
let pp: String = build_pp(loc)
return make_node3("VP", make_leaf("V", verb_surf), obj_np, pp)
}
return make_node2("VP", make_leaf("V", verb_surf), obj_np)
}
if !str_eq(loc, "") {
let pp: String = build_pp(loc)
return make_node2("VP", make_leaf("V", verb_surf), pp)
}
return make_node1("VP", make_leaf("V", verb_surf))
}
fn build_vp_from_slots(slots: [String]) -> String {
let aux_surf: String = slots_get(slots, "aux_surf")
if !str_eq(aux_surf, "") {
let verb_surf: String = slots_get(slots, "verb_surf")
let patient: String = slots_get(slots, "patient")
let loc: String = slots_get(slots, "location")
if !str_eq(patient, "") {
let obj_np: String = build_np(patient, slots)
return make_node3("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf), obj_np)
}
return make_node2("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf))
}
return build_vp_body(slots)
}
// Tree generator
fn generate_tree(rule_id_str: String, slots: [String]) -> String {
let rule: [String] = find_rule(rule_id_str)
let n: Int = native_list_len(rule)
if n == 0 {
return make_leaf("ERR", "unknown-rule")
}
let lhs: String = native_list_get(rule, 1)
if str_eq(rule_id_str, "S-DECL") {
let agent: String = slots_get(slots, "agent")
let np_tree: String = build_np(agent, slots)
let vp_tree: String = build_vp_from_slots(slots)
return make_node2("S", np_tree, vp_tree)
}
if str_eq(rule_id_str, "S-QUEST") {
let agent: String = slots_get(slots, "agent")
let np_tree: String = build_np(agent, slots)
let vp_tree: String = build_vp_body(slots)
let aux_surf: String = slots_get(slots, "aux_surf")
return make_node3("S", make_leaf("Aux", aux_surf), np_tree, vp_tree)
}
if str_eq(rule_id_str, "S-IMP") {
let vp_tree: String = build_vp_from_slots(slots)
return make_node1("S", vp_tree)
}
return make_leaf(lhs, "?")
}
+38
View File
@@ -0,0 +1,38 @@
// auto-generated by elc --emit-header — do not edit
extern fn slots_get(slots: [String], key: String) -> String
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
extern fn make_slots(k0: String, v0: String) -> [String]
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
extern fn rule_id(rule: [String]) -> String
extern fn rule_lhs(rule: [String]) -> String
extern fn rule_rhs_len(rule: [String]) -> Int
extern fn rule_rhs(rule: [String], idx: Int) -> String
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
extern fn build_rules() -> [[String]]
extern fn get_rules() -> [[String]]
extern fn find_rule(rule_id_str: String) -> [String]
extern fn make_leaf(label: String, word: String) -> String
extern fn make_node1(label: String, child0: String) -> String
extern fn make_node2(label: String, child0: String, child1: String) -> String
extern fn make_node3(label: String, child0: String, child1: String, child2: String) -> String
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 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 is_pronoun(word: String) -> Bool
extern fn build_np(referent: String, slots: [String]) -> 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
+761
View File
@@ -0,0 +1,761 @@
// big language-profile for testing
fn lang_profile_big0(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big0(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big0("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big1(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big1(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big1("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big2(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big2(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big2("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big3(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big3(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big3("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big4(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big4(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big4("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big5(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big5(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big5("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big6(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big6(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big6("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big7(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big7(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big7("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big8(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big8(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big8("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big9(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big9(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big9("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big10(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big10(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big10("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big11(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big11(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big11("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big12(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big12(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big12("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big13(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big13(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big13("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big14(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big14(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big14("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big15(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big15(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big15("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big16(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big16(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big16("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big17(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big17(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big17("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big18(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big18(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big18("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big19(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big19(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big19("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
+353
View File
@@ -0,0 +1,353 @@
// language-profile.el - Language profile data and accessors.
//
// A language profile is a slot map ([String] key-value list) describing the
// typological properties of a natural language. The engine reads these
// properties to drive morphology, word-order, and question-formation without
// any per-language code paths.
//
// Profile slot keys:
// code - ISO 639-1 code: "en", "ja", "ar", "zh", "de", "fr", "es", "sw", "hi", "ru", etc.
// word_order - "SVO" | "SOV" | "VSO" | "VOS" | "OVS" | "OSV" | "free"
// morph_type - "isolating" | "agglutinative" | "fusional" | "polysynthetic"
// has_case - "true" | "false"
// has_gender - "true" | "false"
// script_dir - "ltr" | "rtl" | "ttb"
// agreement - semicolon-separated features: "number;person" | "number;person;gender;case" | "none"
// null_subject - "true" | "false" (pro-drop: subject may be omitted)
// Constructor
fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
// Accessor
fn lang_get(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
// Built-in profiles
//
// Each profile encodes typological facts about one language. These are data,
// not separate code paths. Adding a new language means adding a new profile
// and loading its vocabulary/suffix tables into the Engram - no engine changes.
// English: SVO, fusional, no grammatical case (nominative/accusative collapsed),
// no grammatical gender, left-to-right, agreement on number and person,
// obligatory subject (no pro-drop).
fn lang_profile_en() -> [String] {
return lang_profile("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
// Japanese: SOV, agglutinative, grammatical relations marked by postpositions
// (not inflectional case), no grammatical gender, left-to-right, no agreement
// morphology on verbs, pro-drop (null subject frequent).
fn lang_profile_ja() -> [String] {
return lang_profile("ja", "SOV", "agglutinative", "false", "false", "ltr", "none", "true")
}
// Arabic: VSO, fusional, full case system, grammatical gender (masc/fem),
// right-to-left script, agreement on number, person, gender, and case,
// pro-drop (subject agreement marking on verb allows subject omission).
fn lang_profile_ar() -> [String] {
return lang_profile("ar", "VSO", "fusional", "true", "true", "rtl", "number;person;gender;case", "true")
}
// Mandarin Chinese: SVO, isolating (no morphological inflection), no case,
// no grammatical gender, left-to-right, no agreement (no morphological marking),
// null subject allowed in discourse context.
fn lang_profile_zh() -> [String] {
return lang_profile("zh", "SVO", "isolating", "false", "false", "ltr", "none", "true")
}
// German: V2 (second-position verb, base SOV in subordinate clauses), fusional,
// four-case system, three grammatical genders, left-to-right, agreement on
// number, person, gender, and case, obligatory subject.
fn lang_profile_de() -> [String] {
return lang_profile("de", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Spanish: SVO, fusional, no morphological case (but object clitics exist),
// grammatical gender (masc/fem), left-to-right, agreement on number, person,
// and gender, pro-drop (rich verbal agreement allows subject omission).
fn lang_profile_es() -> [String] {
return lang_profile("es", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "true")
}
// Finnish: SOV, agglutinative, fifteen grammatical cases, no grammatical gender,
// left-to-right, agreement on number, person, and case, no pro-drop (subject
// required in finite clauses).
fn lang_profile_fi() -> [String] {
return lang_profile("fi", "SOV", "agglutinative", "true", "false", "ltr", "number;person;case", "false")
}
// Swahili: SVO, agglutinative, noun-class system (15+ classes replacing gender),
// no case inflection, left-to-right, agreement driven by noun class and number,
// pro-drop (subject prefix on verb can stand alone).
fn lang_profile_sw() -> [String] {
return lang_profile("sw", "SVO", "agglutinative", "false", "false", "ltr", "noun-class;number", "true")
}
// Hindi: SOV, fusional, case-marked postpositional system, grammatical gender
// (masc/fem), left-to-right (Devanagari script still ltr), agreement on number,
// person, gender, and case, pro-drop (subject frequently dropped).
fn lang_profile_hi() -> [String] {
return lang_profile("hi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Russian: free word order (pragmatically determined), fusional, six-case system,
// three grammatical genders, left-to-right (Cyrillic), agreement on number,
// person, gender, and case, no pro-drop (subject required).
fn lang_profile_ru() -> [String] {
return lang_profile("ru", "free", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// French: SVO, fusional, no morphological case (but clitic object pronouns),
// two grammatical genders (masc/fem), left-to-right, agreement on number,
// person, and gender, no pro-drop.
fn lang_profile_fr() -> [String] {
return lang_profile("fr", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "false")
}
// Latin: SOV (highly free word order), fusional, six-case system (nom/gen/dat/acc/abl/voc),
// three genders (masc/fem/neut), left-to-right, rich agreement on number, person, gender,
// and case, pro-drop (subject expressed in verb ending).
fn lang_profile_la() -> [String] {
return lang_profile("la", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Hebrew (Modern): SVO, Semitic trilateral root morphology, two genders (masc/fem),
// two numbers (singular/plural; dual vestigial), right-to-left (Hebrew script),
// agreement on number, person, gender; zero copula in present tense; no grammatical cases.
fn lang_profile_he() -> [String] {
return lang_profile("he", "SVO", "semitic", "true", "false", "rtl", "number;person;gender", "true")
}
// Sanskrit: SOV/free, highly fusional, 3 genders, 8 cases, 3 numbers (sg/du/pl),
// Devanagari script, rich verb system (10 classes, 9 tenses/moods), pro-drop.
fn lang_profile_sa() -> [String] {
return lang_profile("sa", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Gothic: SOV, fusional, 3 genders, 4 cases, singular/plural,
// Gothic alphabet (romanized as þ/ƕ/ai/au/ei), strong and weak classes, pro-drop.
fn lang_profile_got() -> [String] {
return lang_profile("got", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old Norse: free/SOV, fusional, 3 genders, 4 cases, singular/plural,
// definite article as noun suffix (-inn/-in/-it), strong and weak classes, pro-drop.
fn lang_profile_non() -> [String] {
return lang_profile("non", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Middle English (ca. 11001500): SVO emerging, mostly lost case system,
// -es plural/genitive, strong and weak verbs, no grammatical gender on nouns.
fn lang_profile_enm() -> [String] {
return lang_profile("enm", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
// Pali: SOV, fusional (simplified Sanskrit), 3 genders, 8 cases, sg/pl,
// Latin transliteration with IAST diacritics, Buddhist canonical language.
fn lang_profile_pi() -> [String] {
return lang_profile("pi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Ancient Greek: free/SOV word order, highly fusional, 3 genders, 5 cases (nom/acc/gen/dat/voc),
// singular/dual/plural, polytonic Greek script (Unicode), complex verb system with aspect
// (imperfective/perfective), augment in past tenses, pro-drop.
fn lang_profile_grc() -> [String] {
return lang_profile("grc", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case;aspect", "true")
}
// Old English (Anglo-Saxon): SOV/V2, fusional, 3 genders, 4 cases (nom/acc/gen/dat),
// singular/plural, Latin alphabet + þ/ð/ƿ/æ, strong and weak noun/verb classes, pro-drop.
fn lang_profile_ang() -> [String] {
return lang_profile("ang", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old French (ca. 10001300 CE): SVO/V2, fusional, two-case system (nominative/oblique),
// two genders (masculine/feminine), left-to-right, agreement on number, person, gender,
// and case, no pro-drop (subject generally required).
fn lang_profile_fro() -> [String] {
return lang_profile("fro", "SVO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Old High German (ca. 7501050 CE): SOV/V2, fusional, four-case system, three genders,
// left-to-right, agreement on number, person, gender, and case, pro-drop.
fn lang_profile_goh() -> [String] {
return lang_profile("goh", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old Irish (ca. 600900 CE): VSO, fusional, case system, three genders,
// left-to-right, agreement on number, person, gender, and case, pro-drop.
fn lang_profile_sga() -> [String] {
return lang_profile("sga", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Tocharian B (ca. 5001000 CE): SOV, fusional, case system, two genders,
// left-to-right, agreement on number, person, gender, and case, no pro-drop.
fn lang_profile_txb() -> [String] {
return lang_profile("txb", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Old Persian (ca. 525330 BCE): SOV, fusional, 8-case system, no grammatical gender,
// left-to-right, agreement on number, person, and case, pro-drop.
fn lang_profile_peo() -> [String] {
return lang_profile("peo", "SOV", "fusional", "true", "false", "ltr", "number;person;case", "true")
}
// Akkadian (Old Babylonian period, ca. 19001600 BCE): VSO, fusional, 3-case system
// (nominative/accusative/genitive with mimation), two genders, left-to-right,
// agreement on number, person, gender, and case, no pro-drop.
fn lang_profile_akk() -> [String] {
return lang_profile("akk", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Ugaritic (ca. 14001200 BCE): VSO, Semitic trilateral root morphology, 3-case system,
// two genders, left-to-right (cuneiform alphabetic script), agreement on number, person,
// gender, and case, no pro-drop.
fn lang_profile_uga() -> [String] {
return lang_profile("uga", "VSO", "semitic", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Ancient Egyptian / Middle Egyptian (ca. 21001300 BCE): SVO, agglutinative,
// no morphological case (word order + prepositions), two genders, left-to-right,
// agreement on number, person, and gender, pro-drop (zero copula in present).
fn lang_profile_egy() -> [String] {
return lang_profile("egy", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "true")
}
// Sumerian (ca. 30002000 BCE): SOV, agglutinative, ergative-absolutive case system,
// no grammatical gender (animacy distinction instead), left-to-right, agreement on
// number and person, pro-drop.
fn lang_profile_sux() -> [String] {
return lang_profile("sux", "SOV", "agglutinative", "true", "false", "ltr", "number;person", "true")
}
// Ge'ez (Classical Ethiopic, ca. 4th7th century CE): SOV, Semitic trilateral root
// morphology, two genders (masc/fem), Ethiopic/Fidel script (ltr), agreement on
// number, person, and gender, pro-drop (subject inflection on verb).
fn lang_profile_gez() -> [String] {
return lang_profile("gez", "SOV", "semitic", "true", "true", "ltr", "number;person;gender", "true")
}
// Coptic (Sahidic dialect, ca. 3rd11th century CE): SVO, agglutinative, no
// morphological case, two genders (masc/fem), left-to-right (Coptic alphabet),
// agreement on number and gender via bound subject pronouns, no pro-drop (explicit
// subject prefix required on every verb).
fn lang_profile_cop() -> [String] {
return lang_profile("cop", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "false")
}
// Dispatch: code -> profile
fn lang_from_code(code: String) -> [String] {
if str_eq(code, "en") { return lang_profile_en() }
if str_eq(code, "ja") { return lang_profile_ja() }
if str_eq(code, "ar") { return lang_profile_ar() }
if str_eq(code, "zh") { return lang_profile_zh() }
if str_eq(code, "de") { return lang_profile_de() }
if str_eq(code, "es") { return lang_profile_es() }
if str_eq(code, "fi") { return lang_profile_fi() }
if str_eq(code, "sw") { return lang_profile_sw() }
if str_eq(code, "hi") { return lang_profile_hi() }
if str_eq(code, "ru") { return lang_profile_ru() }
if str_eq(code, "fr") { return lang_profile_fr() }
if str_eq(code, "la") { return lang_profile_la() }
if str_eq(code, "he") { return lang_profile_he() }
if str_eq(code, "grc") { return lang_profile_grc() }
if str_eq(code, "ang") { return lang_profile_ang() }
if str_eq(code, "sa") { return lang_profile_sa() }
if str_eq(code, "got") { return lang_profile_got() }
if str_eq(code, "non") { return lang_profile_non() }
if str_eq(code, "enm") { return lang_profile_enm() }
if str_eq(code, "pi") { return lang_profile_pi() }
if str_eq(code, "fro") { return lang_profile_fro() }
if str_eq(code, "goh") { return lang_profile_goh() }
if str_eq(code, "sga") { return lang_profile_sga() }
if str_eq(code, "txb") { return lang_profile_txb() }
if str_eq(code, "peo") { return lang_profile_peo() }
if str_eq(code, "akk") { return lang_profile_akk() }
if str_eq(code, "uga") { return lang_profile_uga() }
if str_eq(code, "egy") { return lang_profile_egy() }
if str_eq(code, "sux") { return lang_profile_sux() }
if str_eq(code, "gez") { return lang_profile_gez() }
if str_eq(code, "cop") { return lang_profile_cop() }
// Unknown code: fall back to English profile
return lang_profile_en()
}
// English default - backward compatibility entry point.
fn lang_default() -> [String] {
return lang_profile_en()
}
// Typed convenience predicates
fn lang_is_isolating(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "isolating")
}
fn lang_is_agglutinative(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "agglutinative")
}
fn lang_is_fusional(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "fusional")
}
fn lang_is_polysynthetic(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "polysynthetic")
}
fn lang_is_rtl(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "script_dir"), "rtl")
}
fn lang_has_null_subject(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "null_subject"), "true")
}
fn lang_has_case(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "has_case"), "true")
}
fn lang_has_gender(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "has_gender"), "true")
}
fn lang_word_order(profile: [String]) -> String {
return lang_get(profile, "word_order")
}
fn lang_code(profile: [String]) -> String {
return lang_get(profile, "code")
}
+46
View File
@@ -0,0 +1,46 @@
// auto-generated by elc --emit-header — do not edit
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
extern fn lang_get(profile: [String], key: String) -> String
extern fn lang_profile_en() -> [String]
extern fn lang_profile_ja() -> [String]
extern fn lang_profile_ar() -> [String]
extern fn lang_profile_zh() -> [String]
extern fn lang_profile_de() -> [String]
extern fn lang_profile_es() -> [String]
extern fn lang_profile_fi() -> [String]
extern fn lang_profile_sw() -> [String]
extern fn lang_profile_hi() -> [String]
extern fn lang_profile_ru() -> [String]
extern fn lang_profile_fr() -> [String]
extern fn lang_profile_la() -> [String]
extern fn lang_profile_he() -> [String]
extern fn lang_profile_sa() -> [String]
extern fn lang_profile_got() -> [String]
extern fn lang_profile_non() -> [String]
extern fn lang_profile_enm() -> [String]
extern fn lang_profile_pi() -> [String]
extern fn lang_profile_grc() -> [String]
extern fn lang_profile_ang() -> [String]
extern fn lang_profile_fro() -> [String]
extern fn lang_profile_goh() -> [String]
extern fn lang_profile_sga() -> [String]
extern fn lang_profile_txb() -> [String]
extern fn lang_profile_peo() -> [String]
extern fn lang_profile_akk() -> [String]
extern fn lang_profile_uga() -> [String]
extern fn lang_profile_egy() -> [String]
extern fn lang_profile_sux() -> [String]
extern fn lang_profile_gez() -> [String]
extern fn lang_profile_cop() -> [String]
extern fn lang_from_code(code: String) -> [String]
extern fn lang_default() -> [String]
extern fn lang_is_isolating(profile: [String]) -> Bool
extern fn lang_is_agglutinative(profile: [String]) -> Bool
extern fn lang_is_fusional(profile: [String]) -> Bool
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
extern fn lang_is_rtl(profile: [String]) -> Bool
extern fn lang_has_null_subject(profile: [String]) -> Bool
extern fn lang_has_case(profile: [String]) -> Bool
extern fn lang_has_gender(profile: [String]) -> Bool
extern fn lang_word_order(profile: [String]) -> String
extern fn lang_code(profile: [String]) -> String
+40
View File
@@ -0,0 +1,40 @@
import "language-profile.el"
extern fn es_pluralize(noun: String) -> String
extern fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fr_pluralize(noun: String) -> String
extern fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn de_noun_plural(noun: String, gender: String) -> String
extern fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
extern fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String
extern fn ja_conjugate(dict_form: String, form: String) -> String
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ar_sound_plural(noun: String, gender: String) -> String
extern fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn hi_noun_direct(noun: String, gender: String, number: String) -> String
extern fn hi_gender(noun: String) -> String
extern fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn sw_noun_plural(noun: String) -> String
extern fn sw_conjugate(verb: String, person: String, number: String, noun_class: String, tense: String) -> String
extern fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String
+3
View File
@@ -0,0 +1,3 @@
fn morph_tiny(x: String) -> String {
return x
}
+528
View File
@@ -0,0 +1,528 @@
// morphology-akk.el - Akkadian morphology for the NLG engine.
// 𒀭𒂗𒍪 Akkadian (akkadû), the language of Babylon and Assyria.
//
// Implements Old Babylonian Akkadian verb conjugation (G-stem / Grundstamm),
// noun declension with mimation, and noun-phrase construction.
//
// Akkadian is the oldest attested Semitic language (ca. 2800100 BCE).
// It uses cuneiform script; we work in standard Latin transliteration
// (Old Babylonian dialect the classical prestige form).
//
// Language profile:
// code=akk, name=Akkadian, morph_type=semitic, word_order=VSO/SOV,
// script=cuneiform (transliterated), family=semitic/east-semitic
//
// Key grammatical facts:
// - Semitic trilateral root system: words built from 3-consonant roots
// by inserting vowel patterns (e.g. root p-r-s iparras "he decides")
// - Grammatical gender: masculine / feminine (no neuter)
// - Cases: nominative (-um), accusative (-am), genitive (-im) "mimation"
// - Number: singular / plural (dual is vestigial in verbs)
// - Verb stems: G (basic), D (intensive), Š (causative), N (passive);
// this file implements G-stem throughout
// - Two main tense/aspect systems:
// Present-future (iparras pattern): action in progress or future
// Perfect (iptaras pattern): completed action with present relevance
// Stative (paris pattern): resultant state, often adjectival
// - No definite or indefinite article; case endings convey
// determination contextually
// - Copula: bašû (to exist/be)
//
// Verb conjugation conventions:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "present" | "perfect" | "stative"
//
// Noun declension conventions:
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural"
// gender: "m" | "f" (passed to akk_decline for gender-specific forms)
//
// Verbs covered (G-stem infinitive, transliterated):
// "bašû" to exist / be (copula)
// "alāku" to go
// "amāru" to see
// "qabû" to say
// "epēšu" to do / make
//
// Nouns covered with known mimation forms:
// "šarrum" king
// "awīlum" man / person
// "bītum" house
// "ilum" god
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn akk_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn akk_str_len(s: String) -> Int {
return str_len(s)
}
fn akk_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Slot index
//
// Maps person × number to a 0-based slot for table lookups.
// Akkadian verb agreement does not distinguish gender in 1st person,
// and the 2nd person often conflates masc/fem in some paradigms.
// We use a 6-cell paradigm matching the most common OB presentation:
//
// 0 = 1sg (I)
// 1 = 2sg (you sg)
// 2 = 3sg m (he)
// 3 = 3sg f (she)
// 4 = 1pl (we)
// 5 = 3pl (they)
//
// Note: 2pl is rare / vestigial in attested OB texts; omitted here.
fn akk_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "plural") { return 4 }
return 0
}
if str_eq(person, "second") {
return 1
}
// third
if str_eq(number, "plural") { return 5 }
return 2 // default: 3sg masc; caller may override with gender check below
}
// akk_slot_g: gender-aware slot for third person singular.
// Returns 3 (3sg fem) when person=third, number=singular, gender=f.
fn akk_slot_g(person: String, gender: String, number: String) -> Int {
let base: Int = akk_slot(person, number)
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
}
}
return base
}
// Copula: bašû to exist / be
//
// bašû is suppletive and highly irregular.
// Present: ibašši (3sg m/f), abašši (1sg), tabašši (2sg)
// Stative: bašī (3sg m), bašiat (3sg f), bašāku (1sg)
// Perfect: not commonly attested in G-stem; use present forms as fallback.
fn akk_copula_present(slot: Int) -> String {
if slot == 0 { return "abašši" } // 1sg
if slot == 1 { return "tabašši" } // 2sg
if slot == 2 { return "ibašši" } // 3sg m
if slot == 3 { return "ibašši" } // 3sg f (same form in attested OB)
if slot == 4 { return "nibašši" } // 1pl
return "ibaššū" // 3pl
}
fn akk_copula_stative(slot: Int) -> String {
if slot == 0 { return "bašāku" } // 1sg (stative 1sg: -āku suffix)
if slot == 1 { return "bašāta" } // 2sg (-āta suffix)
if slot == 2 { return "bašī" } // 3sg m (unmarked base)
if slot == 3 { return "bašiat" } // 3sg f (-at suffix)
if slot == 4 { return "bašānu" } // 1pl (-ānu suffix)
return "bašū" // 3pl ( suffix)
}
fn akk_is_copula(verb: String) -> Bool {
if str_eq(verb, "bašû") { return true }
if str_eq(verb, "bashu") { return true }
if str_eq(verb, "be") { return true }
return false
}
fn akk_conjugate_copula(tense: String, slot: Int) -> String {
if str_eq(tense, "stative") { return akk_copula_stative(slot) }
// present and perfect both fall back to present forms for bašû
return akk_copula_present(slot)
}
// alāku to go
//
// Irregular: present stem is illak- (not the expected alakk-).
// Present: illak (3sg), allak (1sg), tallak (2sg), nillak (1pl), illaku (3pl)
// Perfect: ittalk- forms (less common, use illak- + perf marker)
// Stative: use present as proxy
fn akk_alaku_present(slot: Int) -> String {
if slot == 0 { return "allak" } // 1sg
if slot == 1 { return "tallak" } // 2sg
if slot == 2 { return "illak" } // 3sg m
if slot == 3 { return "tallak" } // 3sg f (same as 2sg OB pattern)
if slot == 4 { return "nillak" } // 1pl
return "illaku" // 3pl
}
fn akk_alaku_perfect(slot: Int) -> String {
if slot == 0 { return "ittalak" } // 1sg
if slot == 1 { return "tattalak" } // 2sg
if slot == 2 { return "ittalak" } // 3sg m
if slot == 3 { return "tattalak" } // 3sg f
if slot == 4 { return "nittalak" } // 1pl
return "ittalku" // 3pl
}
// amāru to see
//
// Present (immar-): immar (3sg), ammar (1sg), tammar (2sg)
// Perfect (imtamar-): imtamar (3sg), amtamar (1sg), tamtamar (2sg)
fn akk_amaru_present(slot: Int) -> String {
if slot == 0 { return "ammar" } // 1sg
if slot == 1 { return "tammar" } // 2sg
if slot == 2 { return "immar" } // 3sg m
if slot == 3 { return "tammar" } // 3sg f
if slot == 4 { return "nimmar" } // 1pl
return "immaru" // 3pl
}
fn akk_amaru_perfect(slot: Int) -> String {
if slot == 0 { return "amtamar" } // 1sg
if slot == 1 { return "tamtamar" } // 2sg
if slot == 2 { return "imtamar" } // 3sg m
if slot == 3 { return "tamtamar" } // 3sg f
if slot == 4 { return "nimtamar" } // 1pl
return "imtamaru" // 3pl
}
fn akk_amaru_stative(slot: Int) -> String {
// amāru stative: 3sg "amir" (the one who saw / he has seen)
if slot == 0 { return "amrāku" }
if slot == 1 { return "amrāta" }
if slot == 2 { return "amir" }
if slot == 3 { return "amrat" }
if slot == 4 { return "amrānu" }
return "amrū"
}
// qabû to say / speak
//
// Present: iqabbi (3sg), aqabbi (1sg), taqabbi (2sg)
// Perfect: iqtabi (3sg), aqtabi (1sg), taqtabi (2sg)
fn akk_qabu_present(slot: Int) -> String {
if slot == 0 { return "aqabbi" } // 1sg
if slot == 1 { return "taqabbi" } // 2sg
if slot == 2 { return "iqabbi" } // 3sg m
if slot == 3 { return "taqabbi" } // 3sg f
if slot == 4 { return "niqabbi" } // 1pl
return "iqabbû" // 3pl
}
fn akk_qabu_perfect(slot: Int) -> String {
if slot == 0 { return "aqtabi" } // 1sg
if slot == 1 { return "taqtabi" } // 2sg
if slot == 2 { return "iqtabi" } // 3sg m
if slot == 3 { return "taqtabi" } // 3sg f
if slot == 4 { return "niqtabi" } // 1pl
return "iqtabû" // 3pl
}
fn akk_qabu_stative(slot: Int) -> String {
if slot == 0 { return "qabāku" }
if slot == 1 { return "qabāta" }
if slot == 2 { return "qabi" }
if slot == 3 { return "qabiat" }
if slot == 4 { return "qabānu" }
return "qabû"
}
// epēšu to do / make
//
// Present (ieppuš / eppuš): ieppuš (3sg), eppuš (1sg), teppuš (2sg)
// Perfect: iptešu forms
fn akk_epesu_present(slot: Int) -> String {
if slot == 0 { return "eppuš" } // 1sg
if slot == 1 { return "teppuš" } // 2sg
if slot == 2 { return "ieppuš" } // 3sg m
if slot == 3 { return "teppuš" } // 3sg f
if slot == 4 { return "neppuš" } // 1pl
return "ieppušu" // 3pl
}
fn akk_epesu_perfect(slot: Int) -> String {
if slot == 0 { return "iptešu" } // 1sg (irregular: root ʿ-p-š)
if slot == 1 { return "taptešu" } // 2sg
if slot == 2 { return "iptešu" } // 3sg m
if slot == 3 { return "taptešu" } // 3sg f
if slot == 4 { return "niptešu" } // 1pl
return "iptešū" // 3pl
}
fn akk_epesu_stative(slot: Int) -> String {
if slot == 0 { return "epšāku" }
if slot == 1 { return "epšāta" }
if slot == 2 { return "epuš" }
if slot == 3 { return "epšat" }
if slot == 4 { return "epšānu" }
return "epšū"
}
// Regular G-stem paradigms (iparras model)
//
// For regular verbs not in the irregular table, we apply the standard
// OB G-stem paradigm using a caller-supplied present stem and perfect stem.
// The stems must be pre-computed by the caller (or vocabulary layer).
//
// iparras (present) endings by slot:
// 1sg: a- prefix
// 2sg: ta- prefix
// 3sg m: i- prefix
// 3sg f: ta- prefix (same prefix as 2sg)
// 1pl: ni- prefix
// 3pl: i- prefix + suffix
//
// For the generic fallback we use "iparras" as the model template.
fn akk_regular_present(stem: String, slot: Int) -> String {
// stem is the 3sg m form (i-prefix already present in conventional citation)
// We rebuild from the bare root portion by stripping/adding prefixes.
// Simplification: return prefixed forms using the provided present-3sg string.
if slot == 0 { return "a" + stem } // 1sg: a + stem (strip i-, add a-)
if slot == 1 { return "ta" + stem } // 2sg
if slot == 2 { return "i" + stem } // 3sg m
if slot == 3 { return "ta" + stem } // 3sg f
if slot == 4 { return "ni" + stem } // 1pl
return "i" + stem + "u" // 3pl: i + stem +
}
fn akk_regular_perfect(stem: String, slot: Int) -> String {
// Perfect (iptaras) uses infix -ta- after first root consonant.
// stem here is the 3sg perfect form; we apply person endings.
if slot == 0 { return "a" + stem } // 1sg
if slot == 1 { return "ta" + stem } // 2sg
if slot == 2 { return "i" + stem } // 3sg m
if slot == 3 { return "ta" + stem } // 3sg f
if slot == 4 { return "ni" + stem } // 1pl
return "i" + stem + "u" // 3pl
}
fn akk_regular_stative(stem: String, slot: Int) -> String {
// Stative (paris): 3sg m has zero ending; others take person suffixes.
if slot == 0 { return stem + "āku" } // 1sg
if slot == 1 { return stem + "āta" } // 2sg
if slot == 2 { return stem } // 3sg m: bare stem
if slot == 3 { return stem + "at" } // 3sg f
if slot == 4 { return stem + "ānu" } // 1pl
return stem + "ū" // 3pl
}
// Known-verb dispatcher
fn akk_known_verb(verb: String, tense: String, slot: Int) -> String {
// bašû to be / exist
if str_eq(verb, "bašû") {
return akk_conjugate_copula(tense, slot)
}
if str_eq(verb, "bashu") {
return akk_conjugate_copula(tense, slot)
}
// alāku to go
if str_eq(verb, "alāku") {
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
if str_eq(tense, "stative") { return akk_alaku_present(slot) }
return akk_alaku_present(slot)
}
if str_eq(verb, "alaku") {
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
return akk_alaku_present(slot)
}
// amāru to see
if str_eq(verb, "amāru") {
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
return akk_amaru_present(slot)
}
if str_eq(verb, "amaru") {
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
return akk_amaru_present(slot)
}
// qabû to say
if str_eq(verb, "qabû") {
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
return akk_qabu_present(slot)
}
if str_eq(verb, "qabu") {
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
return akk_qabu_present(slot)
}
// epēšu to do / make
if str_eq(verb, "epēšu") {
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
return akk_epesu_present(slot)
}
if str_eq(verb, "epesu") {
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
return akk_epesu_present(slot)
}
return ""
}
// Main conjugation entry point
//
// akk_conjugate: conjugate an Akkadian verb (G-stem).
//
// verb: G-stem infinitive (transliterated, e.g. "alāku", "amāru")
// tense: "present" | "perfect" | "stative"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns:
// - Inflected form for known verbs
// - verb unchanged as safe fallback for unknown verbs
fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = akk_slot(person, number)
// Copula shortcut
if akk_is_copula(verb) {
return akk_conjugate_copula(tense, slot)
}
// Known-verb table
let known: String = akk_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
// Unknown verb: safe fallback
return verb
}
// Noun declension
//
// akk_decline: decline an Akkadian noun for gram_case and number.
//
// Mimation: OB nouns bear final -m in all case endings (mimation).
// The base noun (dictionary form) is the nominative singular with mimation.
// We strip the nominative -um ending (if present) to obtain the bare stem,
// then apply the requested ending.
//
// Masculine case endings (singular):
// Nominative: -um
// Accusative: -am
// Genitive: -im
//
// Masculine case endings (plural):
// Nominative: -ūtum (or in construct)
// Accusative/Genitive: -ātim (or in construct)
//
// Feminine nouns (identified by -tum nom sg ending):
// Sg nominative: -tum, accusative: -tam, genitive: -tim
// Pl nominative: -ātum, genitive/accusative: -ātim
//
// Known irregular stems (the vocabulary layer should pass dictionary forms):
// šarrum stem: šarr-
// awīlum stem: awīl-
// bītum stem: bīt-
// ilum stem: il-
fn akk_strip_nom(noun: String) -> String {
// Strip -um (masc nom sg mimation ending) to get bare stem
if akk_str_ends(noun, "um") {
return akk_str_drop_last(noun, 2)
}
// Strip -tum (fem nom sg)
if akk_str_ends(noun, "tum") {
return akk_str_drop_last(noun, 3)
}
// Already a bare stem or unusual form: return as-is
return noun
}
fn akk_is_fem(noun: String) -> Bool {
// Feminine nouns in OB typically end in -tum (nom sg)
if akk_str_ends(noun, "tum") { return true }
if akk_str_ends(noun, "tam") { return true }
if akk_str_ends(noun, "tim") { return true }
return false
}
fn akk_decline(noun: String, gram_case: String, number: String) -> String {
let fem: Bool = akk_is_fem(noun)
let stem: String = akk_strip_nom(noun)
if str_eq(number, "singular") {
if fem {
if str_eq(gram_case, "nom") { return stem + "tum" }
if str_eq(gram_case, "acc") { return stem + "tam" }
if str_eq(gram_case, "gen") { return stem + "tim" }
return stem + "tum"
}
// Masculine
if str_eq(gram_case, "nom") { return stem + "um" }
if str_eq(gram_case, "acc") { return stem + "am" }
if str_eq(gram_case, "gen") { return stem + "im" }
return stem + "um"
}
// Plural
if fem {
if str_eq(gram_case, "nom") { return stem + "ātum" }
// acc and gen merge in the oblique plural
return stem + "ātim"
}
// Masculine plural
if str_eq(gram_case, "nom") { return stem + "ūtum" }
return stem + "ātim"
}
// Noun phrase
//
// akk_noun_phrase: produce the surface noun phrase.
//
// Akkadian has no definite or indefinite article. Determination is conveyed
// by context, word order, and the genitive construct chain (status constructus).
// The definite parameter is accepted but has no surface effect: the declined
// noun is returned in either case.
//
// noun: dictionary form (nominative singular with mimation, e.g. "šarrum")
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural"
// definite: "true" | "false" (no surface effect in Akkadian)
fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return akk_decline(noun, gram_case, number)
}
// Canonical verb mapping
//
// akk_map_canonical: map cross-lingual English canonical verb labels to
// their Akkadian G-stem infinitive equivalents.
fn akk_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "bašû" }
if str_eq(verb, "go") { return "alāku" }
if str_eq(verb, "see") { return "amāru" }
if str_eq(verb, "say") { return "qabû" }
if str_eq(verb, "speak") { return "qabû" }
if str_eq(verb, "do") { return "epēšu" }
if str_eq(verb, "make") { return "epēšu" }
return verb
}
+31
View File
@@ -0,0 +1,31 @@
// auto-generated by elc --emit-header — do not edit
extern fn akk_str_ends(s: String, suf: String) -> Bool
extern fn akk_str_len(s: String) -> Int
extern fn akk_str_drop_last(s: String, n: Int) -> String
extern fn akk_slot(person: String, number: String) -> Int
extern fn akk_slot_g(person: String, gender: String, number: String) -> Int
extern fn akk_copula_present(slot: Int) -> String
extern fn akk_copula_stative(slot: Int) -> String
extern fn akk_is_copula(verb: String) -> Bool
extern fn akk_conjugate_copula(tense: String, slot: Int) -> String
extern fn akk_alaku_present(slot: Int) -> String
extern fn akk_alaku_perfect(slot: Int) -> String
extern fn akk_amaru_present(slot: Int) -> String
extern fn akk_amaru_perfect(slot: Int) -> String
extern fn akk_amaru_stative(slot: Int) -> String
extern fn akk_qabu_present(slot: Int) -> String
extern fn akk_qabu_perfect(slot: Int) -> String
extern fn akk_qabu_stative(slot: Int) -> String
extern fn akk_epesu_present(slot: Int) -> String
extern fn akk_epesu_perfect(slot: Int) -> String
extern fn akk_epesu_stative(slot: Int) -> String
extern fn akk_regular_present(stem: String, slot: Int) -> String
extern fn akk_regular_perfect(stem: String, slot: Int) -> String
extern fn akk_regular_stative(stem: String, slot: Int) -> String
extern fn akk_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn akk_strip_nom(noun: String) -> String
extern fn akk_is_fem(noun: String) -> Bool
extern fn akk_decline(noun: String, gram_case: String, number: String) -> String
extern fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn akk_map_canonical(verb: String) -> String
+752
View File
@@ -0,0 +1,752 @@
// morphology-ang.el - Old English (Anglo-Saxon) morphology for the NLG engine.
//
// Implements Old English verb conjugation, noun declension, and the definite
// article/demonstrative pronoun. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "ang".
//
// Language profile: code=ang, name=Old English, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=latin, family=germanic.
//
// Typology note: Old English is a synthetic Germanic language with four
// grammatical cases (nominative, accusative, genitive, dative), three genders,
// and strong/weak noun and verb classes. Strong verbs form their past tense by
// internal vowel change (ablaut); weak verbs use a dental (-de/-ode) suffix.
// Long vowels are marked with a macron (ā ē ī ō ū) and are preserved in all
// string literals; ǣ, æ, þ, ð, and ƿ (wynn) are used where historically
// appropriate. V2 (verb-second) word order applies in main clauses but is not
// enforced by this module the realizer handles constituent ordering.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third × singular/plural (slots 0-5)
// Classes: weak (regular -ian), strong irregular table
// Irregulars: wesan/beon (be), habban (have), gān (go), cuman (come),
// secgan (say), sēon (see), dōn (do), willan (want), magan (can)
// Canonical map: "be" -> "wesan" (past) / "beon" (present)
//
// Noun declension covered:
// Strong masc a-stem (cyning pattern): nom/acc -, gen -es, dat -e; pl -as/-a/-um
// Strong neut a-stem (word pattern): sg same as masc; pl nom/acc -∅
// Weak n-stem (nama pattern): sg nom -a, obl -an; pl -an/-ena/-um
//
// Article: simplified demonstrative/article forms for masculine, feminine,
// neuter (se/sēo/þæt), fully declined.
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn ang_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn ang_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn ang_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn ang_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index for paradigm tables.
// 0 = 1st singular (ic)
// 1 = 2nd singular (þū)
// 2 = 3rd singular (hē/hēo/hit)
// 3 = 1st plural ()
// 4 = 2nd plural ()
// 5 = 3rd plural (hīe)
//
// Old English also has a dual (wit, git) not handled; dual falls through
// to plural.
fn ang_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// The semantic layer may pass English canonical labels. Map to Old English
// citation (infinitive) forms. "be" maps to "beon" for present and "wesan"
// for past the caller selects tense, so we map "be" to "beon" and handle
// the past-tense wesan forms inside the conjugation function.
fn ang_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "beon" }
if str_eq(verb, "have") { return "habban" }
if str_eq(verb, "go") { return "gān" }
if str_eq(verb, "come") { return "cuman" }
if str_eq(verb, "say") { return "secgan" }
if str_eq(verb, "see") { return "sēon" }
if str_eq(verb, "do") { return "dōn" }
if str_eq(verb, "want") { return "willan" }
if str_eq(verb, "will") { return "willan" }
if str_eq(verb, "can") { return "magan" }
if str_eq(verb, "know") { return "witan" }
if str_eq(verb, "give") { return "giefan" }
if str_eq(verb, "take") { return "niman" }
if str_eq(verb, "find") { return "findan" }
if str_eq(verb, "make") { return "macian" }
return verb
}
// Irregular: wesan (to be past tense forms)
//
// Past: wæs wǣre wæs wǣron wǣron wǣron
fn ang_wesan_past(slot: Int) -> String {
if slot == 0 { return "wæs" }
if slot == 1 { return "wǣre" }
if slot == 2 { return "wæs" }
if slot == 3 { return "wǣron" }
if slot == 4 { return "wǣron" }
return "wǣron"
}
// Irregular: beon (to be present / habitual / future)
//
// Present: bēo bist biþ bēoþ bēoþ bēoþ
//
// The present indicative of "wesan" is eom/eart/is/sind that paradigm is
// also provided below for completeness and for callers who specifically request
// wesan present.
fn ang_beon_present(slot: Int) -> String {
if slot == 0 { return "bēo" }
if slot == 1 { return "bist" }
if slot == 2 { return "biþ" }
if slot == 3 { return "bēoþ" }
if slot == 4 { return "bēoþ" }
return "bēoþ"
}
// Irregular: wesan present (eom/eart/is/sind)
//
// Present: eom eart is sind/sindon sind sind
fn ang_wesan_present(slot: Int) -> String {
if slot == 0 { return "eom" }
if slot == 1 { return "eart" }
if slot == 2 { return "is" }
if slot == 3 { return "sind" }
if slot == 4 { return "sind" }
return "sind"
}
// Irregular: habban (to have)
//
// Present: hæbbe hæfst hæfþ habbað habbað habbað
// Past: hæfde hæfdest hæfde hæfdon hæfdon hæfdon
fn ang_habban_present(slot: Int) -> String {
if slot == 0 { return "hæbbe" }
if slot == 1 { return "hæfst" }
if slot == 2 { return "hæfþ" }
if slot == 3 { return "habbað" }
if slot == 4 { return "habbað" }
return "habbað"
}
fn ang_habban_past(slot: Int) -> String {
if slot == 0 { return "hæfde" }
if slot == 1 { return "hæfdest" }
if slot == 2 { return "hæfde" }
if slot == 3 { return "hæfdon" }
if slot == 4 { return "hæfdon" }
return "hæfdon"
}
// Irregular: gān (to go)
//
// Present: gǣst gǣþ gāð gāð gāð
// Past: ēode ēodest ēode ēodon ēodon ēodon
fn ang_gan_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "gǣst" }
if slot == 2 { return "gǣþ" }
if slot == 3 { return "gāð" }
if slot == 4 { return "gāð" }
return "gāð"
}
fn ang_gan_past(slot: Int) -> String {
if slot == 0 { return "ēode" }
if slot == 1 { return "ēodest" }
if slot == 2 { return "ēode" }
if slot == 3 { return "ēodon" }
if slot == 4 { return "ēodon" }
return "ēodon"
}
// Irregular: cuman (to come)
//
// Present: cume cymst cymþ cumað cumað cumað
// Past: cōm cōme cōm cōmon cōmon cōmon
fn ang_cuman_present(slot: Int) -> String {
if slot == 0 { return "cume" }
if slot == 1 { return "cymst" }
if slot == 2 { return "cymþ" }
if slot == 3 { return "cumað" }
if slot == 4 { return "cumað" }
return "cumað"
}
fn ang_cuman_past(slot: Int) -> String {
if slot == 0 { return "cōm" }
if slot == 1 { return "cōme" }
if slot == 2 { return "cōm" }
if slot == 3 { return "cōmon" }
if slot == 4 { return "cōmon" }
return "cōmon"
}
// Irregular: secgan (to say)
//
// Present: secge sagast sagað secgað secgað secgað
// Past: sægde sægdest sægde sægdon sægdon sægdon
fn ang_secgan_present(slot: Int) -> String {
if slot == 0 { return "secge" }
if slot == 1 { return "sagast" }
if slot == 2 { return "sagað" }
if slot == 3 { return "secgað" }
if slot == 4 { return "secgað" }
return "secgað"
}
fn ang_secgan_past(slot: Int) -> String {
if slot == 0 { return "sægde" }
if slot == 1 { return "sægdest" }
if slot == 2 { return "sægde" }
if slot == 3 { return "sægdon" }
if slot == 4 { return "sægdon" }
return "sægdon"
}
// Irregular: sēon (to see)
//
// Present: sēo siehst siehþ sēoð sēoð sēoð
// Past: seah sāwe seah sāwon sāwon sāwon
fn ang_seon_present(slot: Int) -> String {
if slot == 0 { return "sēo" }
if slot == 1 { return "siehst" }
if slot == 2 { return "siehþ" }
if slot == 3 { return "sēoð" }
if slot == 4 { return "sēoð" }
return "sēoð"
}
fn ang_seon_past(slot: Int) -> String {
if slot == 0 { return "seah" }
if slot == 1 { return "sāwe" }
if slot == 2 { return "seah" }
if slot == 3 { return "sāwon" }
if slot == 4 { return "sāwon" }
return "sāwon"
}
// Irregular: dōn (to do)
//
// Present: dēst dēþ dōð dōð dōð
// Past: dyde dydest dyde dydon dydon dydon
fn ang_don_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "dēst" }
if slot == 2 { return "dēþ" }
if slot == 3 { return "dōð" }
if slot == 4 { return "dōð" }
return "dōð"
}
fn ang_don_past(slot: Int) -> String {
if slot == 0 { return "dyde" }
if slot == 1 { return "dydest" }
if slot == 2 { return "dyde" }
if slot == 3 { return "dydon" }
if slot == 4 { return "dydon" }
return "dydon"
}
// Irregular: willan (to want / will)
//
// Present: wille wilt wile willað willað willað
// Past: wolde woldest wolde woldon woldon woldon
fn ang_willan_present(slot: Int) -> String {
if slot == 0 { return "wille" }
if slot == 1 { return "wilt" }
if slot == 2 { return "wile" }
if slot == 3 { return "willað" }
if slot == 4 { return "willað" }
return "willað"
}
fn ang_willan_past(slot: Int) -> String {
if slot == 0 { return "wolde" }
if slot == 1 { return "woldest" }
if slot == 2 { return "wolde" }
if slot == 3 { return "woldon" }
if slot == 4 { return "woldon" }
return "woldon"
}
// Irregular: magan (to be able / can)
//
// Present: mæg meaht mæg magon magon magon
// Past: meahte meahtest meahte meahton meahton meahton
fn ang_magan_present(slot: Int) -> String {
if slot == 0 { return "mæg" }
if slot == 1 { return "meaht" }
if slot == 2 { return "mæg" }
if slot == 3 { return "magon" }
if slot == 4 { return "magon" }
return "magon"
}
fn ang_magan_past(slot: Int) -> String {
if slot == 0 { return "meahte" }
if slot == 1 { return "meahtest" }
if slot == 2 { return "meahte" }
if slot == 3 { return "meahton" }
if slot == 4 { return "meahton" }
return "meahton"
}
// Irregular: witan (to know)
//
// Present: wāt wāst wāt witon witon witon
// Past: wisse/wiste wissest wisse wisson wisson wisson
fn ang_witan_present(slot: Int) -> String {
if slot == 0 { return "wāt" }
if slot == 1 { return "wāst" }
if slot == 2 { return "wāt" }
if slot == 3 { return "witon" }
if slot == 4 { return "witon" }
return "witon"
}
fn ang_witan_past(slot: Int) -> String {
if slot == 0 { return "wisse" }
if slot == 1 { return "wissest" }
if slot == 2 { return "wisse" }
if slot == 3 { return "wisson" }
if slot == 4 { return "wisson" }
return "wisson"
}
// Weak verb: present-tense endings
//
// Weak verbs with -ian infinitives form their present tense as:
// stem + -e, -est, -eþ, -aþ, -aþ, -aþ
//
// The stem is the infinitive with -ian stripped (or -an for class-2 verbs).
fn ang_weak_present_ending(slot: Int) -> String {
if slot == 0 { return "e" }
if slot == 1 { return "est" }
if slot == 2 { return "" }
if slot == 3 { return "" }
if slot == 4 { return "" }
return ""
}
// Weak verb: past-tense ending selection
//
// Class 1 (-ian with short stem): past -ede (e.g. nerian -> nerede)
// Class 2 (-ian with long/heavy stem): past -ode (e.g. macian -> macode)
// Class 3 (-ian, small group): past -de (e.g. habban -> hæfde irregular)
//
// Heuristic: if the stem length is 1 char, use -ede; otherwise use -ode.
// This is a simplification; correct assignment requires lexical class marking.
//
// For the past, all persons in the plural share -on, and all singulars share
// the same dental-suffixed stem.
fn ang_weak_past_stem(stem: String) -> String {
let slen: Int = str_len(stem)
if slen <= 2 {
return stem + "ede"
}
return stem + "ode"
}
fn ang_weak_past(stem: String, slot: Int) -> String {
let pstem: String = ang_weak_past_stem(stem)
if slot == 0 { return pstem }
if slot == 1 { return pstem + "st" }
if slot == 2 { return pstem }
if slot == 3 { return ang_str_drop_last(pstem, 1) + "on" }
if slot == 4 { return ang_str_drop_last(pstem, 1) + "on" }
return ang_str_drop_last(pstem, 1) + "on"
}
// Stem extraction for weak verbs
//
// Strip the infinitive ending to recover the stem:
// -ian -> strip 3 chars (nerian -> ner-, macian -> mac-)
// -an -> strip 2 chars (habban -> habb-; fallback for non -ian)
// otherwise: return as-is
fn ang_weak_stem(verb: String) -> String {
if ang_str_ends(verb, "ian") {
return ang_str_drop_last(verb, 3)
}
if ang_str_ends(verb, "an") {
return ang_str_drop_last(verb, 2)
}
return verb
}
// ang_conjugate: main conjugation entry point
//
// verb: Old English infinitive or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Strategy:
// 1. Map canonical English labels to OE verbs.
// 2. Check the full irregular table.
// 3. Fall back to weak conjugation for unknown -ian/-an verbs.
// 4. Return the base form if nothing matches.
fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = ang_map_canonical(verb)
let slot: Int = ang_slot(person, number)
// Irregulars
// beon: present-tense "be" (habitual/future/general)
if str_eq(v, "beon") {
if str_eq(tense, "present") { return ang_beon_present(slot) }
// past: use wesan past forms
return ang_wesan_past(slot)
}
// wesan: past "be" and present "be" (existential/stative)
if str_eq(v, "wesan") {
if str_eq(tense, "present") { return ang_wesan_present(slot) }
return ang_wesan_past(slot)
}
if str_eq(v, "habban") {
if str_eq(tense, "present") { return ang_habban_present(slot) }
return ang_habban_past(slot)
}
if str_eq(v, "gān") {
if str_eq(tense, "present") { return ang_gan_present(slot) }
return ang_gan_past(slot)
}
if str_eq(v, "cuman") {
if str_eq(tense, "present") { return ang_cuman_present(slot) }
return ang_cuman_past(slot)
}
if str_eq(v, "secgan") {
if str_eq(tense, "present") { return ang_secgan_present(slot) }
return ang_secgan_past(slot)
}
if str_eq(v, "sēon") {
if str_eq(tense, "present") { return ang_seon_present(slot) }
return ang_seon_past(slot)
}
if str_eq(v, "dōn") {
if str_eq(tense, "present") { return ang_don_present(slot) }
return ang_don_past(slot)
}
if str_eq(v, "willan") {
if str_eq(tense, "present") { return ang_willan_present(slot) }
return ang_willan_past(slot)
}
if str_eq(v, "magan") {
if str_eq(tense, "present") { return ang_magan_present(slot) }
return ang_magan_past(slot)
}
if str_eq(v, "witan") {
if str_eq(tense, "present") { return ang_witan_present(slot) }
return ang_witan_past(slot)
}
// Regular weak conjugation
let stem: String = ang_weak_stem(v)
if str_eq(tense, "present") {
return stem + ang_weak_present_ending(slot)
}
if str_eq(tense, "past") {
return ang_weak_past(stem, slot)
}
// Unknown tense: return infinitive
return v
}
// Noun declension class detection
//
// Infer the declension class from the nominative singular form and an optional
// gender hint. Without a full lexicon, ending-based heuristics are used:
//
// ends in -a -> weak n-stem (nama pattern)
// ends in -e (long) -> may be various; default to strong masc a-stem
// any other ending -> strong a-stem; gender distinguishes masc vs neut
//
// The caller may pass gender as a hint:
// "masculine" | "feminine" | "neuter" | "" (empty = infer)
//
// For simplicity this module handles three paradigms:
// "strong_masc" a-stem masculine (cyning, mann)
// "strong_neut" a-stem neuter (word, scip)
// "weak" n-stem (nama, ēage)
fn ang_declension(noun: String, gender: String) -> String {
if ang_str_ends(noun, "a") { return "weak" }
if str_eq(gender, "neuter") { return "strong_neut" }
return "strong_masc"
}
// Strong masculine a-stem (cyning pattern)
//
// Stem: the noun as given (nom sg lacks an inflectional ending in this class).
//
// Singular: nom - acc - gen -es dat -e
// Plural: nom -as acc -as gen -a dat -um
fn ang_decline_strong_masc(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "es" }
if str_eq(gram_case, "dative") { return noun + "e" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return noun + "as" }
if str_eq(gram_case, "accusative") { return noun + "as" }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun + "as"
}
// Strong neuter a-stem (word pattern)
//
// Singular: same as strong masc
// Plural: nom/acc - gen -a dat -um
fn ang_decline_strong_neut(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "es" }
if str_eq(gram_case, "dative") { return noun + "e" }
return noun
}
// plural: neuters have zero ending in nom/acc
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun
}
// Weak n-stem (nama pattern)
//
// The nom sg ends in -a; the oblique stem is formed by stripping -a and adding
// -an. Plural genitive is -ena.
//
// Singular: nom -a acc -an gen -an dat -an
// Plural: nom -an acc -an gen -ena dat -um
fn ang_decline_weak(noun: String, gram_case: String, number: String) -> String {
// Oblique stem: strip the final -a
let stem: String = ang_str_drop_last(noun, 1)
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "an" }
if str_eq(gram_case, "dative") { return stem + "an" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return stem + "an" }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "ena" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "an"
}
// ang_decline: main declension entry point
//
// noun: nominative singular Old English noun (e.g. "cyning", "word", "nama")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// gender: "masculine" | "neuter" | "feminine" | "" (empty triggers inference)
//
// Returns the inflected form. Falls back to the nominative singular for any
// unrecognised combination.
fn ang_decline(noun: String, gram_case: String, number: String, gender: String) -> String {
let decl: String = ang_declension(noun, gender)
if str_eq(decl, "strong_masc") {
return ang_decline_strong_masc(noun, gram_case, number)
}
if str_eq(decl, "strong_neut") {
return ang_decline_strong_neut(noun, gram_case, number)
}
if str_eq(decl, "weak") {
return ang_decline_weak(noun, gram_case, number)
}
// Unknown: return nominative unchanged
return noun
}
// Definite article / demonstrative: se/sēo/þæt
//
// Old English used the demonstrative pronoun se/sēo/þæt as a definite article.
// The full paradigm (gender × case × number) is given below.
//
// Masculine:
// sg: nom se acc þone gen þæs dat þǣm
// pl: nom þā acc þā gen þāra dat þǣm
//
// Feminine:
// sg: nom sēo acc þā gen þǣre dat þǣre
// pl: nom þā acc þā gen þāra dat þǣm
//
// Neuter:
// sg: nom þæt acc þæt gen þæs dat þǣm
// pl: nom þā acc þā gen þāra dat þǣm
fn ang_article_masculine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "se" }
if str_eq(gram_case, "accusative") { return "þone" }
if str_eq(gram_case, "genitive") { return "þæs" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "se"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article_feminine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "sēo" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þǣre" }
if str_eq(gram_case, "dative") { return "þǣre" }
return "sēo"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article_neuter(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "þæt" }
if str_eq(gram_case, "accusative") { return "þæt" }
if str_eq(gram_case, "genitive") { return "þæs" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þæt"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article(gender: String, gram_case: String, number: String) -> String {
if str_eq(gender, "masculine") { return ang_article_masculine(gram_case, number) }
if str_eq(gender, "feminine") { return ang_article_feminine(gram_case, number) }
// neuter
return ang_article_neuter(gram_case, number)
}
// Gender inference from noun form
//
// A last-resort heuristic when the caller provides no gender hint.
// -a ending strongly suggests weak masculine or neuter (but most -a nouns are
// masculine weak). Without a full lexicon, masculine is the safe default.
fn ang_infer_gender(noun: String) -> String {
if ang_str_ends(noun, "u") { return "feminine" }
if ang_str_ends(noun, "e") { return "feminine" }
return "masculine"
}
// ang_noun_phrase: noun phrase builder
//
// Produces a declined noun with optional definite article (demonstrative)
// prepended. When gender is empty ("") it is inferred from the noun form.
//
// noun: nominative singular Old English noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false"
fn ang_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let gender: String = ang_infer_gender(noun)
let declined: String = ang_decline(noun, gram_case, number, gender)
if str_eq(definite, "true") {
let art: String = ang_article(gender, gram_case, number)
return art + " " + declined
}
return declined
}
+44
View File
@@ -0,0 +1,44 @@
// auto-generated by elc --emit-header — do not edit
extern fn ang_str_ends(s: String, suf: String) -> Bool
extern fn ang_str_drop_last(s: String, n: Int) -> String
extern fn ang_str_last_char(s: String) -> String
extern fn ang_str_last2(s: String) -> String
extern fn ang_slot(person: String, number: String) -> Int
extern fn ang_map_canonical(verb: String) -> String
extern fn ang_wesan_past(slot: Int) -> String
extern fn ang_beon_present(slot: Int) -> String
extern fn ang_wesan_present(slot: Int) -> String
extern fn ang_habban_present(slot: Int) -> String
extern fn ang_habban_past(slot: Int) -> String
extern fn ang_gan_present(slot: Int) -> String
extern fn ang_gan_past(slot: Int) -> String
extern fn ang_cuman_present(slot: Int) -> String
extern fn ang_cuman_past(slot: Int) -> String
extern fn ang_secgan_present(slot: Int) -> String
extern fn ang_secgan_past(slot: Int) -> String
extern fn ang_seon_present(slot: Int) -> String
extern fn ang_seon_past(slot: Int) -> String
extern fn ang_don_present(slot: Int) -> String
extern fn ang_don_past(slot: Int) -> String
extern fn ang_willan_present(slot: Int) -> String
extern fn ang_willan_past(slot: Int) -> String
extern fn ang_magan_present(slot: Int) -> String
extern fn ang_magan_past(slot: Int) -> String
extern fn ang_witan_present(slot: Int) -> String
extern fn ang_witan_past(slot: Int) -> String
extern fn ang_weak_present_ending(slot: Int) -> String
extern fn ang_weak_past_stem(stem: String) -> String
extern fn ang_weak_past(stem: String, slot: Int) -> String
extern fn ang_weak_stem(verb: String) -> String
extern fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ang_declension(noun: String, gender: String) -> String
extern fn ang_decline_strong_masc(noun: String, gram_case: String, number: String) -> String
extern fn ang_decline_strong_neut(noun: String, gram_case: String, number: String) -> String
extern fn ang_decline_weak(noun: String, gram_case: String, number: String) -> String
extern fn ang_decline(noun: String, gram_case: String, number: String, gender: String) -> String
extern fn ang_article_masculine(gram_case: String, number: String) -> String
extern fn ang_article_feminine(gram_case: String, number: String) -> String
extern fn ang_article_neuter(gram_case: String, number: String) -> String
extern fn ang_article(gender: String, gram_case: String, number: String) -> String
extern fn ang_infer_gender(noun: String) -> String
extern fn ang_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+729
View File
@@ -0,0 +1,729 @@
// morphology-ar.el - Arabic morphology for the NLG engine.
//
// Implements Arabic verb conjugation, noun inflection (gram_case, gender, number,
// definiteness), and definite-article attachment with sun/moon letter handling.
//
// Arabic is a Semitic language with a trilateral root system: most words derive
// from 3-consonant roots by inserting vowel patterns (أوزان awzan) around the
// root consonants. Verb conjugation is realised as prefix + stem + suffix.
//
// Strategy: the engine takes the 3ms perfect (past tense) form as the canonical
// dictionary key (e.g. كَتَبَ kataba) and applies affix patterns to derive all
// other conjugated forms for Form I (الفعل المجرد) regular verbs. A lookup
// table covers essential irregular and hollow verbs.
//
// Verb tenses covered: "past" (perfect/الماضي), "present" (imperfect/المضارع),
// "future" (سَيَفْعَلُ = sa- + imperfect).
// Persons: first/second/third × masculine/feminine × singular/plural (+ dual stubs).
// Gender params: "m" (masculine) | "f" (feminine).
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq, str_drop_last concept)
// String helpers
import "morphology.el"
fn ar_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn ar_str_len(s: String) -> Int {
return str_len(s)
}
fn ar_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn ar_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Slot index
//
// Maps person × gender × number to a 0-based slot for table lookups.
// Slot layout (10 cells, matching classical Arabic conjugation paradigm):
// 0 = 3ms (he)
// 1 = 3fs (she)
// 2 = 2ms (you m sg)
// 3 = 2fs (you f sg)
// 4 = 1s (I)
// 5 = 3mp (they m pl)
// 6 = 3fp (they f pl)
// 7 = 2mp (you m pl)
// 8 = 2fp (you f pl)
// 9 = 1p (we)
fn ar_slot(person: String, gender: String, number: String) -> Int {
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 1 }
return 0
}
// plural
if str_eq(gender, "f") { return 6 }
return 5
}
if str_eq(person, "second") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
return 2
}
// plural
if str_eq(gender, "f") { return 8 }
return 7
}
// first
if str_eq(number, "plural") { return 9 }
return 4
}
// Perfect (past) suffixes
//
// Form I perfect: root-past-stem (e.g. كَتَبَ kataba) + suffix.
// The 3ms form IS the base (no suffix added). All other persons add a suffix
// that replaces or follows the final short vowel of the base.
//
// Pattern (dropping the final -a of the 3ms base, then adding):
// 3ms: -a (base as given)
// 3fs: -at
// 2ms: -ta
// 2fs: -ti
// 1s: -tu
// 3mp: -uu
// 3fp: -na
// 2mp: -tum
// 2fp: -tunna
// 1p: -naa
//
// The base passed to ar_conjugate_form1 is the full 3ms form (ends in -a).
// For suffixed forms we drop the final vowel character (1 byte = the -a) then
// apply the suffix. In Arabic script the final short vowel (fatha ـَ) on the
// last consonant of the base is part of the grapheme cluster of that consonant;
// for our stored strings the form كَتَبَ is stored with the final fatha attached
// to the ب. The suffix strings already include the vowel that replaces it, so
// we drop 1 character from the base.
//
// For simplicity the suffixes below are given as Arabic transliteration that
// the El string system handles as UTF-8. The actual Arabic forms are stored
// as UTF-8 Arabic script literals.
//
// Returns the suffix string (including the vowel carried on the junction
// consonant for suffixed forms). Returns "" for 3ms (base is the full form).
fn ar_perfect_suffix(slot: Int) -> String {
if slot == 0 { return "" } // 3ms: base is already complete
if slot == 1 { return "ت" } // 3fs: -at (تْ taa saakina)
if slot == 2 { return "تَ" } // 2ms: -ta
if slot == 3 { return "تِ" } // 2fs: -ti
if slot == 4 { return "تُ" } // 1s: -tu
if slot == 5 { return "وا" } // 3mp: -uu (واو + alif farika)
if slot == 6 { return "نَ" } // 3fp: -na
if slot == 7 { return "تُمْ" } // 2mp: -tum
if slot == 8 { return "تُنَّ" } // 2fp: -tunna
return "نَا" // 1p: -naa (9)
}
// Imperfect (present) prefixes
//
// Form I imperfect: prefix + middle vowel pattern + suffix.
// Prefix depends on person (and for 1s the prefix is أَ).
fn ar_imperfect_prefix(slot: Int) -> String {
if slot == 0 { return "يَ" } // 3ms: ya-
if slot == 1 { return "تَ" } // 3fs: ta-
if slot == 2 { return "تَ" } // 2ms: ta-
if slot == 3 { return "تَ" } // 2fs: ta-
if slot == 4 { return "أَ" } // 1s: a-
if slot == 5 { return "يَ" } // 3mp: ya-
if slot == 6 { return "يَ" } // 3fp: ya-
if slot == 7 { return "تَ" } // 2mp: ta-
if slot == 8 { return "تَ" } // 2fp: ta-
return "نَ" // 1p: na- (9)
}
// Imperfect (present) suffixes
//
// Standard Form I imperfect yaf'ulu / yaf'alu / yaf'ilu vowel class.
// The stem vowel is encoded in the verb's imperfect stem (stored in the lookup
// table or derived from the base). The suffix encodes number/gender/person.
//
// Suffix pattern (after the u-class stem: yaktubu):
// 3ms: -u (yaktub-u)
// 3fs: -u (taktub-u)
// 2ms: -u (taktub-u)
// 2fs: -iina (taktub-iina)
// 1s: -u (aktub-u)
// 3mp: -uuna (yaktub-uuna)
// 3fp: -na (yaktub-na)
// 2mp: -uuna (taktub-uuna)
// 2fp: -na (taktub-na)
// 1p: -u (naktub-u)
fn ar_imperfect_suffix(slot: Int) -> String {
if slot == 0 { return "ُ" } // 3ms: -u
if slot == 1 { return "ُ" } // 3fs: -u
if slot == 2 { return "ُ" } // 2ms: -u
if slot == 3 { return "ِينَ" } // 2fs: -iina
if slot == 4 { return "ُ" } // 1s: -u
if slot == 5 { return "ُونَ" } // 3mp: -uuna
if slot == 6 { return "نَ" } // 3fp: -na
if slot == 7 { return "ُونَ" } // 2mp: -uuna
if slot == 8 { return "نَ" } // 2fp: -na
return "ُ" // 1p: -u (9)
}
// Form I conjugation
//
// ar_conjugate_form1: conjugate a regular Form I verb.
//
// past_base: the 3ms perfect form (e.g. "كَتَبَ")
// present_stem: the imperfect stem without prefix (e.g. "كْتُبُ" for yaktubu)
// This is the middle part after stripping the prefix: for يَكْتُبُ
// the stem = "كْتُبُ". We strip the final -u vowel diacritic
// (1 char) from the stem and re-add via the suffix.
// tense: "past" | "present" | "future"
// slot: ar_slot result
fn ar_conjugate_form1(past_base: String, present_stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "past") {
// 3ms: return base as-is
if slot == 0 { return past_base }
// All other forms: drop final character of base (the short -a vowel mark
// on the last root consonant), then append the suffix.
let suf: String = ar_perfect_suffix(slot)
// Drop the last character (the fatha diacritic or final vowel-letter)
let stem: String = ar_str_drop_last(past_base, 1)
return stem + suf
}
if str_eq(tense, "present") {
let pre: String = ar_imperfect_prefix(slot)
let suf: String = ar_imperfect_suffix(slot)
// present_stem already includes the medial vowel pattern (e.g. "كْتُبُ")
// Drop its final character (the -u diacritic) before adding the suffix.
let mid: String = ar_str_drop_last(present_stem, 1)
return pre + mid + suf
}
if str_eq(tense, "future") {
// Future = سَ (sa-) + imperfect 3ms form
let pres_3ms: String = ar_conjugate_form1(past_base, present_stem, "present", 0)
return "سَ" + pres_3ms
}
// Unknown tense: return base form
return past_base
}
// Irregular verb lookup table
//
// Returns the inflected form for verbs that cannot be derived by Form I rules,
// or "" if the verb is not in the table.
//
// Covered verbs (by their 3ms past / dictionary key):
// كَانَ kaana to be (hollow verb, waw-medial)
// ذَهَبَ dhahaba to go (Form I, regular; explicit table for certainty)
// جَاءَ jaa'a to come (hamzated + defective)
// قَالَ qaala to say (hollow verb, waw-medial)
// رَأَى ra'aa to see (hamzated + defective)
// أَكَلَ akala to eat (hamzated initial)
// شَرِبَ shariba to drink (Form I i-class)
// عَرَفَ arafa to know (Form I a-class)
// أَرَادَ araada to want (Form IV hollow)
// اِسْتَطَاعَ istata'a can/be able (Form X)
// فَعَلَ fa'ala to do/act (Form I; paradigm verb)
// أَخَذَ akhadha to take (hamzated initial)
// عَمِلَ amila to work (Form I i-class)
//
// For each verb: [past_3ms, past_3fs, past_2ms, past_2fs, past_1s,
// past_3mp, past_3fp, past_2mp, past_2fp, past_1p,
// pres_3ms, pres_3fs, pres_2ms, pres_2fs, pres_1s,
// pres_3mp, pres_3fp, pres_2mp, pres_2fp, pres_1p]
fn ar_irregular_kaana(slot: Int, tense: String) -> String {
// كَانَ to be
if str_eq(tense, "past") {
if slot == 0 { return "كَانَ" }
if slot == 1 { return "كَانَتْ" }
if slot == 2 { return "كُنْتَ" }
if slot == 3 { return "كُنْتِ" }
if slot == 4 { return "كُنْتُ" }
if slot == 5 { return "كَانُوا" }
if slot == 6 { return "كُنَّ" }
if slot == 7 { return "كُنْتُمْ" }
if slot == 8 { return "كُنْتُنَّ" }
return "كُنَّا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يَكُونُ" }
if slot == 1 { return "تَكُونُ" }
if slot == 2 { return "تَكُونُ" }
if slot == 3 { return "تَكُونِينَ" }
if slot == 4 { return "أَكُونُ" }
if slot == 5 { return "يَكُونُونَ" }
if slot == 6 { return "يَكُنَّ" }
if slot == 7 { return "تَكُونُونَ" }
if slot == 8 { return "تَكُنَّ" }
return "نَكُونُ"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_kaana(slot, "present")
return "سَ" + pres
}
return "كَانَ"
}
fn ar_irregular_qaala(slot: Int, tense: String) -> String {
// قَالَ to say (hollow waw-medial)
if str_eq(tense, "past") {
if slot == 0 { return "قَالَ" }
if slot == 1 { return "قَالَتْ" }
if slot == 2 { return "قُلْتَ" }
if slot == 3 { return "قُلْتِ" }
if slot == 4 { return "قُلْتُ" }
if slot == 5 { return "قَالُوا" }
if slot == 6 { return "قُلْنَ" }
if slot == 7 { return "قُلْتُمْ" }
if slot == 8 { return "قُلْتُنَّ" }
return "قُلْنَا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يَقُولُ" }
if slot == 1 { return "تَقُولُ" }
if slot == 2 { return "تَقُولُ" }
if slot == 3 { return "تَقُولِينَ" }
if slot == 4 { return "أَقُولُ" }
if slot == 5 { return "يَقُولُونَ" }
if slot == 6 { return "يَقُلْنَ" }
if slot == 7 { return "تَقُولُونَ" }
if slot == 8 { return "تَقُلْنَ" }
return "نَقُولُ"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_qaala(slot, "present")
return "سَ" + pres
}
return "قَالَ"
}
fn ar_irregular_jaa(slot: Int, tense: String) -> String {
// جَاءَ to come (hamzated defective)
if str_eq(tense, "past") {
if slot == 0 { return "جَاءَ" }
if slot == 1 { return "جَاءَتْ" }
if slot == 2 { return "جِئْتَ" }
if slot == 3 { return "جِئْتِ" }
if slot == 4 { return "جِئْتُ" }
if slot == 5 { return "جَاءُوا" }
if slot == 6 { return "جِئْنَ" }
if slot == 7 { return "جِئْتُمْ" }
if slot == 8 { return "جِئْتُنَّ" }
return "جِئْنَا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يَجِيءُ" }
if slot == 1 { return "تَجِيءُ" }
if slot == 2 { return "تَجِيءُ" }
if slot == 3 { return "تَجِيئِينَ" }
if slot == 4 { return "أَجِيءُ" }
if slot == 5 { return "يَجِيئُونَ" }
if slot == 6 { return "يَجِئْنَ" }
if slot == 7 { return "تَجِيئُونَ" }
if slot == 8 { return "تَجِئْنَ" }
return "نَجِيءُ"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_jaa(slot, "present")
return "سَ" + pres
}
return "جَاءَ"
}
fn ar_irregular_raaa(slot: Int, tense: String) -> String {
// رَأَى to see (hamzated defective)
if str_eq(tense, "past") {
if slot == 0 { return "رَأَى" }
if slot == 1 { return "رَأَتْ" }
if slot == 2 { return "رَأَيْتَ" }
if slot == 3 { return "رَأَيْتِ" }
if slot == 4 { return "رَأَيْتُ" }
if slot == 5 { return "رَأَوْا" }
if slot == 6 { return "رَأَيْنَ" }
if slot == 7 { return "رَأَيْتُمْ" }
if slot == 8 { return "رَأَيْتُنَّ" }
return "رَأَيْنَا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يَرَى" }
if slot == 1 { return "تَرَى" }
if slot == 2 { return "تَرَى" }
if slot == 3 { return "تَرَيْنَ" }
if slot == 4 { return "أَرَى" }
if slot == 5 { return "يَرَوْنَ" }
if slot == 6 { return "يَرَيْنَ" }
if slot == 7 { return "تَرَوْنَ" }
if slot == 8 { return "تَرَيْنَ" }
return "نَرَى"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_raaa(slot, "present")
return "سَ" + pres
}
return "رَأَى"
}
fn ar_irregular_araada(slot: Int, tense: String) -> String {
// أَرَادَ to want (Form IV hollow)
if str_eq(tense, "past") {
if slot == 0 { return "أَرَادَ" }
if slot == 1 { return "أَرَادَتْ" }
if slot == 2 { return "أَرَدْتَ" }
if slot == 3 { return "أَرَدْتِ" }
if slot == 4 { return "أَرَدْتُ" }
if slot == 5 { return "أَرَادُوا" }
if slot == 6 { return "أَرَدْنَ" }
if slot == 7 { return "أَرَدْتُمْ" }
if slot == 8 { return "أَرَدْتُنَّ" }
return "أَرَدْنَا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يُرِيدُ" }
if slot == 1 { return "تُرِيدُ" }
if slot == 2 { return "تُرِيدُ" }
if slot == 3 { return "تُرِيدِينَ" }
if slot == 4 { return "أُرِيدُ" }
if slot == 5 { return "يُرِيدُونَ" }
if slot == 6 { return "يُرِدْنَ" }
if slot == 7 { return "تُرِيدُونَ" }
if slot == 8 { return "تُرِدْنَ" }
return "نُرِيدُ"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_araada(slot, "present")
return "سَ" + pres
}
return "أَرَادَ"
}
fn ar_irregular_istata(slot: Int, tense: String) -> String {
// اِسْتَطَاعَ can / be able (Form X hollow)
if str_eq(tense, "past") {
if slot == 0 { return "اِسْتَطَاعَ" }
if slot == 1 { return "اِسْتَطَاعَتْ" }
if slot == 2 { return "اِسْتَطَعْتَ" }
if slot == 3 { return "اِسْتَطَعْتِ" }
if slot == 4 { return "اِسْتَطَعْتُ" }
if slot == 5 { return "اِسْتَطَاعُوا" }
if slot == 6 { return "اِسْتَطَعْنَ" }
if slot == 7 { return "اِسْتَطَعْتُمْ" }
if slot == 8 { return "اِسْتَطَعْتُنَّ" }
return "اِسْتَطَعْنَا"
}
if str_eq(tense, "present") {
if slot == 0 { return "يَسْتَطِيعُ" }
if slot == 1 { return "تَسْتَطِيعُ" }
if slot == 2 { return "تَسْتَطِيعُ" }
if slot == 3 { return "تَسْتَطِيعِينَ" }
if slot == 4 { return "أَسْتَطِيعُ" }
if slot == 5 { return "يَسْتَطِيعُونَ" }
if slot == 6 { return "يَسْتَطِعْنَ" }
if slot == 7 { return "تَسْتَطِيعُونَ" }
if slot == 8 { return "تَسْتَطِعْنَ" }
return "نَسْتَطِيعُ"
}
if str_eq(tense, "future") {
let pres: String = ar_irregular_istata(slot, "present")
return "سَ" + pres
}
return "اِسْتَطَاعَ"
}
// Irregular verb dispatcher
//
// ar_irregular: returns the inflected form if verb is in the lookup table,
// or "" if not found (caller should use Form I rules).
//
// verb: 3ms past form (dictionary key) as Arabic string
// tense: "past" | "present" | "future"
// slot: ar_slot result
fn ar_irregular(verb: String, tense: String, slot: Int) -> String {
if str_eq(verb, "كَانَ") { return ar_irregular_kaana(slot, tense) }
if str_eq(verb, "قَالَ") { return ar_irregular_qaala(slot, tense) }
if str_eq(verb, "جَاءَ") { return ar_irregular_jaa(slot, tense) }
if str_eq(verb, "رَأَى") { return ar_irregular_raaa(slot, tense) }
if str_eq(verb, "أَرَادَ") { return ar_irregular_araada(slot, tense) }
if str_eq(verb, "اِسْتَطَاعَ") { return ar_irregular_istata(slot, tense) }
return ""
}
// Regular Form I verb table
//
// For regular Form I verbs that would be correctly generated by ar_conjugate_form1
// but whose imperfect stem must be looked up (Arabic verbs have three vowel
// classes for the imperfect medial vowel: a, i, u فَعَلَ/يَفْعَلُ,
// فَعِلَ/يَفْعَلُ, فَعَلَ/يَفْعُلُ). We store the present stem for each.
//
// Returns present_stem (the imperfect without prefix, e.g. "كْتُبُ" for yaktubu),
// or "" if not in table.
fn ar_present_stem(verb: String) -> String {
if str_eq(verb, "كَتَبَ") { return "كْتُبُ" } // kataba -> yaktubu (u-class)
if str_eq(verb, "ذَهَبَ") { return "ذْهَبُ" } // dhahaba -> yadhhabu (a-class)
if str_eq(verb, "أَكَلَ") { return "أْكُلُ" } // akala -> yaakulu (u-class)
if str_eq(verb, "شَرِبَ") { return "شْرَبُ" } // shariba -> yashrabu (a-class)
if str_eq(verb, "عَرَفَ") { return "عْرِفُ" } // arafa -> yarifu (i-class)
if str_eq(verb, "فَعَلَ") { return "فْعَلُ" } // fa'ala -> yaf'alu (a-class)
if str_eq(verb, "أَخَذَ") { return "أْخُذُ" } // akhadha -> yaakhudhu (u-class)
if str_eq(verb, "عَمِلَ") { return "عْمَلُ" } // amila -> ya'malu (a-class)
if str_eq(verb, "دَرَسَ") { return "دْرُسُ" } // darasa -> yadrusu (u-class)
if str_eq(verb, "فَهِمَ") { return "فْهَمُ" } // fahima -> yafhamu (a-class)
if str_eq(verb, "سَمِعَ") { return "سْمَعُ" } // sami'a -> yasma'u (a-class)
if str_eq(verb, "جَلَسَ") { return "جْلِسُ" } // jalasa -> yajlisu (i-class)
if str_eq(verb, "فَتَحَ") { return "فْتَحُ" } // fataha -> yaftahu (a-class)
if str_eq(verb, "خَرَجَ") { return "خْرُجُ" } // kharaja -> yakhruju (u-class)
if str_eq(verb, "دَخَلَ") { return "دْخُلُ" } // dakhala -> yadkhulu (u-class)
if str_eq(verb, "وَجَدَ") { return "جِدُ" } // wajada -> yajidu (i-class, waw-initial)
if str_eq(verb, "صَنَعَ") { return "صْنَعُ" } // sana'a -> yasna'u (a-class)
if str_eq(verb, "رَجَعَ") { return "رْجِعُ" } // raja'a -> yarji'u (i-class)
if str_eq(verb, "وَقَفَ") { return "قِفُ" } // waqafa -> yaqifu (i-class, waw-initial)
if str_eq(verb, "قَرَأَ") { return "قْرَأُ" } // qara'a -> yaqra'u (a-class)
if str_eq(verb, "كَذَبَ") { return "كْذِبُ" } // kadhaba -> yakdhibu (i-class)
return ""
}
// Main conjugation dispatcher
//
// ar_conjugate: conjugate an Arabic verb.
//
// verb: 3ms perfect form (dictionary key), e.g. "كَتَبَ"
// tense: "past" | "present" | "future"
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String {
let slot: Int = ar_slot(person, gender, number)
// 1. Check irregular table
let irreg: String = ar_irregular(verb, tense, slot)
if !str_eq(irreg, "") {
return irreg
}
// 2. Look up present stem for regular Form I
let present_stem: String = ar_present_stem(verb)
if !str_eq(present_stem, "") {
return ar_conjugate_form1(verb, present_stem, tense, slot)
}
// 3. Fallback: return base form (3ms past) unknown verb
return verb
}
// Definite article
//
// ar_definite_article: prefix ال (al-) to a noun with sun/moon letter handling.
//
// Sun letters (الحروف الشمسية) cause the lam of the article to assimilate to
// the first letter of the noun. Moon letters (الحروف القمرية) do not.
//
// Sun letters (Unicode Arabic code points):
// ت ث د ذ ر ز س ش ص ض ط ظ ل ن
//
// Moon letters (all others):
// أ ب ج ح خ ع غ ف ق ك م ه و ي
//
// In Arabic orthography the assimilation is shown with a shadda on the sun letter.
// Here we return "ال" (al-) for moon letters and the assimilated form for sun
// letters. The noun is prefixed with the article; the article lam is replaced
// by a shadda on the sun consonant.
fn ar_is_sun_letter(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, "ذ") { return true }
if str_eq(c, "ر") { return true }
if str_eq(c, "ز") { return true }
if str_eq(c, "س") { return true }
if str_eq(c, "ش") { return true }
if str_eq(c, "ص") { return true }
if str_eq(c, "ض") { return true }
if str_eq(c, "ط") { return true }
if str_eq(c, "ظ") { return true }
if str_eq(c, "ل") { return true }
if str_eq(c, "ن") { return true }
return false
}
fn ar_definite_article(noun: String) -> String {
// Extract first character to determine sun/moon
let n: Int = ar_str_len(noun)
if n == 0 {
return noun
}
let first: String = str_slice(noun, 0, 1)
if ar_is_sun_letter(first) {
// Sun letter: article lam assimilates -> الـ + shadda on first letter
// Written as: أَلْ + first + shadda + rest
// We represent this as "ال" + first_with_shadda + rest_of_noun
// The shadda diacritic (U+0651) attaches to the sun letter.
let shadda: String = "ّ"
let rest: String = str_slice(noun, 1, n)
return "ال" + first + shadda + rest
}
// Moon letter: simple al- prefix
return "ال" + noun
}
// Case endings
//
// ar_case_ending: return the short vowel ending for a noun given its gram_case
// and definiteness.
//
// case: "nom" | "acc" | "gen"
// definite: "true" | "false"
//
// Indefinite endings carry nunation (tanwin):
// nom: -un (ٌ)
// acc: -an (ً)
// gen: -in (ٍ)
//
// Definite endings are single short vowels:
// nom: -u (ُ)
// acc: -a (َ)
// gen: -i (ِ)
fn ar_case_ending(kase: String, definite: String) -> String {
let is_def: Bool = str_eq(definite, "true")
if str_eq(kase, "nom") {
if is_def { return "ُ" }
return "ٌ"
}
if str_eq(kase, "acc") {
if is_def { return "َ" }
return "ً"
}
if str_eq(kase, "gen") {
if is_def { return "ِ" }
return "ٍ"
}
return ""
}
// Gender inference
//
// ar_gender: infer gender from noun form.
// Returns "f" for nouns ending in taa marbuta (ة or ـة), otherwise "m".
// This covers the most reliable heuristic; broken plurals and loanwords may
// vary but are handled by explicit lookup in the Engram.
fn ar_gender(noun: String) -> String {
if ar_str_ends(noun, "ة") { return "f" }
if ar_str_ends(noun, "ـة") { return "f" }
return "m"
}
// Sound plurals
//
// ar_sound_plural: form the sound masculine or feminine plural.
//
// Sound masculine plural (جمع المذكر السالم):
// nom: -uuna (ونَ)
// acc/gen: -iina (ينَ)
//
// Sound feminine plural (جمع المؤنث السالم):
// Remove final ة (taa marbuta) if present, then add -aat (اتٌ/اتُ).
//
// This function returns the base plural form (without case ending) suitable
// for passing to ar_noun_form. For masculine plural case variation, callers
// should use ar_masc_pl_ending.
fn ar_masc_pl_ending(kase: String) -> String {
if str_eq(kase, "nom") { return "ونَ" }
// acc and gen both use -iina in sound masculine plural
return "ينَ"
}
fn ar_sound_plural(noun: String, gender: String) -> String {
if str_eq(gender, "f") {
// Feminine sound plural: drop ة, add ات
if ar_str_ends(noun, "ة") {
let base: String = ar_str_drop_last(noun, 1)
return base + "ات"
}
return noun + "ات"
}
// Masculine sound plural (nominative form as default): -uuna
return noun + "ون"
}
// Full noun inflection
//
// ar_noun_form: produce the inflected noun form.
//
// noun: base (singular) noun string
// gender: "m" | "f" (pass "" to infer from noun ending)
// kase: "nom" | "acc" | "gen" | "" (no case ending added)
// number: "singular" | "plural"
// definite: "true" | "false"
//
// For plurals, the function applies the sound plural (broken plurals are
// language-external and must be supplied via Engram vocabulary nodes).
fn ar_noun_form(noun: String, gender: String, kase: String, number: String, definite: String) -> String {
// Resolve gender
let g: String = gender
if str_eq(g, "") {
let g = ar_gender(noun)
}
// Build the stem (with definiteness and number)
let stem: String = noun
if str_eq(number, "plural") {
if str_eq(g, "m") {
// Masculine sound plural: stem + case-dependent ending
let pl_suf: String = ar_masc_pl_ending(kase)
if str_eq(definite, "true") {
let def_stem: String = ar_definite_article(noun)
return def_stem + pl_suf
}
return noun + pl_suf
}
// Feminine plural: drop ة, add ات + case ending
let fem_pl: String = ar_sound_plural(noun, "f")
let case_end: String = ar_case_ending(kase, definite)
if str_eq(definite, "true") {
return ar_definite_article(fem_pl) + case_end
}
return fem_pl + case_end
}
// Singular
let case_end: String = ar_case_ending(kase, definite)
if str_eq(definite, "true") {
let def_stem: String = ar_definite_article(noun)
return def_stem + case_end
}
return noun + case_end
}
// Convenience: verb inflect entry point
//
// ar_verb_form: thin wrapper matching the signature style of the main engine.
// Accepts gender as part of person encoding: "third_m" | "third_f" | "first" | "second_m" | "second_f".
// Alternatively accepts explicit gender param.
fn ar_verb_form(verb: String, tense: String, person: String, number: String) -> String {
// Default gender to masculine
return ar_conjugate(verb, tense, person, "m", number)
}
+27
View File
@@ -0,0 +1,27 @@
// auto-generated by elc --emit-header — do not edit
extern fn ar_str_ends(s: String, suf: String) -> Bool
extern fn ar_str_len(s: String) -> Int
extern fn ar_str_drop_last(s: String, n: Int) -> String
extern fn ar_str_last_char(s: String) -> String
extern fn ar_slot(person: String, gender: String, number: String) -> Int
extern fn ar_perfect_suffix(slot: Int) -> String
extern fn ar_imperfect_prefix(slot: Int) -> String
extern fn ar_imperfect_suffix(slot: Int) -> String
extern fn ar_conjugate_form1(past_base: String, present_stem: String, tense: String, slot: Int) -> String
extern fn ar_irregular_kaana(slot: Int, tense: String) -> String
extern fn ar_irregular_qaala(slot: Int, tense: String) -> String
extern fn ar_irregular_jaa(slot: Int, tense: String) -> String
extern fn ar_irregular_raaa(slot: Int, tense: String) -> String
extern fn ar_irregular_araada(slot: Int, tense: String) -> String
extern fn ar_irregular_istata(slot: Int, tense: String) -> String
extern fn ar_irregular(verb: String, tense: String, slot: Int) -> String
extern fn ar_present_stem(verb: String) -> String
extern fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn ar_is_sun_letter(c: String) -> Bool
extern fn ar_definite_article(noun: String) -> String
extern fn ar_case_ending(kase: String, definite: String) -> String
extern fn ar_gender(noun: String) -> String
extern fn ar_masc_pl_ending(kase: String) -> String
extern fn ar_sound_plural(noun: String, gender: String) -> String
extern fn ar_noun_form(noun: String, gender: String, kase: String, number: String, definite: String) -> String
extern fn ar_verb_form(verb: String, tense: String, person: String, number: String) -> String
+577
View File
@@ -0,0 +1,577 @@
// morphology-cop.el - Coptic (Sahidic dialect) morphology for the NLG engine.
//
// Implements Coptic verb conjugation (bipartite and tripartite patterns), noun
// phrase assembly with definite and indefinite articles, and noun number marking.
// Designed as a companion to morphology.el; called when language code is "cop".
//
// Language profile: code=cop, name=Coptic, morph_type=agglutinative,
// word_order=SVO, question_strategy=particle, script=coptic, family=afro-asiatic-egyptian.
//
// Script: Coptic uses the Greek alphabet plus seven additional letters borrowed
// from Demotic Egyptian. All Coptic-script characters in this file use their
// correct Unicode code points (Coptic block U+2C80U+2CFF; Coptic letters also
// appear in the Greek block: ϣ U+03E3, ϥ U+03E5, ϩ U+03E9, ϫ U+03EB, ϭ U+03ED).
//
// The El runtime stores strings as byte arrays. String literals with Coptic
// Unicode characters are encoded as UTF-8 and compared via str_eq byte equality.
// The runtime limitation on non-ASCII *output display* does not affect internal
// string logic str_eq and concatenation work correctly.
//
// Grammatical notes (Sahidic Coptic, ca. 2001000 CE):
// - SVO word order (Greek influence; reversed from classical Egyptian)
// - Definite articles prefixed directly to the noun (no space):
// p- (masc sg), t- (fem sg), n- (plural) definite
// ou- (sg indefinite), hen- (pl indefinite)
// - Grammatical gender: masculine / feminine (still active)
// - No case endings grammatical role expressed by word order + prepositions
// - Verb tense/aspect expressed by conjugation base (bipartite pattern):
// Present I: pronoun prefix + verb stem ("f-bwk" = he goes)
// Perfect: a- + pronoun prefix + verb ("a-f-bwk" = he went)
// Future: pronoun prefix + na- + verb ("f-na-bwk" = he will go)
// - Pronoun prefixes (Sahidic used as subject markers in bipartite conjugation):
// 1sg: a-/t- (full: ⲁⲛⲟⲕ) 2sg m: k- 2sg f: te-
// 3sg m: f- 3sg f: s-
// 1pl: n- 2pl: teten- 3pl: se-
// - Copula: "pe" (m sg), "te" (f sg), "ne" (pl); zero copula for adj predicates
// - "to be/become": ϣωπε (Sahidic; present: fϣoop / sϣoop; past: afϣwpe)
//
// Verbs covered (Sahidic transliteration / Coptic script):
// ϣωπε (shwpe) to be / become bwk to go
// nau to see jw to say / speak
// di to give
//
// Canonical English Coptic mapping:
// "be" ϣωπε / zero copula "go" bwk
// "see" nau "say" jw
// "give" di
//
// Persons/numbers covered:
// person: "first" | "second" | "third"
// gender: "m" | "f" (relevant for 2sg and 3sg pronoun prefix selection)
// number: "singular" | "plural"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn cop_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn cop_str_len(s: String) -> Int {
return str_len(s)
}
fn cop_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn cop_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index used in paradigm tables.
// Gender is not encoded in the slot index here; it is passed separately to
// cop_subject_prefix where it matters (2sg and 3sg distinction).
//
// Slot layout:
// 0 = 1st singular (ⲁⲛⲟⲕ anok)
// 1 = 2nd singular (ⲛⲧⲟⲕ/ⲛⲧⲟ ntok/nto) gender resolved in cop_subject_prefix
// 2 = 3rd singular (ⲛⲧⲟϥ/ⲛⲧⲟⲥ ntof/ntos) gender resolved in cop_subject_prefix
// 3 = 1st plural (ⲁⲛⲟⲛ anon)
// 4 = 2nd plural (ⲛⲧⲱⲧⲉⲛ ntwten)
// 5 = 3rd plural (ⲛⲧⲟⲩ ntou)
fn cop_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Subject pronoun prefixes
//
// Coptic bipartite conjugation uses short pronoun prefixes attached directly to
// the verb stem (or to the tense base in tripartite). These are the Sahidic
// bound subject pronouns.
//
// Full independent pronouns (for reference):
// 1sg: ⲁⲛⲟⲕ (anok) prefix: ⲁ- / ⲧ- (varies by tense base)
// 2sg m: ⲛⲧⲟⲕ (ntok) prefix: ⲕ-
// 2sg f: ⲛⲧⲟ (nto) prefix: ⲧⲉ-
// 3sg m: ⲛⲧⲟϥ (ntof) prefix: ϥ-
// 3sg f: ⲛⲧⲟⲥ (ntos) prefix: -
// 1pl: ⲁⲛⲟⲛ (anon) prefix: ⲛ-
// 2pl: ⲛⲧⲱⲧⲉⲛ (ntwten) prefix: ⲧⲉⲧⲉⲛ-
// 3pl: ⲛⲧⲟⲩ (ntou) prefix: ⲥⲉ-
//
// cop_subject_prefix returns the short bound prefix used in bipartite conjugation.
// For the perfect (a-prefix tense base), the subject prefix follows "a-" directly.
fn cop_subject_prefix(person: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "" }
return ""
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return "" }
return "ⲧⲉⲧⲉⲛ"
}
// third
if str_eq(number, "singular") { return "ϥ" }
return "ⲥⲉ"
}
// cop_subject_prefix_gendered: like cop_subject_prefix but handles the
// 2sg feminine (ⲧⲉ-) and 3sg feminine (-) distinction.
fn cop_subject_prefix_gendered(person: String, gender: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "" }
return ""
}
if str_eq(person, "second") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return "ⲧⲉ" }
return ""
}
return "ⲧⲉⲧⲉⲛ"
}
// third person
if str_eq(number, "singular") {
if str_eq(gender, "f") { return "" }
return "ϥ"
}
return "ⲥⲉ"
}
// Copula
//
// The Coptic nominal/adjectival copula is a standalone particle that agrees with
// the gender and number of the subject:
// Masculine sg: ⲡⲉ (pe)
// Feminine sg: ⲧⲉ (te)
// Plural: ⲛⲉ (ne)
//
// For adjective predicates in the present tense, the copula is often zero
// (following the inherited Egyptian zero-copula rule). This engine returns ""
// for the present adjective predicate and the full copula particle otherwise.
fn cop_copula_particle(gender: String, number: String) -> String {
if str_eq(number, "plural") { return "ⲛⲉ" }
if str_eq(gender, "f") { return "ⲧⲉ" }
return "ⲡⲉ"
}
// Verb: ϣωπε (to be / become)
//
// ϣωπε is the Sahidic verb meaning "to be" or "to become". It is used as a
// substantive/existential copula. For adjective predicate sentences the zero
// copula is preferred (inherited from Egyptian).
//
// Sahidic forms:
// Present I (bipartite): prefix + ϣⲟⲟⲡ (e.g. ϥϣⲟⲟⲡ "he is/exists")
// Perfect (a- base): + prefix + ϣⲱⲡⲉ (e.g. ⲁϥϣⲱⲡⲉ "he became")
// Future (na- infix): prefix + ⲛⲁϣⲱⲡⲉ (e.g. ϥⲛⲁϣⲱⲡⲉ "he will become")
//
// Note: ϣⲟⲟⲡ (shoop) is the present stem; ϣⲱⲡⲉ (shwpe) is the infinitive/perfect stem.
fn cop_shwpe_present(prefix: String) -> String {
return prefix + "ϣⲟⲟⲡ"
}
fn cop_shwpe_perfect(prefix: String) -> String {
return "" + prefix + "ϣⲱⲡⲉ"
}
fn cop_shwpe_future(prefix: String) -> String {
return prefix + "ⲛⲁϣⲱⲡⲉ"
}
// Verb: bwk (to go) written ⲃⲱⲕ
//
// A common strong verb. The standard bipartite/tripartite pattern applies.
// Present: prefix + ⲃⲱⲕ (e.g. ϥⲃⲱⲕ "he goes")
// Perfect: + prefix + ⲃⲱⲕ (e.g. ⲁϥⲃⲱⲕ "he went")
// Future: prefix + ⲛⲁⲃⲱⲕ (e.g. ϥⲛⲁⲃⲱⲕ "he will go")
fn cop_bwk_present(prefix: String) -> String {
return prefix + "ⲃⲱⲕ"
}
fn cop_bwk_perfect(prefix: String) -> String {
return "" + prefix + "ⲃⲱⲕ"
}
fn cop_bwk_future(prefix: String) -> String {
return prefix + "ⲛⲁⲃⲱⲕ"
}
// Verb: nau (to see) written ⲛⲁⲩ
//
// nau is a biconsonantal verb. Regular bipartite conjugation:
// Present: prefix + ⲛⲁⲩ (e.g. ϥⲛⲁⲩ "he sees")
// Perfect: + prefix + ⲛⲁⲩ (e.g. ⲁϥⲛⲁⲩ "he saw")
// Future: prefix + ⲛⲁⲛⲁⲩ (e.g. ϥⲛⲁⲛⲁⲩ "he will see")
//
// Note: the future prefix "na-" followed by "nau" produces "nanau" standard.
fn cop_nau_present(prefix: String) -> String {
return prefix + "ⲛⲁⲩ"
}
fn cop_nau_perfect(prefix: String) -> String {
return "" + prefix + "ⲛⲁⲩ"
}
fn cop_nau_future(prefix: String) -> String {
return prefix + "ⲛⲁⲛⲁⲩ"
}
// Verb: jw (to say / speak) written ϫⲱ
//
// ϫⲱ is the Sahidic verb for "to say". Bipartite pattern:
// Present: prefix + ϫⲱ (e.g. ϥϫⲱ "he says")
// Perfect: + prefix + ϫⲱ (e.g. ⲁϥϫⲱ "he said")
// Future: prefix + ⲛⲁϫⲱ (e.g. ϥⲛⲁϫⲱ "he will say")
fn cop_jw_present(prefix: String) -> String {
return prefix + "ϫⲱ"
}
fn cop_jw_perfect(prefix: String) -> String {
return "" + prefix + "ϫⲱ"
}
fn cop_jw_future(prefix: String) -> String {
return prefix + "ⲛⲁϫⲱ"
}
// Verb: di (to give) written ϯ
//
// ϯ (ti/di) is a monosyllabic verb meaning "to give". It is very common in
// Coptic texts. Bipartite pattern:
// Present: prefix + ϯ (e.g. ϥϯ "he gives")
// Perfect: + prefix + ϯ (e.g. ⲁϥϯ "he gave")
// Future: prefix + ⲛⲁϯ (e.g. ϥⲛⲁϯ "he will give")
fn cop_di_present(prefix: String) -> String {
return prefix + "ϯ"
}
fn cop_di_perfect(prefix: String) -> String {
return "" + prefix + "ϯ"
}
fn cop_di_future(prefix: String) -> String {
return prefix + "ⲛⲁϯ"
}
// Copula detection
fn cop_is_copula(verb: String) -> Bool {
if str_eq(verb, "ϣωπε") { return true }
if str_eq(verb, "shwpe") { return true }
if str_eq(verb, "be") { return true }
return false
}
// Known-verb dispatcher
//
// Returns the inflected form for a known verb given the subject prefix string
// and tense. Returns "" if the verb is not in the table.
fn cop_known_verb_prefixed(verb: String, tense: String, prefix: String) -> String {
// ϣωπε / shwpe / "be" to be / become
if str_eq(verb, "ϣωπε") {
if str_eq(tense, "present") { return cop_shwpe_present(prefix) }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return cop_shwpe_present(prefix)
}
if str_eq(verb, "shwpe") {
if str_eq(tense, "present") { return cop_shwpe_present(prefix) }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return cop_shwpe_present(prefix)
}
// bwk / ⲃⲱⲕ to go
if str_eq(verb, "bwk") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
if str_eq(verb, "ⲃⲱⲕ") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
if str_eq(verb, "go") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
// nau / ⲛⲁⲩ to see
if str_eq(verb, "nau") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
if str_eq(verb, "ⲛⲁⲩ") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
if str_eq(verb, "see") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
// jw / ϫⲱ to say / speak
if str_eq(verb, "jw") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
if str_eq(verb, "ϫⲱ") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
if str_eq(verb, "say") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
// di / ϯ to give
if str_eq(verb, "di") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
if str_eq(verb, "ϯ") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
if str_eq(verb, "give") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
// Verb not in table
return ""
}
// Regular verb conjugation
//
// For verbs not in the explicit table, apply the productive bipartite pattern:
// Present: prefix + stem
// Perfect: + prefix + stem
// Future: prefix + ⲛⲁ + stem
fn cop_regular_present(prefix: String, stem: String) -> String {
return prefix + stem
}
fn cop_regular_perfect(prefix: String, stem: String) -> String {
return "" + prefix + stem
}
fn cop_regular_future(prefix: String, stem: String) -> String {
return prefix + "ⲛⲁ" + stem
}
// cop_conjugate: main conjugation entry point
//
// verb: Coptic verb (Sahidic stem, transliterated, or English canonical label)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the fully conjugated form with subject prefix embedded.
// Zero copula ("") is returned for present "be" (adj predicate context).
// For unknown verbs the regular bipartite pattern is applied as a productive fallback.
fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let prefix: String = cop_subject_prefix(person, number)
// Handle "be" canonical zero copula in present; ϣωπε otherwise
if str_eq(verb, "be") {
if str_eq(tense, "present") { return "" }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return ""
}
// Try the known-verb table
let known: String = cop_known_verb_prefixed(verb, tense, prefix)
if !str_eq(known, "") {
return known
}
// Regular productive bipartite conjugation
if str_eq(tense, "present") { return cop_regular_present(prefix, verb) }
if str_eq(tense, "past") { return cop_regular_perfect(prefix, verb) }
if str_eq(tense, "future") { return cop_regular_future(prefix, verb) }
// Unknown tense: return verb as safe fallback
return verb
}
// Article system
//
// cop_article: return the Coptic article string for the given gender/number/definiteness.
//
// Definite articles (prefixed directly to noun, no space):
// Masculine singular: ⲡ- (p-)
// Feminine singular: ⲧ- (t-)
// Plural (both): ⲛ- (n-)
//
// Indefinite articles:
// Singular (both genders): ⲟⲩ- (ou-)
// Plural: ϩⲉⲛ- (hen-)
//
// gender: "m" | "f"
// number: "singular" | "plural"
// definite: "true" | "false"
//
// Returns the article prefix string (to be concatenated with the noun).
fn cop_article(gender: String, number: String, definite: String) -> String {
if str_eq(definite, "true") {
if str_eq(number, "plural") { return "" }
if str_eq(gender, "f") { return "" }
return ""
}
// Indefinite
if str_eq(number, "plural") { return "ϩⲉⲛ" }
return "ⲟⲩ"
}
// Noun number
//
// cop_decline: return the noun in the appropriate number form.
//
// Coptic nouns have no case endings. Grammatical role is expressed entirely by
// word order and prepositions. The gram_case parameter is accepted for API
// symmetry with other morphology modules but has no effect.
//
// Plural formation:
// Coptic plural morphology is highly irregular (inherited from Egyptian and
// influenced by Greek loanwords). Common patterns:
// - Many nouns show no suffix change plurality is indicated only by the plural article ⲛ-.
// - Some nouns take -ⲟⲟⲩⲉ (-ooue): e.g. ϩⲟ (face) ϩⲟⲟⲩⲉ
// - Greek loanwords often add -ⲟⲥ / -ⲟⲩ in Greek fashion
//
// This function implements:
// - No suffix change (base form) as the productive default the article carries number.
// - Words ending in (a common Coptic nominal ending) may take -ⲟⲟⲩⲉ in the plural;
// this suffix is applied only when the caller explicitly requests plural and the
// noun ends in (productive pattern).
// Vocabulary-layer irregular plurals should be stored in vocabulary-cop.el and
// passed already inflected.
fn cop_decline(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") { return noun }
// Plural: if noun ends in ⲉ, attempt -ooue suffix (common productive pattern)
if cop_str_ends(noun, "") {
let stem: String = cop_drop(noun, 1)
return stem + "ⲟⲟⲩⲉ"
}
// Default: base form (article carries the plural signal)
return noun
}
// Noun phrase assembly
//
// cop_noun_phrase: build a complete Coptic noun phrase.
//
// noun: base noun (Coptic script or transliteration)
// gram_case: accepted for API symmetry; has no effect (Coptic is caseless)
// number: "singular" | "plural"
// definite: "true" | "false"
//
// The article is prefixed directly to the noun with no intervening space,
// following standard Coptic orthographic convention.
// Gender defaults to masculine when not determinable from context; the caller
// should supply the declined noun already in its correct form if gender-sensitive
// plural forms are needed.
fn cop_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let form: String = cop_decline(noun, gram_case, number)
// Infer gender from number: if plural, gender is moot for the article (always ⲛ-)
// For singular, default to masculine (caller provides gender via article if known)
let art: String = cop_article("m", number, definite)
if str_eq(definite, "true") {
return art + form
}
if str_eq(definite, "false") {
// Indefinite article + noun (no space Coptic convention for proclitic articles)
return art + form
}
return form
}
// cop_noun_phrase_gendered: noun phrase with explicit gender for correct article selection.
//
// gender: "m" | "f"
fn cop_noun_phrase_gendered(noun: String, gram_case: String, number: String, definite: String, gender: String) -> String {
let form: String = cop_decline(noun, gram_case, number)
let art: String = cop_article(gender, number, definite)
if str_eq(definite, "true") {
return art + form
}
if str_eq(definite, "false") {
return art + form
}
return form
}
// Canonical verb mapping
//
// cop_map_canonical: map cross-lingual English canonical verb labels to their
// Sahidic Coptic equivalents before dispatching to cop_conjugate.
fn cop_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "be" }
if str_eq(verb, "go") { return "bwk" }
if str_eq(verb, "see") { return "nau" }
if str_eq(verb, "say") { return "jw" }
if str_eq(verb, "speak") { return "jw" }
if str_eq(verb, "give") { return "di" }
// Unknown: return as-is; cop_conjugate will apply the regular pattern
return verb
}
+35
View File
@@ -0,0 +1,35 @@
// auto-generated by elc --emit-header — do not edit
extern fn cop_str_ends(s: String, suf: String) -> Bool
extern fn cop_str_len(s: String) -> Int
extern fn cop_drop(s: String, n: Int) -> String
extern fn cop_last_char(s: String) -> String
extern fn cop_slot(person: String, number: String) -> Int
extern fn cop_subject_prefix(person: String, number: String) -> String
extern fn cop_subject_prefix_gendered(person: String, gender: String, number: String) -> String
extern fn cop_copula_particle(gender: String, number: String) -> String
extern fn cop_shwpe_present(prefix: String) -> String
extern fn cop_shwpe_perfect(prefix: String) -> String
extern fn cop_shwpe_future(prefix: String) -> String
extern fn cop_bwk_present(prefix: String) -> String
extern fn cop_bwk_perfect(prefix: String) -> String
extern fn cop_bwk_future(prefix: String) -> String
extern fn cop_nau_present(prefix: String) -> String
extern fn cop_nau_perfect(prefix: String) -> String
extern fn cop_nau_future(prefix: String) -> String
extern fn cop_jw_present(prefix: String) -> String
extern fn cop_jw_perfect(prefix: String) -> String
extern fn cop_jw_future(prefix: String) -> String
extern fn cop_di_present(prefix: String) -> String
extern fn cop_di_perfect(prefix: String) -> String
extern fn cop_di_future(prefix: String) -> String
extern fn cop_is_copula(verb: String) -> Bool
extern fn cop_known_verb_prefixed(verb: String, tense: String, prefix: String) -> String
extern fn cop_regular_present(prefix: String, stem: String) -> String
extern fn cop_regular_perfect(prefix: String, stem: String) -> String
extern fn cop_regular_future(prefix: String, stem: String) -> String
extern fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn cop_article(gender: String, number: String, definite: String) -> String
extern fn cop_decline(noun: String, gram_case: String, number: String) -> String
extern fn cop_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn cop_noun_phrase_gendered(noun: String, gram_case: String, number: String, definite: String, gender: String) -> String
extern fn cop_map_canonical(verb: String) -> String
+814
View File
@@ -0,0 +1,814 @@
// morphology-de.el - German morphology: articles, adjective endings, noun
// plurals, and verb conjugation.
//
// German is a fusional language with:
// - 4 grammatical cases: nominative, accusative, dative, genitive
// - 3 genders: masculine (m), feminine (f), neuter (n)
// - 2 numbers: singular, plural
// - Strong and weak verb classes
//
// Conventions used throughout:
// gender: "m" | "f" | "n"
// case: "nom" | "acc" | "dat" | "gen"
// number: "sg" | "pl"
// person: "1" | "2" | "3"
// tense: "present" | "past" | "future"
// article_type: "def" | "indef" | "none"
//
// Depends on: language-profile (str_eq, str_len, str_slice, str_drop_last,
// str_ends_with)
// Definite articles (der-words)
//
// Masc Fem Neut Plural
// Nom: der die das die
// Acc: den die das die
// Dat: dem der dem den
// Gen: des der des der
import "morphology.el"
fn de_article_def(gender: String, gram_case: String, number: String) -> String {
if str_eq(number, "pl") {
if str_eq(gram_case, "nom") { return "die" }
if str_eq(gram_case, "acc") { return "die" }
if str_eq(gram_case, "dat") { return "den" }
if str_eq(gram_case, "gen") { return "der" }
return "die"
}
if str_eq(gender, "m") {
if str_eq(gram_case, "nom") { return "der" }
if str_eq(gram_case, "acc") { return "den" }
if str_eq(gram_case, "dat") { return "dem" }
if str_eq(gram_case, "gen") { return "des" }
return "der"
}
if str_eq(gender, "f") {
if str_eq(gram_case, "nom") { return "die" }
if str_eq(gram_case, "acc") { return "die" }
if str_eq(gram_case, "dat") { return "der" }
if str_eq(gram_case, "gen") { return "der" }
return "die"
}
// neuter
if str_eq(gram_case, "nom") { return "das" }
if str_eq(gram_case, "acc") { return "das" }
if str_eq(gram_case, "dat") { return "dem" }
if str_eq(gram_case, "gen") { return "des" }
return "das"
}
// Indefinite articles (ein-words)
//
// Masc Fem Neut Plural
// Nom: ein eine ein
// Acc: einen eine ein
// Dat: einem einer einem
// Gen: eines einer eines
fn de_article_indef(gender: String, gram_case: String, number: String) -> String {
if str_eq(number, "pl") {
// Indefinite article has no plural form
return ""
}
if str_eq(gender, "m") {
if str_eq(gram_case, "nom") { return "ein" }
if str_eq(gram_case, "acc") { return "einen" }
if str_eq(gram_case, "dat") { return "einem" }
if str_eq(gram_case, "gen") { return "eines" }
return "ein"
}
if str_eq(gender, "f") {
if str_eq(gram_case, "nom") { return "eine" }
if str_eq(gram_case, "acc") { return "eine" }
if str_eq(gram_case, "dat") { return "einer" }
if str_eq(gram_case, "gen") { return "einer" }
return "eine"
}
// neuter
if str_eq(gram_case, "nom") { return "ein" }
if str_eq(gram_case, "acc") { return "ein" }
if str_eq(gram_case, "dat") { return "einem" }
if str_eq(gram_case, "gen") { return "eines" }
return "ein"
}
// de_article: unified article dispatch.
// definite: "def" | "indef" | "none"
fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String {
if str_eq(definite, "def") { return de_article_def(gender, gram_case, number) }
if str_eq(definite, "indef") { return de_article_indef(gender, gram_case, number) }
return ""
}
// Adjective endings
//
// Weak endings (after a definite article or der-word):
//
// Masc Fem Neut Plural
// Nom: -e -e -e -en
// Acc: -en -e -e -en
// Dat: -en -en -en -en
// Gen: -en -en -en -en
//
// Mixed endings (after ein-words with no marking, i.e. indef article):
//
// Masc Fem Neut Plural
// Nom: -er -e -es -en
// Acc: -en -e -es -en
// Dat: -en -en -en -en
// Gen: -en -en -en -en
//
// Strong endings (no preceding article):
//
// Masc Fem Neut Plural
// Nom: -er -e -es -e
// Acc: -en -e -es -e
// Dat: -em -er -em -en
// Gen: -en -er -en -er
//
// article_type: "def" | "indef" | "none"
fn de_adj_ending(gender: String, gram_case: String, number: String, article_type: String) -> String {
if str_eq(article_type, "def") {
// Weak declension
if str_eq(number, "pl") {
return "en"
}
if str_eq(gender, "m") {
if str_eq(gram_case, "nom") { return "e" }
return "en"
}
if str_eq(gender, "f") {
if str_eq(gram_case, "nom") { return "e" }
if str_eq(gram_case, "acc") { return "e" }
return "en"
}
// neuter
if str_eq(gram_case, "nom") { return "e" }
if str_eq(gram_case, "acc") { return "e" }
return "en"
}
if str_eq(article_type, "indef") {
// Mixed declension
if str_eq(number, "pl") {
return "en"
}
if str_eq(gender, "m") {
if str_eq(gram_case, "nom") { return "er" }
return "en"
}
if str_eq(gender, "f") {
if str_eq(gram_case, "nom") { return "e" }
if str_eq(gram_case, "acc") { return "e" }
return "en"
}
// neuter
if str_eq(gram_case, "nom") { return "es" }
if str_eq(gram_case, "acc") { return "es" }
return "en"
}
// Strong declension (no article)
if str_eq(number, "pl") {
if str_eq(gram_case, "nom") { return "e" }
if str_eq(gram_case, "acc") { return "e" }
if str_eq(gram_case, "dat") { return "en" }
if str_eq(gram_case, "gen") { return "er" }
return "e"
}
if str_eq(gender, "m") {
if str_eq(gram_case, "nom") { return "er" }
if str_eq(gram_case, "acc") { return "en" }
if str_eq(gram_case, "dat") { return "em" }
if str_eq(gram_case, "gen") { return "en" }
return "er"
}
if str_eq(gender, "f") {
if str_eq(gram_case, "nom") { return "e" }
if str_eq(gram_case, "acc") { return "e" }
if str_eq(gram_case, "dat") { return "er" }
if str_eq(gram_case, "gen") { return "er" }
return "e"
}
// neuter
if str_eq(gram_case, "nom") { return "es" }
if str_eq(gram_case, "acc") { return "es" }
if str_eq(gram_case, "dat") { return "em" }
if str_eq(gram_case, "gen") { return "en" }
return "es"
}
// Noun plural formation
//
// Major patterns, keyed on lemma. Where a noun is known irregular, the full
// plural is returned. Otherwise a productive heuristic by gender and ending
// is applied:
//
// Masculine hard nouns +e (der Tag Tage)
// Feminine nouns in -e +n (die Katze Katzen)
// Feminine nouns +en (die Frau Frauen)
// Neuter nouns in -chen/-lein (das Mädchen Mädchen)
// Neuter nouns in -um -um +en (das Zentrum Zentren)
// Loanwords in -a,-o,-i +s (das Auto Autos)
// Default +e
fn de_noun_plural(noun: String, gender: String) -> String {
// Lexical irregulars
if str_eq(noun, "Mann") { return "Männer" }
if str_eq(noun, "Kind") { return "Kinder" }
if str_eq(noun, "Haus") { return "Häuser" }
if str_eq(noun, "Buch") { return "Bücher" }
if str_eq(noun, "Mutter") { return "Mütter" }
if str_eq(noun, "Vater") { return "Väter" }
if str_eq(noun, "Bruder") { return "Brüder" }
if str_eq(noun, "Tochter") { return "Töchter" }
if str_eq(noun, "Nacht") { return "Nächte" }
if str_eq(noun, "Stadt") { return "Städte" }
if str_eq(noun, "Wort") { return "Wörter" }
if str_eq(noun, "Gott") { return "Götter" }
if str_eq(noun, "Wald") { return "Wälder" }
if str_eq(noun, "Band") { return "Bände" }
if str_eq(noun, "Hund") { return "Hunde" }
if str_eq(noun, "Baum") { return "Bäume" }
if str_eq(noun, "Raum") { return "Räume" }
if str_eq(noun, "Traum") { return "Träume" }
if str_eq(noun, "Zug") { return "Züge" }
if str_eq(noun, "Flug") { return "Flüge" }
if str_eq(noun, "Fuß") { return "Füße" }
if str_eq(noun, "Gruß") { return "Grüße" }
if str_eq(noun, "Geist") { return "Geister" }
if str_eq(noun, "Schwanz") { return "Schwänze" }
if str_eq(noun, "Stuhl") { return "Stühle" }
if str_eq(noun, "Stuhl") { return "Stühle" }
if str_eq(noun, "Sohn") { return "Söhne" }
if str_eq(noun, "Ton") { return "Töne" }
if str_eq(noun, "Fluss") { return "Flüsse" }
if str_eq(noun, "Frau") { return "Frauen" }
if str_eq(noun, "Straße") { return "Straßen" }
if str_eq(noun, "Schule") { return "Schulen" }
if str_eq(noun, "Blume") { return "Blumen" }
if str_eq(noun, "Katze") { return "Katzen" }
if str_eq(noun, "Sprache") { return "Sprachen" }
if str_eq(noun, "Kirche") { return "Kirchen" }
if str_eq(noun, "Tür") { return "Türen" }
if str_eq(noun, "Uhr") { return "Uhren" }
if str_eq(noun, "Zahl") { return "Zahlen" }
if str_eq(noun, "Wahl") { return "Wahlen" }
if str_eq(noun, "Bahn") { return "Bahnen" }
if str_eq(noun, "Zahn") { return "Zähne" }
if str_eq(noun, "Nase") { return "Nasen" }
if str_eq(noun, "Maus") { return "Mäuse" }
if str_eq(noun, "Mädchen") { return "Mädchen" }
if str_eq(noun, "Messer") { return "Messer" }
if str_eq(noun, "Fenster") { return "Fenster" }
if str_eq(noun, "Zimmer") { return "Zimmer" }
if str_eq(noun, "Wasser") { return "Wasser" }
if str_eq(noun, "Bett") { return "Betten" }
if str_eq(noun, "Auto") { return "Autos" }
if str_eq(noun, "Kino") { return "Kinos" }
if str_eq(noun, "Radio") { return "Radios" }
if str_eq(noun, "Foto") { return "Fotos" }
if str_eq(noun, "Cafe") { return "Cafes" }
if str_eq(noun, "Zentrum") { return "Zentren" }
if str_eq(noun, "Museum") { return "Museen" }
if str_eq(noun, "Gymnasium") { return "Gymnasien" }
if str_eq(noun, "Studium") { return "Studien" }
if str_eq(noun, "Datum") { return "Daten" }
// Productive heuristics
// Nouns ending in -chen or -lein: no change (diminutives)
if str_ends_with(noun, "chen") { return noun }
if str_ends_with(noun, "lein") { return noun }
// Nouns ending in -um: replace with -en
if str_ends_with(noun, "um") {
return str_drop_last(noun, 2) + "en"
}
// Loanwords ending in vowel or -s: add -s
if str_ends_with(noun, "a") { return noun + "s" }
if str_ends_with(noun, "o") { return noun + "s" }
if str_ends_with(noun, "i") { return noun + "s" }
if str_ends_with(noun, "u") { return noun + "s" }
if str_ends_with(noun, "y") { return noun + "s" }
// Feminine nouns ending in -e: add -n
if str_eq(gender, "f") {
if str_ends_with(noun, "e") {
return noun + "n"
}
// Feminine nouns ending in -in: add -nen
if str_ends_with(noun, "in") {
return noun + "nen"
}
// Most other feminines: add -en
return noun + "en"
}
// Neuter and masculine: default to +e
return noun + "e"
}
// Noun case endings
//
// In German, noun case inflection is mostly carried by the article and
// adjective. The noun itself only changes in two regular situations:
// - Genitive singular masculine/neuter: -(e)s
// - Dative plural: -n (if not already ending in -n or -s)
//
// Irregular genitive forms (e.g. N-declension: Herr Herrn) are
// handled per-lemma in de_case_ending.
fn de_case_ending(noun: String, gender: String, gram_case: String, number: String) -> String {
// N-declension masculines (weak nouns): all non-nominative singular forms + all plural add -(e)n
if str_eq(noun, "Herr") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "Herr" }
return "Herrn"
}
return "Herren"
}
if str_eq(noun, "Mensch") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "Mensch" }
return "Menschen"
}
return "Menschen"
}
if str_eq(noun, "Student") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "Student" }
return "Studenten"
}
return "Studenten"
}
if str_eq(noun, "Kollege") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "Kollege" }
return "Kollegen"
}
return "Kollegen"
}
if str_eq(noun, "Name") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "Name" }
if str_eq(gram_case, "gen") { return "Namens" }
return "Namen"
}
return "Namen"
}
// Regular masculine/neuter: genitive singular gets -(e)s
if str_eq(number, "sg") {
if str_eq(gram_case, "gen") {
if str_eq(gender, "m") {
if str_ends_with(noun, "s") { return noun + "es" }
if str_ends_with(noun, "x") { return noun + "es" }
if str_ends_with(noun, "z") { return noun + "es" }
if str_ends_with(noun, "sch") { return noun + "es" }
return noun + "s"
}
if str_eq(gender, "n") {
if str_ends_with(noun, "s") { return noun + "es" }
if str_ends_with(noun, "x") { return noun + "es" }
if str_ends_with(noun, "z") { return noun + "es" }
return noun + "s"
}
}
// All other singular cases: noun unchanged
return noun
}
// Plural dative: add -n unless already ending in -n or -s
if str_eq(gram_case, "dat") {
let pl: String = de_noun_plural(noun, gender)
if str_ends_with(pl, "n") { return pl }
if str_ends_with(pl, "s") { return pl }
return pl + "n"
}
// All other plural cases: return the standard plural form
return de_noun_plural(noun, gender)
}
// Weak verb conjugation
//
// Model: machen (mach-)
//
// Present:
// 1sg ich mache 2sg du machst 3sg er/sie/es macht
// 1pl wir machen 2pl ihr macht 3pl sie machen
//
// Past (Präteritum):
// 1sg ich machte 2sg du machtest 3sg er/sie/es machte
// 1pl wir machten 2pl ihr machtet 3pl sie machten
fn de_conjugate_weak(stem: String, tense: String, person: String, number: String) -> String {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return stem + "e" }
if str_eq(person, "2") {
// Stems ending in -t or -d insert -e- before -st
if str_ends_with(stem, "t") { return stem + "est" }
if str_ends_with(stem, "d") { return stem + "est" }
return stem + "st"
}
// 3sg
if str_ends_with(stem, "t") { return stem + "et" }
if str_ends_with(stem, "d") { return stem + "et" }
return stem + "t"
}
// plural
if str_eq(person, "1") { return stem + "en" }
if str_eq(person, "2") {
if str_ends_with(stem, "t") { return stem + "et" }
if str_ends_with(stem, "d") { return stem + "et" }
return stem + "t"
}
// 3pl
return stem + "en"
}
if str_eq(tense, "past") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return stem + "te" }
if str_eq(person, "2") { return stem + "test" }
return stem + "te"
}
if str_eq(person, "1") { return stem + "ten" }
if str_eq(person, "2") { return stem + "tet" }
return stem + "ten"
}
// Future: werden + infinitive caller must prepend the auxiliary
return stem + "en"
}
// Strong / irregular verb present-tense forms
//
// Returns the correct surface form if the verb is irregular, or "" if unknown.
// Only present-tense irregulars are encoded here because past tense for strong
// verbs is stored as a separate stem (Ablaut) see de_conjugate.
fn de_irregular_present(verb: String, person: String, number: String) -> String {
// sein fully irregular
if str_eq(verb, "sein") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "bin" }
if str_eq(person, "2") { return "bist" }
return "ist"
}
if str_eq(person, "1") { return "sind" }
if str_eq(person, "2") { return "seid" }
return "sind"
}
// haben
if str_eq(verb, "haben") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "habe" }
if str_eq(person, "2") { return "hast" }
return "hat"
}
if str_eq(person, "1") { return "haben" }
if str_eq(person, "2") { return "habt" }
return "haben"
}
// werden
if str_eq(verb, "werden") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "werde" }
if str_eq(person, "2") { return "wirst" }
return "wird"
}
if str_eq(person, "1") { return "werden" }
if str_eq(person, "2") { return "werdet" }
return "werden"
}
// gehen
if str_eq(verb, "gehen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "gehe" }
if str_eq(person, "2") { return "gehst" }
return "geht"
}
if str_eq(person, "1") { return "gehen" }
if str_eq(person, "2") { return "geht" }
return "gehen"
}
// kommen
if str_eq(verb, "kommen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "komme" }
if str_eq(person, "2") { return "kommst" }
return "kommt"
}
if str_eq(person, "1") { return "kommen" }
if str_eq(person, "2") { return "kommt" }
return "kommen"
}
// sehen vowel change eie in 2sg/3sg
if str_eq(verb, "sehen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "sehe" }
if str_eq(person, "2") { return "siehst" }
return "sieht"
}
if str_eq(person, "1") { return "sehen" }
if str_eq(person, "2") { return "seht" }
return "sehen"
}
// essen vowel change ei in 2sg/3sg
if str_eq(verb, "essen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "esse" }
if str_eq(person, "2") { return "isst" }
return "isst"
}
if str_eq(person, "1") { return "essen" }
if str_eq(person, "2") { return "esst" }
return "essen"
}
// geben vowel change ei in 2sg/3sg
if str_eq(verb, "geben") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "gebe" }
if str_eq(person, "2") { return "gibst" }
return "gibt"
}
if str_eq(person, "1") { return "geben" }
if str_eq(person, "2") { return "gebt" }
return "geben"
}
// nehmen vowel change ei + consonant change in 2sg/3sg
if str_eq(verb, "nehmen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "nehme" }
if str_eq(person, "2") { return "nimmst" }
return "nimmt"
}
if str_eq(person, "1") { return "nehmen" }
if str_eq(person, "2") { return "nehmt" }
return "nehmen"
}
// fahren vowel change aä in 2sg/3sg
if str_eq(verb, "fahren") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "fahre" }
if str_eq(person, "2") { return "fährst" }
return "fährt"
}
if str_eq(person, "1") { return "fahren" }
if str_eq(person, "2") { return "fahrt" }
return "fahren"
}
// laufen vowel change auäu in 2sg/3sg
if str_eq(verb, "laufen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "laufe" }
if str_eq(person, "2") { return "läufst" }
return "läuft"
}
if str_eq(person, "1") { return "laufen" }
if str_eq(person, "2") { return "lauft" }
return "laufen"
}
// wissen irregular throughout
if str_eq(verb, "wissen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "weiß" }
if str_eq(person, "2") { return "weißt" }
return "weiß"
}
if str_eq(person, "1") { return "wissen" }
if str_eq(person, "2") { return "wisst" }
return "wissen"
}
// können modal
if str_eq(verb, "können") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "kann" }
if str_eq(person, "2") { return "kannst" }
return "kann"
}
if str_eq(person, "1") { return "können" }
if str_eq(person, "2") { return "könnt" }
return "können"
}
// müssen modal
if str_eq(verb, "müssen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "muss" }
if str_eq(person, "2") { return "musst" }
return "muss"
}
if str_eq(person, "1") { return "müssen" }
if str_eq(person, "2") { return "müsst" }
return "müssen"
}
// wollen modal
if str_eq(verb, "wollen") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "will" }
if str_eq(person, "2") { return "willst" }
return "will"
}
if str_eq(person, "1") { return "wollen" }
if str_eq(person, "2") { return "wollt" }
return "wollen"
}
// Unknown: signal caller to fall through to weak conjugation
return ""
}
// Strong verb past-tense (Präteritum) Ablaut stems
//
// Returns the past stem for strong verbs (Ablaut form), or "" if unknown/weak.
// The past-tense endings for strong verbs differ from weak:
// 1sg/3sg: bare stem (no ending)
// 2sg: stem + -st
// 1pl/3pl: stem + -en
// 2pl: stem + -t
fn de_strong_past_stem(verb: String) -> String {
if str_eq(verb, "gehen") { return "ging" }
if str_eq(verb, "kommen") { return "kam" }
if str_eq(verb, "sehen") { return "sah" }
if str_eq(verb, "geben") { return "gab" }
if str_eq(verb, "nehmen") { return "nahm" }
if str_eq(verb, "fahren") { return "fuhr" }
if str_eq(verb, "laufen") { return "lief" }
if str_eq(verb, "schreiben") { return "schrieb" }
if str_eq(verb, "bleiben") { return "blieb" }
if str_eq(verb, "steigen") { return "stieg" }
if str_eq(verb, "lesen") { return "las" }
if str_eq(verb, "sprechen") { return "sprach" }
if str_eq(verb, "treffen") { return "traf" }
if str_eq(verb, "essen") { return "" }
if str_eq(verb, "trinken") { return "trank" }
if str_eq(verb, "finden") { return "fand" }
if str_eq(verb, "denken") { return "dachte" }
if str_eq(verb, "bringen") { return "brachte" }
if str_eq(verb, "stehen") { return "stand" }
if str_eq(verb, "liegen") { return "lag" }
if str_eq(verb, "sitzen") { return "saß" }
if str_eq(verb, "fallen") { return "fiel" }
if str_eq(verb, "halten") { return "hielt" }
if str_eq(verb, "rufen") { return "rief" }
if str_eq(verb, "tragen") { return "trug" }
if str_eq(verb, "schlagen") { return "schlug" }
if str_eq(verb, "ziehen") { return "zog" }
if str_eq(verb, "wachsen") { return "wuchs" }
if str_eq(verb, "helfen") { return "half" }
if str_eq(verb, "werfen") { return "warf" }
return ""
}
// Normalization helpers
//
// The realizer sends long-form labels ("singular", "first").
// German morphology uses short forms ("sg", "1"). Normalize on entry.
fn de_norm_number(number: String) -> String {
if str_eq(number, "singular") { return "sg" }
if str_eq(number, "plural") { return "pl" }
return number
}
fn de_norm_person(person: String) -> String {
if str_eq(person, "first") { return "1" }
if str_eq(person, "second") { return "2" }
if str_eq(person, "third") { return "3" }
return person
}
// Unified German verb conjugation
//
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3" (also accepts "first" | "second" | "third")
// number: "sg" | "pl" (also accepts "singular" | "plural")
fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let number = de_norm_number(number)
let person = de_norm_person(person)
// Future tense: werden (conjugated) + infinitive
if str_eq(tense, "future") {
let aux: String = de_irregular_present("werden", person, number)
return aux + " " + verb
}
// sein past is also fully irregular
if str_eq(verb, "sein") {
if str_eq(tense, "present") {
return de_irregular_present("sein", person, number)
}
// Past (war)
if str_eq(number, "sg") {
if str_eq(person, "1") { return "war" }
if str_eq(person, "2") { return "warst" }
return "war"
}
if str_eq(person, "1") { return "waren" }
if str_eq(person, "2") { return "wart" }
return "waren"
}
// haben past: hatte
if str_eq(verb, "haben") {
if str_eq(tense, "present") {
return de_irregular_present("haben", person, number)
}
if str_eq(number, "sg") {
if str_eq(person, "1") { return "hatte" }
if str_eq(person, "2") { return "hattest" }
return "hatte"
}
if str_eq(person, "1") { return "hatten" }
if str_eq(person, "2") { return "hattet" }
return "hatten"
}
// wissen past: wusste (mixed/irregular)
if str_eq(verb, "wissen") {
if str_eq(tense, "present") {
return de_irregular_present("wissen", person, number)
}
if str_eq(number, "sg") {
if str_eq(person, "1") { return "wusste" }
if str_eq(person, "2") { return "wusstest" }
return "wusste"
}
if str_eq(person, "1") { return "wussten" }
if str_eq(person, "2") { return "wusstet" }
return "wussten"
}
// Modals: können, müssen, wollen past uses weak -te suffix on preterite stem
if str_eq(verb, "können") {
if str_eq(tense, "present") {
return de_irregular_present("können", person, number)
}
return de_conjugate_weak("konnt", "past", person, number)
}
if str_eq(verb, "müssen") {
if str_eq(tense, "present") {
return de_irregular_present("müssen", person, number)
}
return de_conjugate_weak("musst", "past", person, number)
}
if str_eq(verb, "wollen") {
if str_eq(tense, "present") {
return de_irregular_present("wollen", person, number)
}
return de_conjugate_weak("wollt", "past", person, number)
}
// Present: try irregular table first
if str_eq(tense, "present") {
let irr: String = de_irregular_present(verb, person, number)
if !str_eq(irr, "") {
return irr
}
// Fall through to weak conjugation using infinitive stem (drop -en)
let stem: String = str_drop_last(verb, 2)
return de_conjugate_weak(stem, "present", person, number)
}
// Past: try strong Ablaut first
if str_eq(tense, "past") {
let ps: String = de_strong_past_stem(verb)
if !str_eq(ps, "") {
// Strong past endings: 1sg/3sg bare, 2sg+st, 1pl/3pl+en, 2pl+t
if str_eq(number, "sg") {
if str_eq(person, "1") { return ps }
if str_eq(person, "2") { return ps + "st" }
return ps
}
if str_eq(person, "1") { return ps + "en" }
if str_eq(person, "2") { return ps + "t" }
return ps + "en"
}
// Weak past
let stem: String = str_drop_last(verb, 2)
return de_conjugate_weak(stem, "past", person, number)
}
// Fallback: return infinitive
return verb
}
+13
View File
@@ -0,0 +1,13 @@
// auto-generated by elc --emit-header — do not edit
extern fn de_article_def(gender: String, gram_case: String, number: String) -> String
extern fn de_article_indef(gender: String, gram_case: String, number: String) -> String
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
extern fn de_adj_ending(gender: String, gram_case: String, number: String, article_type: String) -> String
extern fn de_noun_plural(noun: String, gender: String) -> String
extern fn de_case_ending(noun: String, gender: String, gram_case: String, number: String) -> String
extern fn de_conjugate_weak(stem: String, tense: String, person: String, number: String) -> String
extern fn de_irregular_present(verb: String, person: String, number: String) -> String
extern fn de_strong_past_stem(verb: String) -> String
extern fn de_norm_number(number: String) -> String
extern fn de_norm_person(person: String) -> String
extern fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String
+571
View File
@@ -0,0 +1,571 @@
// morphology-egy.el - Ancient Egyptian (Middle Egyptian) morphology for the NLG engine.
//
// Implements Middle Egyptian verb conjugation (sdm=f / sdm.n=f paradigm),
// noun number marking, suffix pronouns, and noun phrase assembly.
// Designed as a companion to morphology.el; called when language code is "egy".
//
// Language profile: code=egy, name=Ancient Egyptian, morph_type=agglutinative,
// word_order=SVO (Middle Egyptian nominal sentences), question_strategy=particle,
// script=hieroglyphic (transliterated here as ASCII), family=afro-asiatic-egyptian.
//
// Script note: Classical transliteration uses special characters ( š q ).
// This engine uses a safe ASCII mapping:
// A = (aleph/glottal stop) a = (ayin)
// H = (h with dot) x = (velar fricative)
// X = (emphatic h) sh = š (sh sound)
// q = q (emphatic k) T = (tj sound)
// D = (dj sound)
// This mapping keeps all string literals ASCII-safe for the El runtime.
//
// Grammatical notes (Middle Egyptian, ca. 20001300 BCE):
// - Aspectual system: Imperfective (sdm=f), Perfective (sdm.n=f), Prospective
// - "tense" labels used here: "present" (imperfective), "past" (perfective),
// "future" (prospective/sdm.xr=f), "infinitive"
// - Two grammatical genders: masculine (unmarked) and feminine (suffix -t)
// - Number: singular (unmarked), dual (-wy masc / -ty fem), plural (-w masc / -wt fem)
// - No case endings syntactic role expressed by word order and prepositions
// - No definite/indefinite article in Middle Egyptian (Late Egyptian introduced pꜣ/tꜣ/nꜣ)
// - Zero copula: adjectival predicates need no verb "to be" ("nfr sw" = "he is good")
// - Suffix pronouns attach directly to the verb stem with = (e.g. sdm=f "he hears")
//
// Persons/numbers covered (suffix pronoun paradigm):
// person: "first" | "second" | "third"
// gender: "m" | "f" (relevant for 2sg, 3sg; 1sg and plurals often unmarked)
// number: "singular" | "dual" | "plural"
//
// Verbs covered (ASCII transliteration gloss):
// wnn to be/exist (copular auxiliary)
// rdi / di to give
// mAA to see
// Dd to say
// Sm to go
// iri to do / make
// sdm to hear (the paradigm verb for the sdm=f construction)
//
// Canonical English Egyptian mapping:
// "be" wnn / zero copula "give" rdi
// "see" mAA "say" Dd
// "go" Sm "do" iri
// "make" iri "hear" sdm
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn egy_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn egy_str_len(s: String) -> Int {
return str_len(s)
}
fn egy_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn egy_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Person/number slot
//
// Maps person × gender × number to a 0-based index used in paradigm tables.
// Egyptian suffix pronouns distinguish gender in 2nd and 3rd person singular.
//
// Slot layout:
// 0 = 1sg (=i)
// 1 = 2sg masc (=k)
// 2 = 2sg fem (=T)
// 3 = 3sg masc (=f)
// 4 = 3sg fem (=s)
// 5 = 1pl (=n)
// 6 = 2pl (=Tn)
// 7 = 3pl (=sn)
// 8 = 1du / 2du / 3du (=sny simplified; dual pronouns are rare in sources)
//
// Dual falls through to slot 8 (a single dual pronoun slot for all persons).
fn egy_slot(person: String, number: String) -> Int {
if str_eq(number, "dual") { return 8 }
if str_eq(person, "first") {
if str_eq(number, "plural") { return 5 }
return 0
}
if str_eq(person, "second") {
if str_eq(number, "plural") { return 6 }
return 1
}
// third person
if str_eq(number, "plural") { return 7 }
return 3
}
// egy_slot_with_gender: slot variant that factors in gender for 2sg and 3sg.
fn egy_slot_with_gender(person: String, gender: String, number: String) -> Int {
if str_eq(number, "dual") { return 8 }
if str_eq(person, "first") {
if str_eq(number, "plural") { return 5 }
return 0
}
if str_eq(person, "second") {
if str_eq(number, "plural") { return 6 }
if str_eq(gender, "f") { return 2 }
return 1
}
// third person
if str_eq(number, "plural") { return 7 }
if str_eq(gender, "f") { return 4 }
return 3
}
// Suffix pronouns
//
// Egyptian suffix pronouns attach to verbs, nouns, and prepositions.
// Written with = before the pronoun in transliteration (e.g. =f = "his / he").
//
// Standard Middle Egyptian paradigm:
// 1sg: =i ("I / me / my")
// 2sg m: =k ("you / your" masc)
// 2sg f: =T ("you / your" fem, in classical)
// 3sg m: =f ("he / him / his")
// 3sg f: =s ("she / her")
// 1pl: =n ("we / us / our")
// 2pl: =Tn ("you all / your" plural)
// 3pl: =sn ("they / them / their")
// dual: =sny (simplified dual rare in Middle Egyptian texts)
fn egy_conjugate_pronoun(person: String, number: String) -> String {
let slot: Int = egy_slot(person, number)
if slot == 0 { return "=i" }
if slot == 1 { return "=k" }
if slot == 5 { return "=n" }
if slot == 6 { return "=Tn" }
if slot == 7 { return "=sn" }
if slot == 8 { return "=sny" }
// slots 24 need gender; default to masc for slot 3
return "=f"
}
fn egy_suffix_pronoun(slot: Int) -> String {
if slot == 0 { return "=i" }
if slot == 1 { return "=k" }
if slot == 2 { return "=T" }
if slot == 3 { return "=f" }
if slot == 4 { return "=s" }
if slot == 5 { return "=n" }
if slot == 6 { return "=Tn" }
if slot == 7 { return "=sn" }
// dual (slot 8)
return "=sny"
}
// Copula detection
//
// In Middle Egyptian the verb "to be" as predicate is often omitted in the
// present (zero copula for adjective predicates). The auxiliary wnn is used
// for existence / substantive "to be" and in subordinate clauses.
// Canonical English label "be" maps to zero copula in the present.
fn egy_is_copula(verb: String) -> Bool {
if str_eq(verb, "wnn") { return true }
if str_eq(verb, "be") { return true }
return false
}
// Copula conjugation
//
// Present ("imperfective"): zero copula for adjectival predicate return "".
// The auxiliary iw...wnn is used in certain syntactic environments but the
// bare zero is the canonical Middle Egyptian form.
// Past ("perfective"): wnn.n (with perfective suffix .n)
// Future ("prospective"): wnn.xr (prospective form, simplified)
fn egy_conjugate_copula(tense: String, slot: Int) -> String {
if str_eq(tense, "present") { return "" }
if str_eq(tense, "past") {
return "wnn.n" + egy_suffix_pronoun(slot)
}
if str_eq(tense, "future") {
return "wnn.xr" + egy_suffix_pronoun(slot)
}
if str_eq(tense, "infinitive") { return "wnn" }
// Default: zero copula
return ""
}
// Irregular verb: rdi / di (to give)
//
// rdi is the full form; di is the common abbreviated written form.
// Imperfective (present): di=f (3sg m), with full pronoun for other persons.
// Perfective (past): di.n=f
// Prospective (future): di.xr=f
// Infinitive: rdi
fn egy_rdi_present(slot: Int) -> String {
return "di" + egy_suffix_pronoun(slot)
}
fn egy_rdi_past(slot: Int) -> String {
return "di.n" + egy_suffix_pronoun(slot)
}
fn egy_rdi_future(slot: Int) -> String {
return "di.xr" + egy_suffix_pronoun(slot)
}
// Irregular verb: mAA (to see)
//
// mAA is a geminated root (m-AA).
// Present: mAA=f; Past: mAA.n=f; Future: mAA.xr=f
fn egy_mAA_present(slot: Int) -> String {
return "mAA" + egy_suffix_pronoun(slot)
}
fn egy_mAA_past(slot: Int) -> String {
return "mAA.n" + egy_suffix_pronoun(slot)
}
fn egy_mAA_future(slot: Int) -> String {
return "mAA.xr" + egy_suffix_pronoun(slot)
}
// Irregular verb: Dd (to say)
//
// Present: Dd=f; Past: Dd.n=f; Future: Dd.xr=f
// Infinitive: Dd
fn egy_Dd_present(slot: Int) -> String {
return "Dd" + egy_suffix_pronoun(slot)
}
fn egy_Dd_past(slot: Int) -> String {
return "Dd.n" + egy_suffix_pronoun(slot)
}
fn egy_Dd_future(slot: Int) -> String {
return "Dd.xr" + egy_suffix_pronoun(slot)
}
// Irregular verb: Sm (to go)
//
// Present: Sm=f; Past: Sm.n=f; Future: Sm.xr=f
// (Note: the verb Smt "to go" appears in texts; Sm is the most common short form.)
fn egy_Sm_present(slot: Int) -> String {
return "Sm" + egy_suffix_pronoun(slot)
}
fn egy_Sm_past(slot: Int) -> String {
return "Sm.n" + egy_suffix_pronoun(slot)
}
fn egy_Sm_future(slot: Int) -> String {
return "Sm.xr" + egy_suffix_pronoun(slot)
}
// Irregular verb: iri (to do / make)
//
// iri has a contracted 3-radical stem ir- before pronouns.
// Present: ir=f; Past: ir.n=f; Future: ir.xr=f
// Infinitive: iri
fn egy_iri_present(slot: Int) -> String {
return "ir" + egy_suffix_pronoun(slot)
}
fn egy_iri_past(slot: Int) -> String {
return "ir.n" + egy_suffix_pronoun(slot)
}
fn egy_iri_future(slot: Int) -> String {
return "ir.xr" + egy_suffix_pronoun(slot)
}
// Regular verb: sdm (to hear)
//
// sdm (to hear) is the paradigm verb used in grammar textbooks to illustrate
// all Egyptian verb forms. The sdm=f construction names the imperfective suffix
// verb pattern itself.
// Present: sdm=f; Past: sdm.n=f; Future: sdm.xr=f
// Infinitive: sdm
fn egy_sdm_present(slot: Int) -> String {
return "sdm" + egy_suffix_pronoun(slot)
}
fn egy_sdm_past(slot: Int) -> String {
return "sdm.n" + egy_suffix_pronoun(slot)
}
fn egy_sdm_future(slot: Int) -> String {
return "sdm.xr" + egy_suffix_pronoun(slot)
}
// Known-verb dispatcher
//
// Returns the inflected form for a known verb, or "" if unknown.
// Accepts both canonical English labels and Egyptian transliterations.
fn egy_known_verb(verb: String, tense: String, slot: Int) -> String {
// rdi / di to give
if str_eq(verb, "rdi") {
if str_eq(tense, "present") { return egy_rdi_present(slot) }
if str_eq(tense, "past") { return egy_rdi_past(slot) }
if str_eq(tense, "future") { return egy_rdi_future(slot) }
if str_eq(tense, "infinitive") { return "rdi" }
return egy_rdi_present(slot)
}
if str_eq(verb, "di") {
if str_eq(tense, "present") { return egy_rdi_present(slot) }
if str_eq(tense, "past") { return egy_rdi_past(slot) }
if str_eq(tense, "future") { return egy_rdi_future(slot) }
if str_eq(tense, "infinitive") { return "rdi" }
return egy_rdi_present(slot)
}
if str_eq(verb, "give") {
if str_eq(tense, "present") { return egy_rdi_present(slot) }
if str_eq(tense, "past") { return egy_rdi_past(slot) }
if str_eq(tense, "future") { return egy_rdi_future(slot) }
if str_eq(tense, "infinitive") { return "rdi" }
return egy_rdi_present(slot)
}
// mAA to see
if str_eq(verb, "mAA") {
if str_eq(tense, "present") { return egy_mAA_present(slot) }
if str_eq(tense, "past") { return egy_mAA_past(slot) }
if str_eq(tense, "future") { return egy_mAA_future(slot) }
if str_eq(tense, "infinitive") { return "mAA" }
return egy_mAA_present(slot)
}
if str_eq(verb, "see") {
if str_eq(tense, "present") { return egy_mAA_present(slot) }
if str_eq(tense, "past") { return egy_mAA_past(slot) }
if str_eq(tense, "future") { return egy_mAA_future(slot) }
if str_eq(tense, "infinitive") { return "mAA" }
return egy_mAA_present(slot)
}
// Dd to say
if str_eq(verb, "Dd") {
if str_eq(tense, "present") { return egy_Dd_present(slot) }
if str_eq(tense, "past") { return egy_Dd_past(slot) }
if str_eq(tense, "future") { return egy_Dd_future(slot) }
if str_eq(tense, "infinitive") { return "Dd" }
return egy_Dd_present(slot)
}
if str_eq(verb, "say") {
if str_eq(tense, "present") { return egy_Dd_present(slot) }
if str_eq(tense, "past") { return egy_Dd_past(slot) }
if str_eq(tense, "future") { return egy_Dd_future(slot) }
if str_eq(tense, "infinitive") { return "Dd" }
return egy_Dd_present(slot)
}
// Sm to go
if str_eq(verb, "Sm") {
if str_eq(tense, "present") { return egy_Sm_present(slot) }
if str_eq(tense, "past") { return egy_Sm_past(slot) }
if str_eq(tense, "future") { return egy_Sm_future(slot) }
if str_eq(tense, "infinitive") { return "Sm" }
return egy_Sm_present(slot)
}
if str_eq(verb, "go") {
if str_eq(tense, "present") { return egy_Sm_present(slot) }
if str_eq(tense, "past") { return egy_Sm_past(slot) }
if str_eq(tense, "future") { return egy_Sm_future(slot) }
if str_eq(tense, "infinitive") { return "Sm" }
return egy_Sm_present(slot)
}
// iri to do / make
if str_eq(verb, "iri") {
if str_eq(tense, "present") { return egy_iri_present(slot) }
if str_eq(tense, "past") { return egy_iri_past(slot) }
if str_eq(tense, "future") { return egy_iri_future(slot) }
if str_eq(tense, "infinitive") { return "iri" }
return egy_iri_present(slot)
}
if str_eq(verb, "do") {
if str_eq(tense, "present") { return egy_iri_present(slot) }
if str_eq(tense, "past") { return egy_iri_past(slot) }
if str_eq(tense, "future") { return egy_iri_future(slot) }
if str_eq(tense, "infinitive") { return "iri" }
return egy_iri_present(slot)
}
if str_eq(verb, "make") {
if str_eq(tense, "present") { return egy_iri_present(slot) }
if str_eq(tense, "past") { return egy_iri_past(slot) }
if str_eq(tense, "future") { return egy_iri_future(slot) }
if str_eq(tense, "infinitive") { return "iri" }
return egy_iri_present(slot)
}
// sdm to hear
if str_eq(verb, "sdm") {
if str_eq(tense, "present") { return egy_sdm_present(slot) }
if str_eq(tense, "past") { return egy_sdm_past(slot) }
if str_eq(tense, "future") { return egy_sdm_future(slot) }
if str_eq(tense, "infinitive") { return "sdm" }
return egy_sdm_present(slot)
}
if str_eq(verb, "hear") {
if str_eq(tense, "present") { return egy_sdm_present(slot) }
if str_eq(tense, "past") { return egy_sdm_past(slot) }
if str_eq(tense, "future") { return egy_sdm_future(slot) }
if str_eq(tense, "infinitive") { return "sdm" }
return egy_sdm_present(slot)
}
// Verb not in table
return ""
}
// Regular verb conjugation
//
// For verbs not in the explicit table, apply the productive suffix-verb pattern:
// Present (imperfective sdm=f): stem + pronoun suffix
// Past (perfective sdm.n=f): stem + ".n" + pronoun suffix
// Future (prospective sdm.xr=f): stem + ".xr" + pronoun suffix
// Infinitive: stem unchanged
//
// This covers the vast majority of strong (sound) verb roots.
fn egy_regular_present(stem: String, slot: Int) -> String {
return stem + egy_suffix_pronoun(slot)
}
fn egy_regular_past(stem: String, slot: Int) -> String {
return stem + ".n" + egy_suffix_pronoun(slot)
}
fn egy_regular_future(stem: String, slot: Int) -> String {
return stem + ".xr" + egy_suffix_pronoun(slot)
}
// egy_conjugate: main conjugation entry point
//
// verb: Egyptian verb (ASCII transliteration) or English canonical label
// tense: "present" | "past" | "future" | "infinitive"
// person: "first" | "second" | "third"
// number: "singular" | "dual" | "plural"
//
// Returns:
// - "" for present copula (zero copula caller omits the verb)
// - inflected form (stem + .n + suffix, etc.) for all other cases
// - verb + regular suffix for unknown verbs (productive fallback)
fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = egy_slot(person, number)
// Handle copula (wnn / "be")
if egy_is_copula(verb) {
return egy_conjugate_copula(tense, slot)
}
// Try the known-verb table
let known: String = egy_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
// Infinitive: return unchanged
if str_eq(tense, "infinitive") { return verb }
// Regular verb: apply productive sdm=f / sdm.n=f pattern
if str_eq(tense, "present") { return egy_regular_present(verb, slot) }
if str_eq(tense, "past") { return egy_regular_past(verb, slot) }
if str_eq(tense, "future") { return egy_regular_future(verb, slot) }
// Unknown tense: return verb unchanged as safe fallback
return verb
}
// Noun number marking
//
// Middle Egyptian nouns are invariant for case syntactic role is expressed by
// word order and prepositions, not noun endings. Number is marked by suffix:
//
// Singular: base form (no suffix)
// Dual: masc + wy / fem + ty (wy and ty in ASCII transliteration)
// Plural: masc + w / fem + wt
//
// Many common nouns have suppletive or irregular plurals (recorded in the
// vocabulary layer). This function implements the productive regular pattern.
//
// gram_case: accepted for API symmetry but has no effect (Egyptian is caseless).
fn egy_decline(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") { return noun }
if str_eq(number, "dual") {
// Feminine dual: if noun ends in t (feminine marker), replace with ty
if egy_str_ends(noun, "t") {
let stem: String = egy_drop(noun, 1)
return stem + "ty"
}
return noun + "wy"
}
// Plural
if egy_str_ends(noun, "t") {
// Feminine noun: add wt
return noun + "wt"
}
// Masculine noun: add w
return noun + "w"
}
// Feminine derivation
//
// egy_fem: derive the feminine form of a noun or adjective by appending -t.
//
// In Middle Egyptian, the feminine gender marker is the suffix -t (written with
// the bread-loaf hieroglyph, Gardiner X1). If the base already ends in -t the
// form is returned unchanged to avoid double-suffixing.
fn egy_fem(noun: String) -> String {
if egy_str_ends(noun, "t") { return noun }
return noun + "t"
}
// Noun phrase assembly
//
// egy_noun_phrase: return the surface form of a noun phrase.
//
// noun: base noun (ASCII transliteration)
// gram_case: passed for API symmetry; has no effect (Egyptian is caseless)
// number: "singular" | "dual" | "plural"
// definite: "true" | "false" Middle Egyptian has no article; parameter accepted
// for API symmetry. Late Egyptian pꜣ/tꜣ/nꜣ articles are not implemented
// here (they would require knowing the gender of each noun).
//
// Returns the noun in its correct number form.
fn egy_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return egy_decline(noun, gram_case, number)
}
// Canonical verb mapping
//
// egy_map_canonical: map cross-lingual English canonical verb labels to their
// Middle Egyptian equivalents before dispatching to egy_conjugate.
fn egy_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wnn" }
if str_eq(verb, "give") { return "rdi" }
if str_eq(verb, "see") { return "mAA" }
if str_eq(verb, "say") { return "Dd" }
if str_eq(verb, "go") { return "Sm" }
if str_eq(verb, "do") { return "iri" }
if str_eq(verb, "make") { return "iri" }
if str_eq(verb, "hear") { return "sdm" }
// Unknown: return as-is; egy_conjugate will apply the regular pattern
return verb
}
+38
View File
@@ -0,0 +1,38 @@
// auto-generated by elc --emit-header — do not edit
extern fn egy_str_ends(s: String, suf: String) -> Bool
extern fn egy_str_len(s: String) -> Int
extern fn egy_drop(s: String, n: Int) -> String
extern fn egy_last_char(s: String) -> String
extern fn egy_slot(person: String, number: String) -> Int
extern fn egy_slot_with_gender(person: String, gender: String, number: String) -> Int
extern fn egy_conjugate_pronoun(person: String, number: String) -> String
extern fn egy_suffix_pronoun(slot: Int) -> String
extern fn egy_is_copula(verb: String) -> Bool
extern fn egy_conjugate_copula(tense: String, slot: Int) -> String
extern fn egy_rdi_present(slot: Int) -> String
extern fn egy_rdi_past(slot: Int) -> String
extern fn egy_rdi_future(slot: Int) -> String
extern fn egy_mAA_present(slot: Int) -> String
extern fn egy_mAA_past(slot: Int) -> String
extern fn egy_mAA_future(slot: Int) -> String
extern fn egy_Dd_present(slot: Int) -> String
extern fn egy_Dd_past(slot: Int) -> String
extern fn egy_Dd_future(slot: Int) -> String
extern fn egy_Sm_present(slot: Int) -> String
extern fn egy_Sm_past(slot: Int) -> String
extern fn egy_Sm_future(slot: Int) -> String
extern fn egy_iri_present(slot: Int) -> String
extern fn egy_iri_past(slot: Int) -> String
extern fn egy_iri_future(slot: Int) -> String
extern fn egy_sdm_present(slot: Int) -> String
extern fn egy_sdm_past(slot: Int) -> String
extern fn egy_sdm_future(slot: Int) -> String
extern fn egy_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn egy_regular_present(stem: String, slot: Int) -> String
extern fn egy_regular_past(stem: String, slot: Int) -> String
extern fn egy_regular_future(stem: String, slot: Int) -> String
extern fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn egy_decline(noun: String, gram_case: String, number: String) -> String
extern fn egy_fem(noun: String) -> String
extern fn egy_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn egy_map_canonical(verb: String) -> String
+451
View File
@@ -0,0 +1,451 @@
// morphology-enm.el - Middle English morphology for the NLG engine.
//
// Implements Middle English verb conjugation and noun declension for the
// ca. 1100-1500 CE period (Chaucerian English). Designed as a companion to
// morphology.el and called by the engine when the language profile code is "enm".
//
// Language profile: code=enm, name=Middle English, morph_type=analytic,
// word_order=SVO, question_strategy=inversion, script=latin, family=germanic.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third x singular/plural (slots 0-5)
// Classes: weak (productive: -est 2sg, -eth 3sg, -en pl; past: -ede/-de/-te)
// Irregulars: been/ben (be), han/haven (have), goon (go), seen (see),
// seyn/seyen (say), comen (come), maken (make)
// Canonical map: "be" -> "been"
//
// Noun declension covered:
// Middle English has largely lost case endings. This module handles:
// - nominative (base form)
// - genitive singular (+es)
// - plural (+es default; irregular forms for common words)
// Common irregulars: man->men, child->children, ox->oxen, foot->feet,
// tooth->teeth
//
// Article formation:
// Definite: "the" prepended
// Indefinite: "a" or "an" based on the first letter of the noun phrase
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn enm_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn enm_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn enm_first_char(s: String) -> String {
if str_len(s) == 0 { return "" }
return str_slice(s, 0, 1)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular (I / ich)
// 1 = 2nd singular (thou)
// 2 = 3rd singular (he / she / it)
// 3 = 1st plural (we)
// 4 = 2nd plural (ye)
// 5 = 3rd plural (they)
fn enm_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Irregular verb tables
//
// Each irregular verb has a present and past paradigm of six forms (slots 0-5).
// Forms are in Middle English spelling, close to Chaucerian usage.
fn enm_been_present(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "art" }
if slot == 2 { return "is" }
if slot == 3 { return "aren" }
if slot == 4 { return "been" }
return "been"
}
fn enm_been_past(slot: Int) -> String {
if slot == 0 { return "was" }
if slot == 1 { return "were" }
if slot == 2 { return "was" }
if slot == 3 { return "were" }
if slot == 4 { return "were" }
return "were"
}
fn enm_haven_present(slot: Int) -> String {
if slot == 0 { return "have" }
if slot == 1 { return "hast" }
if slot == 2 { return "hath" }
if slot == 3 { return "have" }
if slot == 4 { return "have" }
return "have"
}
fn enm_haven_past(slot: Int) -> String {
if slot == 0 { return "hadde" }
if slot == 1 { return "haddest" }
if slot == 2 { return "hadde" }
if slot == 3 { return "hadden" }
if slot == 4 { return "hadden" }
return "hadden"
}
fn enm_goon_present(slot: Int) -> String {
if slot == 0 { return "go" }
if slot == 1 { return "goost" }
if slot == 2 { return "gooth" }
if slot == 3 { return "goon" }
if slot == 4 { return "goon" }
return "goon"
}
fn enm_goon_past(slot: Int) -> String {
if slot == 0 { return "wente" }
if slot == 1 { return "wentest" }
if slot == 2 { return "wente" }
if slot == 3 { return "wenten" }
if slot == 4 { return "wenten" }
return "wenten"
}
fn enm_seen_present(slot: Int) -> String {
if slot == 0 { return "see" }
if slot == 1 { return "seest" }
if slot == 2 { return "seeth" }
if slot == 3 { return "seen" }
if slot == 4 { return "seen" }
return "seen"
}
fn enm_seen_past(slot: Int) -> String {
if slot == 0 { return "saugh" }
if slot == 1 { return "sawest" }
if slot == 2 { return "saugh" }
if slot == 3 { return "sawen" }
if slot == 4 { return "sawen" }
return "sawen"
}
fn enm_seyen_present(slot: Int) -> String {
if slot == 0 { return "seye" }
if slot == 1 { return "seyst" }
if slot == 2 { return "seith" }
if slot == 3 { return "seyen" }
if slot == 4 { return "seyen" }
return "seyen"
}
fn enm_seyen_past(slot: Int) -> String {
if slot == 0 { return "seide" }
if slot == 1 { return "seidest" }
if slot == 2 { return "seide" }
if slot == 3 { return "seiden" }
if slot == 4 { return "seiden" }
return "seiden"
}
fn enm_comen_present(slot: Int) -> String {
if slot == 0 { return "come" }
if slot == 1 { return "comest" }
if slot == 2 { return "cometh" }
if slot == 3 { return "comen" }
if slot == 4 { return "comen" }
return "comen"
}
fn enm_comen_past(slot: Int) -> String {
if slot == 0 { return "cam" }
if slot == 1 { return "come" }
if slot == 2 { return "cam" }
if slot == 3 { return "comen" }
if slot == 4 { return "comen" }
return "comen"
}
fn enm_maken_present(slot: Int) -> String {
if slot == 0 { return "make" }
if slot == 1 { return "makest" }
if slot == 2 { return "maketh" }
if slot == 3 { return "maken" }
if slot == 4 { return "maken" }
return "maken"
}
fn enm_maken_past(slot: Int) -> String {
if slot == 0 { return "made" }
if slot == 1 { return "madest" }
if slot == 2 { return "made" }
if slot == 3 { return "maden" }
if slot == 4 { return "maden" }
return "maden"
}
// Canonical verb mapping
//
// Maps English semantic labels to Middle English infinitives so the semantic
// layer can request forms without knowing the target-language lexeme.
fn enm_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "been" }
if str_eq(verb, "have") { return "haven" }
if str_eq(verb, "go") { return "goon" }
if str_eq(verb, "see") { return "seen" }
if str_eq(verb, "say") { return "seyen" }
if str_eq(verb, "come") { return "comen" }
if str_eq(verb, "make") { return "maken" }
return verb
}
// Weak verb stem derivation
//
// For weak verbs the present stem is the infinitive minus any trailing -en or -e.
// Past tense suffix: most common is -ede (after unvoiced consonants often -te,
// after voiced -de). This module uses -ede as the default productive past suffix.
//
// Present:
// slot 0: stem (I love)
// slot 1: stem + est (thou lovest)
// slot 2: stem + eth (he loveth)
// slot 3: stem + en (we loven)
// slot 4: stem + en (ye loven)
// slot 5: stem + en (they loven)
//
// Past:
// slot 0: stem + ede (I lovede)
// slot 1: stem + edest (thou lovedest)
// slot 2: stem + ede (he lovede)
// slot 3..5: stem + eden (we loveden)
fn enm_weak_stem(verb: String) -> String {
if enm_str_ends(verb, "en") { return enm_drop(verb, 2) }
if enm_str_ends(verb, "e") { return enm_drop(verb, 1) }
return verb
}
fn enm_weak_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "e" }
if slot == 1 { return stem + "est" }
if slot == 2 { return stem + "eth" }
if slot == 3 { return stem + "en" }
if slot == 4 { return stem + "en" }
return stem + "en"
}
fn enm_weak_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "ede" }
if slot == 1 { return stem + "edest" }
if slot == 2 { return stem + "ede" }
if slot == 3 { return stem + "eden" }
if slot == 4 { return stem + "eden" }
return stem + "eden"
}
// enm_conjugate: main conjugation entry point
//
// verb: Middle English infinitive (e.g. "loven", "been") or English canonical
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive rather
// than crashing.
fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = enm_map_canonical(verb)
let slot: Int = enm_slot(person, number)
// Irregulars
if str_eq(v, "been") {
if str_eq(tense, "present") { return enm_been_present(slot) }
if str_eq(tense, "past") { return enm_been_past(slot) }
return v
}
if str_eq(v, "haven") {
if str_eq(tense, "present") { return enm_haven_present(slot) }
if str_eq(tense, "past") { return enm_haven_past(slot) }
return v
}
if str_eq(v, "goon") {
if str_eq(tense, "present") { return enm_goon_present(slot) }
if str_eq(tense, "past") { return enm_goon_past(slot) }
return v
}
if str_eq(v, "seen") {
if str_eq(tense, "present") { return enm_seen_present(slot) }
if str_eq(tense, "past") { return enm_seen_past(slot) }
return v
}
if str_eq(v, "seyen") {
if str_eq(tense, "present") { return enm_seyen_present(slot) }
if str_eq(tense, "past") { return enm_seyen_past(slot) }
return v
}
if str_eq(v, "comen") {
if str_eq(tense, "present") { return enm_comen_present(slot) }
if str_eq(tense, "past") { return enm_comen_past(slot) }
return v
}
if str_eq(v, "maken") {
if str_eq(tense, "present") { return enm_maken_present(slot) }
if str_eq(tense, "past") { return enm_maken_past(slot) }
return v
}
// Regular weak verb
let stem: String = enm_weak_stem(v)
if str_eq(tense, "present") { return enm_weak_present(stem, slot) }
if str_eq(tense, "past") { return enm_weak_past(stem, slot) }
// Unknown tense: return infinitive unchanged
return v
}
// Noun plural irregulars
//
// Returns the suppletive plural form for nouns with non-productive plurals, or ""
// if the noun takes the regular -es plural.
//
// Covered: man, woman, child, ox, foot, tooth, goose, mouse, louse
// These mirror patterns still visible in Modern English, present in ME too.
fn enm_irregular_plural(noun: String) -> String {
if str_eq(noun, "man") { return "men" }
if str_eq(noun, "woman") { return "wommen" }
if str_eq(noun, "child") { return "children" }
if str_eq(noun, "ox") { return "oxen" }
if str_eq(noun, "foot") { return "feet" }
if str_eq(noun, "tooth") { return "teeth" }
if str_eq(noun, "goose") { return "gees" }
if str_eq(noun, "mouse") { return "mees" }
if str_eq(noun, "louse") { return "lees" }
return ""
}
// Regular plural formation
//
// Default: append -es. For nouns already ending in -e, append just -s.
// For nouns ending in -s, -x, -sh, -ch: the -es is still appropriate but
// in ME spelling the forms vary; we use the simple +es rule uniformly.
fn enm_make_plural(noun: String) -> String {
// Check suppletive irregular first
let irreg: String = enm_irregular_plural(noun)
if !str_eq(irreg, "") { return irreg }
// Noun ends in -e: just add -s to avoid double vowel
if enm_str_ends(noun, "e") { return noun + "s" }
// Default: +es
return noun + "es"
}
// enm_decline: main declension entry point
//
// Middle English has largely lost case morphology. This function handles the
// three practically relevant categories:
// nominative base form (used also for accusative and dative)
// genitive base form + es (possessive)
// plural irregular or base + es
//
// noun: base nominative form (e.g. "knyght", "man", "lond")
// gram_case: "nominative" | "accusative" | "dative" | "genitive" | "plural"
// ("accusative" and "dative" return the nominative form)
// number: "singular" | "plural"
//
// Returns the inflected form.
fn enm_decline(noun: String, gram_case: String, number: String) -> String {
// Plural number overrides gram_case for the plural form
if str_eq(number, "plural") {
return enm_make_plural(noun)
}
// Singular
if str_eq(gram_case, "genitive") {
// Genitive singular: +es (even after -e: "the kinges court")
return noun + "es"
}
// Nominative, accusative, dative all the same in ME
return noun
}
// Article selection
//
// Middle English uses "the" (definite) and "a" / "an" (indefinite).
// The indefinite article is "an" before a vowel-initial word, "a" otherwise.
// Vowel check is on the first character of the noun phrase word.
fn enm_is_vowel_initial(s: String) -> Bool {
let c: String = enm_first_char(s)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "i") { return true }
if str_eq(c, "o") { return true }
if str_eq(c, "u") { return true }
// ME also treated initial h as effectively silent in some dialects;
// we conservatively treat h-initial as consonant-initial.
return false
}
fn enm_indef_article(noun_phrase: String) -> String {
if enm_is_vowel_initial(noun_phrase) { return "an" }
return "a"
}
// enm_noun_phrase: noun phrase builder
//
// Constructs a full noun phrase with the appropriate article.
//
// noun: base nominative singular form (e.g. "knyght", "man", "lond")
// gram_case: "nominative" | "accusative" | "dative" | "genitive" | "plural"
// number: "singular" | "plural"
// definite: "true" | "false" (string comparison)
//
// Returns the complete noun phrase string (article + declined noun).
fn enm_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let form: String = enm_decline(noun, gram_case, number)
if str_eq(definite, "true") {
return "the " + form
}
// Indefinite article only makes sense for singular; plural takes no article
if str_eq(number, "plural") {
return form
}
let art: String = enm_indef_article(form)
return art + " " + form
}
+30
View File
@@ -0,0 +1,30 @@
// auto-generated by elc --emit-header — do not edit
extern fn enm_str_ends(s: String, suf: String) -> Bool
extern fn enm_drop(s: String, n: Int) -> String
extern fn enm_first_char(s: String) -> String
extern fn enm_slot(person: String, number: String) -> Int
extern fn enm_been_present(slot: Int) -> String
extern fn enm_been_past(slot: Int) -> String
extern fn enm_haven_present(slot: Int) -> String
extern fn enm_haven_past(slot: Int) -> String
extern fn enm_goon_present(slot: Int) -> String
extern fn enm_goon_past(slot: Int) -> String
extern fn enm_seen_present(slot: Int) -> String
extern fn enm_seen_past(slot: Int) -> String
extern fn enm_seyen_present(slot: Int) -> String
extern fn enm_seyen_past(slot: Int) -> String
extern fn enm_comen_present(slot: Int) -> String
extern fn enm_comen_past(slot: Int) -> String
extern fn enm_maken_present(slot: Int) -> String
extern fn enm_maken_past(slot: Int) -> String
extern fn enm_map_canonical(verb: String) -> String
extern fn enm_weak_stem(verb: String) -> String
extern fn enm_weak_present(stem: String, slot: Int) -> String
extern fn enm_weak_past(stem: String, slot: Int) -> String
extern fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn enm_irregular_plural(noun: String) -> String
extern fn enm_make_plural(noun: String) -> String
extern fn enm_decline(noun: String, gram_case: String, number: String) -> String
extern fn enm_is_vowel_initial(s: String) -> Bool
extern fn enm_indef_article(noun_phrase: String) -> String
extern fn enm_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+716
View File
@@ -0,0 +1,716 @@
// morphology-es.el - Spanish morphology for the NLG engine.
//
// Implements fusional Spanish verb conjugation, noun pluralization, gender
// inference, and article agreement. Designed as a companion to morphology.el
// and called by the engine when the language profile code is "es".
//
// Verb tenses covered: present, preterite (past), future, imperfect.
// Persons: first/second/third × singular/plural (1s 2s 3s 1p 2p 3p).
// Verb classes: -ar, -er, -ir (regular) + a core set of common irregulars.
//
// Depends on: morphology.el (str_ends, str_drop_last, str_last_char, str_last2, str_last3, is_vowel)
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn es_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn es_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn es_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn es_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn es_str_last3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, n - 3, n)
}
// Verb class detection
//
// Spanish verbs fall into three conjugation classes defined by the infinitive
// ending: -ar, -er, -ir. The stem is the infinitive minus those two characters.
fn es_verb_class(base: String) -> String {
if es_str_ends(base, "ar") { return "ar" }
if es_str_ends(base, "er") { return "er" }
if es_str_ends(base, "ir") { return "ir" }
return "ar"
}
fn es_stem(base: String) -> String {
return es_str_drop_last(base, 2)
}
// Person/number index
//
// Maps person × number to a 0-based slot index used inside paradigm tables.
// 0 = 1s, 1 = 2s, 2 = 3s, 3 = 1p, 4 = 2p, 5 = 3p
fn es_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Irregular present tense
//
// Returns the fully-inflected form if the verb is irregular in the present
// tense for the given person/number slot, otherwise returns "".
//
// ser: soy, eres, es, somos, sois, son
// estar: estoy, estás, está, estamos, estáis, están
// tener: tengo, tienes, tiene, tenemos, tenéis, tienen
// hacer: hago, haces, hace, hacemos, hacéis, hacen
// ir: voy, vas, va, vamos, vais, van
// ver: veo, ves, ve, vemos, veis, ven
// dar: doy, das, da, damos, dais, dan
// saber: sé, sabes, sabe, sabemos, sabéis, saben
// poder: puedo, puedes, puede, podemos, podéis, pueden
// querer: quiero, quieres, quiere, queremos, queréis, quieren
// venir: vengo, vienes, viene, venimos, venís, vienen
// decir: digo, dices, dice, decimos, decís, dicen
// haber: he, has, ha, hemos, habéis, han
fn es_irregular_present(verb: String, person: String, number: String) -> String {
let slot: Int = es_slot(person, number)
if str_eq(verb, "ser") {
if slot == 0 { return "soy" }
if slot == 1 { return "eres" }
if slot == 2 { return "es" }
if slot == 3 { return "somos" }
if slot == 4 { return "sois" }
return "son"
}
if str_eq(verb, "estar") {
if slot == 0 { return "estoy" }
if slot == 1 { return "estás" }
if slot == 2 { return "está" }
if slot == 3 { return "estamos" }
if slot == 4 { return "estáis" }
return "están"
}
if str_eq(verb, "tener") {
if slot == 0 { return "tengo" }
if slot == 1 { return "tienes" }
if slot == 2 { return "tiene" }
if slot == 3 { return "tenemos" }
if slot == 4 { return "tenéis" }
return "tienen"
}
if str_eq(verb, "hacer") {
if slot == 0 { return "hago" }
if slot == 1 { return "haces" }
if slot == 2 { return "hace" }
if slot == 3 { return "hacemos" }
if slot == 4 { return "hacéis" }
return "hacen"
}
if str_eq(verb, "ir") {
if slot == 0 { return "voy" }
if slot == 1 { return "vas" }
if slot == 2 { return "va" }
if slot == 3 { return "vamos" }
if slot == 4 { return "vais" }
return "van"
}
if str_eq(verb, "ver") {
if slot == 0 { return "veo" }
if slot == 1 { return "ves" }
if slot == 2 { return "ve" }
if slot == 3 { return "vemos" }
if slot == 4 { return "veis" }
return "ven"
}
if str_eq(verb, "dar") {
if slot == 0 { return "doy" }
if slot == 1 { return "das" }
if slot == 2 { return "da" }
if slot == 3 { return "damos" }
if slot == 4 { return "dais" }
return "dan"
}
if str_eq(verb, "saber") {
if slot == 0 { return "" }
if slot == 1 { return "sabes" }
if slot == 2 { return "sabe" }
if slot == 3 { return "sabemos" }
if slot == 4 { return "sabéis" }
return "saben"
}
if str_eq(verb, "poder") {
if slot == 0 { return "puedo" }
if slot == 1 { return "puedes" }
if slot == 2 { return "puede" }
if slot == 3 { return "podemos" }
if slot == 4 { return "podéis" }
return "pueden"
}
if str_eq(verb, "querer") {
if slot == 0 { return "quiero" }
if slot == 1 { return "quieres" }
if slot == 2 { return "quiere" }
if slot == 3 { return "queremos" }
if slot == 4 { return "queréis" }
return "quieren"
}
if str_eq(verb, "venir") {
if slot == 0 { return "vengo" }
if slot == 1 { return "vienes" }
if slot == 2 { return "viene" }
if slot == 3 { return "venimos" }
if slot == 4 { return "venís" }
return "vienen"
}
if str_eq(verb, "decir") {
if slot == 0 { return "digo" }
if slot == 1 { return "dices" }
if slot == 2 { return "dice" }
if slot == 3 { return "decimos" }
if slot == 4 { return "decís" }
return "dicen"
}
if str_eq(verb, "haber") {
if slot == 0 { return "he" }
if slot == 1 { return "has" }
if slot == 2 { return "ha" }
if slot == 3 { return "hemos" }
if slot == 4 { return "habéis" }
return "han"
}
return ""
}
// Irregular preterite tense
//
// Returns the inflected preterite form for irregular verbs, or "" if regular.
//
// ser/ir (same preterite): fui, fuiste, fue, fuimos, fuisteis, fueron
// tener: tuve, tuviste, tuvo, tuvimos, tuvisteis, tuvieron
// hacer: hice, hiciste, hizo, hicimos, hicisteis, hicieron
// estar: estuve, estuviste, estuvo, estuvimos, estuvisteis, estuvieron
// dar: di, diste, dio, dimos, disteis, dieron
// saber: supe, supiste, supo, supimos, supisteis, supieron
// poder: pude, pudiste, pudo, pudimos, pudisteis, pudieron
// querer: quise, quisiste, quiso, quisimos, quisisteis, quisieron
// venir: vine, viniste, vino, vinimos, vinisteis, vinieron
// decir: dije, dijiste, dijo, dijimos, dijisteis, dijeron
// haber: hube, hubiste, hubo, hubimos, hubisteis, hubieron
// ver: vi, viste, vio, vimos, visteis, vieron
fn es_irregular_preterite(verb: String, person: String, number: String) -> String {
let slot: Int = es_slot(person, number)
if str_eq(verb, "ser") {
if slot == 0 { return "fui" }
if slot == 1 { return "fuiste" }
if slot == 2 { return "fue" }
if slot == 3 { return "fuimos" }
if slot == 4 { return "fuisteis" }
return "fueron"
}
if str_eq(verb, "ir") {
if slot == 0 { return "fui" }
if slot == 1 { return "fuiste" }
if slot == 2 { return "fue" }
if slot == 3 { return "fuimos" }
if slot == 4 { return "fuisteis" }
return "fueron"
}
if str_eq(verb, "tener") {
if slot == 0 { return "tuve" }
if slot == 1 { return "tuviste" }
if slot == 2 { return "tuvo" }
if slot == 3 { return "tuvimos" }
if slot == 4 { return "tuvisteis" }
return "tuvieron"
}
if str_eq(verb, "hacer") {
if slot == 0 { return "hice" }
if slot == 1 { return "hiciste" }
if slot == 2 { return "hizo" }
if slot == 3 { return "hicimos" }
if slot == 4 { return "hicisteis" }
return "hicieron"
}
if str_eq(verb, "estar") {
if slot == 0 { return "estuve" }
if slot == 1 { return "estuviste" }
if slot == 2 { return "estuvo" }
if slot == 3 { return "estuvimos" }
if slot == 4 { return "estuvisteis" }
return "estuvieron"
}
if str_eq(verb, "dar") {
if slot == 0 { return "di" }
if slot == 1 { return "diste" }
if slot == 2 { return "dio" }
if slot == 3 { return "dimos" }
if slot == 4 { return "disteis" }
return "dieron"
}
if str_eq(verb, "saber") {
if slot == 0 { return "supe" }
if slot == 1 { return "supiste" }
if slot == 2 { return "supo" }
if slot == 3 { return "supimos" }
if slot == 4 { return "supisteis" }
return "supieron"
}
if str_eq(verb, "poder") {
if slot == 0 { return "pude" }
if slot == 1 { return "pudiste" }
if slot == 2 { return "pudo" }
if slot == 3 { return "pudimos" }
if slot == 4 { return "pudisteis" }
return "pudieron"
}
if str_eq(verb, "querer") {
if slot == 0 { return "quise" }
if slot == 1 { return "quisiste" }
if slot == 2 { return "quiso" }
if slot == 3 { return "quisimos" }
if slot == 4 { return "quisisteis" }
return "quisieron"
}
if str_eq(verb, "venir") {
if slot == 0 { return "vine" }
if slot == 1 { return "viniste" }
if slot == 2 { return "vino" }
if slot == 3 { return "vinimos" }
if slot == 4 { return "vinisteis" }
return "vinieron"
}
if str_eq(verb, "decir") {
if slot == 0 { return "dije" }
if slot == 1 { return "dijiste" }
if slot == 2 { return "dijo" }
if slot == 3 { return "dijimos" }
if slot == 4 { return "dijisteis" }
return "dijeron"
}
if str_eq(verb, "haber") {
if slot == 0 { return "hube" }
if slot == 1 { return "hubiste" }
if slot == 2 { return "hubo" }
if slot == 3 { return "hubimos" }
if slot == 4 { return "hubisteis" }
return "hubieron"
}
if str_eq(verb, "ver") {
if slot == 0 { return "vi" }
if slot == 1 { return "viste" }
if slot == 2 { return "vio" }
if slot == 3 { return "vimos" }
if slot == 4 { return "visteis" }
return "vieron"
}
return ""
}
// Irregular imperfect tense
//
// Only three verbs are truly irregular in the imperfect:
// ser: era, eras, era, éramos, erais, eran
// ir: iba, ibas, iba, íbamos, ibais, iban
// ver: veía, veías, veía, veíamos, veíais, veían
fn es_irregular_imperfect(verb: String, person: String, number: String) -> String {
let slot: Int = es_slot(person, number)
if str_eq(verb, "ser") {
if slot == 0 { return "era" }
if slot == 1 { return "eras" }
if slot == 2 { return "era" }
if slot == 3 { return "éramos" }
if slot == 4 { return "erais" }
return "eran"
}
if str_eq(verb, "ir") {
if slot == 0 { return "iba" }
if slot == 1 { return "ibas" }
if slot == 2 { return "iba" }
if slot == 3 { return "íbamos" }
if slot == 4 { return "ibais" }
return "iban"
}
if str_eq(verb, "ver") {
if slot == 0 { return "veía" }
if slot == 1 { return "veías" }
if slot == 2 { return "veía" }
if slot == 3 { return "veíamos" }
if slot == 4 { return "veíais" }
return "veían"
}
return ""
}
// Regular present conjugation
//
// -ar: -o, -as, -a, -amos, -áis, -an
// -er: -o, -es, -e, -emos, -éis, -en
// -ir: -o, -es, -e, -imos, -ís, -en
fn es_regular_present(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "ar") {
if slot == 0 { return stem + "o" }
if slot == 1 { return stem + "as" }
if slot == 2 { return stem + "a" }
if slot == 3 { return stem + "amos" }
if slot == 4 { return stem + "áis" }
return stem + "an"
}
if str_eq(vclass, "er") {
if slot == 0 { return stem + "o" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "emos" }
if slot == 4 { return stem + "éis" }
return stem + "en"
}
// -ir
if slot == 0 { return stem + "o" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "imos" }
if slot == 4 { return stem + "ís" }
return stem + "en"
}
// Regular preterite conjugation
//
// -ar: -é, -aste, -ó, -amos, -asteis, -aron
// -er: -í, -iste, -ió, -imos, -isteis, -ieron
// -ir: -í, -iste, -ió, -imos, -isteis, -ieron
fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "ar") {
if slot == 0 { return stem + "é" }
if slot == 1 { return stem + "aste" }
if slot == 2 { return stem + "ó" }
if slot == 3 { return stem + "amos" }
if slot == 4 { return stem + "asteis" }
return stem + "aron"
}
// -er and -ir share the same preterite endings
if slot == 0 { return stem + "í" }
if slot == 1 { return stem + "iste" }
if slot == 2 { return stem + "" }
if slot == 3 { return stem + "imos" }
if slot == 4 { return stem + "isteis" }
return stem + "ieron"
}
// Regular future conjugation
//
// Future is formed from the full infinitive + endings (all classes):
// -é, -ás, -á, -emos, -éis, -án
//
// No stem change; the infinitive is the future stem.
fn es_regular_future(base: String, slot: Int) -> String {
if slot == 0 { return base + "é" }
if slot == 1 { return base + "ás" }
if slot == 2 { return base + "á" }
if slot == 3 { return base + "emos" }
if slot == 4 { return base + "éis" }
return base + "án"
}
// Irregular future stems
//
// Some verbs contract or alter their infinitive for the future stem.
// Returns the irregular future stem, or "" if the verb uses the regular stem.
fn es_irregular_future_stem(verb: String) -> String {
if str_eq(verb, "tener") { return "tendr" }
if str_eq(verb, "hacer") { return "har" }
if str_eq(verb, "poder") { return "podr" }
if str_eq(verb, "querer") { return "querr" }
if str_eq(verb, "venir") { return "vendr" }
if str_eq(verb, "decir") { return "dir" }
if str_eq(verb, "haber") { return "habr" }
if str_eq(verb, "saber") { return "sabr" }
if str_eq(verb, "salir") { return "saldr" }
if str_eq(verb, "poner") { return "pondr" }
return ""
}
// Regular imperfect conjugation
//
// -ar: -aba, -abas, -aba, -ábamos, -abais, -aban
// -er/-ir: -ía, -ías, -ía, -íamos, -íais, -ían
fn es_regular_imperfect(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "ar") {
if slot == 0 { return stem + "aba" }
if slot == 1 { return stem + "abas" }
if slot == 2 { return stem + "aba" }
if slot == 3 { return stem + "ábamos" }
if slot == 4 { return stem + "abais" }
return stem + "aban"
}
// -er and -ir
if slot == 0 { return stem + "ía" }
if slot == 1 { return stem + "ías" }
if slot == 2 { return stem + "ía" }
if slot == 3 { return stem + "íamos" }
if slot == 4 { return stem + "íais" }
return stem + "ían"
}
// Full conjugation entry point
//
// es_conjugate: conjugate a Spanish verb.
//
// verb: Spanish infinitive (e.g. "hablar", "ser", "tener")
// tense: "present" | "past" | "future" | "imperfect"
// (note: "past" maps to the preterite/indefinite past)
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = es_slot(person, number)
if str_eq(tense, "present") {
let irreg: String = es_irregular_present(verb, person, number)
if !str_eq(irreg, "") {
return irreg
}
let vclass: String = es_verb_class(verb)
let stem: String = es_stem(verb)
return es_regular_present(stem, vclass, slot)
}
if str_eq(tense, "past") {
let irreg: String = es_irregular_preterite(verb, person, number)
if !str_eq(irreg, "") {
return irreg
}
let vclass: String = es_verb_class(verb)
let stem: String = es_stem(verb)
return es_regular_preterite(stem, vclass, slot)
}
if str_eq(tense, "future") {
let irreg_stem: String = es_irregular_future_stem(verb)
if !str_eq(irreg_stem, "") {
return es_regular_future(irreg_stem, slot)
}
return es_regular_future(verb, slot)
}
if str_eq(tense, "imperfect") {
let irreg: String = es_irregular_imperfect(verb, person, number)
if !str_eq(irreg, "") {
return irreg
}
let vclass: String = es_verb_class(verb)
let stem: String = es_stem(verb)
return es_regular_imperfect(stem, vclass, slot)
}
// Unknown tense: return infinitive unchanged
return verb
}
// Noun gender inference
//
// Returns "m" (masculine), "f" (feminine), or "unknown".
//
// Heuristics (not exhaustive cover most common patterns):
// ends in -o -> masculine (libro, gato, niño)
// ends in -a -> feminine (casa, mesa, niña)
// ends in -ión -> feminine (canción, nación)
// ends in -dad/-tad -> feminine (ciudad, libertad)
// ends in -umbre -> feminine (costumbre)
// ends in -sis -> feminine (crisis, tesis)
// ends in -ema/-ama -> masculine (problema, programa, tema, idioma)
// ends in -or -> masculine (color, amor, señor)
// ends in -aje -> masculine (viaje, paisaje)
// ends in -án/-ón -> masculine (avión check -ión first)
// otherwise -> unknown
fn es_gender(noun: String) -> String {
// -ión before -o so "avión" feminine (it ends -ión, not just -on)
if es_str_ends(noun, "ión") { return "f" }
if es_str_ends(noun, "dad") { return "f" }
if es_str_ends(noun, "tad") { return "f" }
if es_str_ends(noun, "umbre") { return "f" }
if es_str_ends(noun, "sis") { return "f" }
if es_str_ends(noun, "ema") { return "m" }
if es_str_ends(noun, "ama") { return "m" }
if es_str_ends(noun, "aje") { return "m" }
if es_str_ends(noun, "or") { return "m" }
if es_str_ends(noun, "o") { return "m" }
if es_str_ends(noun, "a") { return "f" }
return "unknown"
}
// Noun pluralization
//
// Rules (applied in order):
// ends in vowel (a e i o u) -> add -s
// ends in consonant -> add -es
// ends in -z -> replace -z with -ces
// ends in -s (unstressed) -> unchanged (e.g. "el lunes" -> "los lunes")
//
// Note: nouns ending in stressed vowel + s (e.g. "el autobús" "los autobuses")
// are handled by the consonant rule since -s is a consonant ending for pluralization
// purposes; but "el lunes" (days of week ending in -s) stay unchanged — this is
// an irregular class. The table below handles common invariant nouns.
fn es_invariant_plural(noun: String) -> String {
if str_eq(noun, "lunes") { return "lunes" }
if str_eq(noun, "martes") { return "martes" }
if str_eq(noun, "miércoles") { return "miércoles" }
if str_eq(noun, "jueves") { return "jueves" }
if str_eq(noun, "viernes") { return "viernes" }
if str_eq(noun, "crisis") { return "crisis" }
if str_eq(noun, "tesis") { return "tesis" }
if str_eq(noun, "análisis") { return "análisis" }
if str_eq(noun, "dosis") { return "dosis" }
if str_eq(noun, "virus") { return "virus" }
return ""
}
fn es_pluralize(noun: String) -> String {
let inv: String = es_invariant_plural(noun)
if !str_eq(inv, "") {
return inv
}
let last: String = es_str_last_char(noun)
// Ends in -z: replace with -ces
if str_eq(last, "z") {
return es_str_drop_last(noun, 1) + "ces"
}
// Ends in a vowel: add -s
if str_eq(last, "a") { return noun + "s" }
if str_eq(last, "e") { return noun + "s" }
if str_eq(last, "i") { return noun + "s" }
if str_eq(last, "o") { return noun + "s" }
if str_eq(last, "u") { return noun + "s" }
// Ends in consonant (including -s for stressed words like autobús): add -es
return noun + "es"
}
// Article agreement
//
// es_agree_article: return the correct Spanish article for a noun.
//
// noun: the noun (used for gender and number inference)
// definite: "true" for definite (el/la/los/las), "false" for indefinite (un/una/unos/unas)
// number: "singular" | "plural"
//
// Special case: feminine nouns beginning with stressed "a-" or "ha-" take
// masculine singular definite article: "el agua", "el hacha".
// This is handled by checking the noun's first character when gender is feminine.
fn es_starts_with_stressed_a(noun: String) -> Bool {
// Approximate: check if noun starts with "a" or "ha" (covers most cases)
// The accent on the first syllable is not detectable orthographically in
// general, so we apply the rule broadly for any feminine noun starting with
// "a" or "ha" in singular.
let n: Int = str_len(noun)
if n == 0 {
return false
}
let c0: String = str_slice(noun, 0, 1)
if str_eq(c0, "a") { return true }
if n >= 2 {
let c1: String = str_slice(noun, 1, 2)
if str_eq(c0, "h") {
if str_eq(c1, "a") { return true }
}
}
return false
}
fn es_agree_article(noun: String, definite: String, number: String) -> String {
let gender: String = es_gender(noun)
let is_plural: Bool = str_eq(number, "plural")
let is_def: Bool = str_eq(definite, "true")
if is_def {
if is_plural {
if str_eq(gender, "f") { return "las" }
return "los"
}
// singular
if str_eq(gender, "f") {
// el agua rule: feminine singular nouns starting with stressed "a"
if es_starts_with_stressed_a(noun) { return "el" }
return "la"
}
return "el"
}
// indefinite
if is_plural {
if str_eq(gender, "f") { return "unas" }
return "unos"
}
if str_eq(gender, "f") { return "una" }
return "un"
}
+23
View File
@@ -0,0 +1,23 @@
// auto-generated by elc --emit-header — do not edit
extern fn es_str_ends(s: String, suf: String) -> Bool
extern fn es_str_drop_last(s: String, n: Int) -> String
extern fn es_str_last_char(s: String) -> String
extern fn es_str_last2(s: String) -> String
extern fn es_str_last3(s: String) -> String
extern fn es_verb_class(base: String) -> String
extern fn es_stem(base: String) -> String
extern fn es_slot(person: String, number: String) -> Int
extern fn es_irregular_present(verb: String, person: String, number: String) -> String
extern fn es_irregular_preterite(verb: String, person: String, number: String) -> String
extern fn es_irregular_imperfect(verb: String, person: String, number: String) -> String
extern fn es_regular_present(stem: String, vclass: String, slot: Int) -> String
extern fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String
extern fn es_regular_future(base: String, slot: Int) -> String
extern fn es_irregular_future_stem(verb: String) -> String
extern fn es_regular_imperfect(stem: String, vclass: String, slot: Int) -> String
extern fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn es_gender(noun: String) -> String
extern fn es_invariant_plural(noun: String) -> String
extern fn es_pluralize(noun: String) -> String
extern fn es_starts_with_stressed_a(noun: String) -> Bool
extern fn es_agree_article(noun: String, definite: String, number: String) -> String
+623
View File
@@ -0,0 +1,623 @@
// morphology-fi.el - Finnish morphology: noun case inflection and verb conjugation.
//
// Finnish is SOV, agglutinative, 15 grammatical cases, no grammatical gender,
// no articles. Morphology is suffix-chain based.
//
// Key facts:
// - Vowel harmony: suffixes harmonize with the stem's vowel class.
// Back vowels (a, o, u) back-harmony suffixes (-ssa, -sta, -an, etc.)
// Front vowels (ä, ö, y) or neutral-only stems front-harmony (-ssä, -stä, -än, etc.)
// - Consonant gradation: alternation between strong and weak consonant grades.
// Full gradation tables are large; this module implements the most common
// patterns. Irregular/exceptional stems must be supplied in inflected form.
// - Verbs conjugate for person (1/2/3) and number (singular/plural), plus tense
// (present/past) and polarity (affirmative/negative).
// - Question suffix: -ko (back harmony) / -kö (front harmony), appended to verb.
//
// Depends on: (no dependencies - standalone morphology module)
// Vowel harmony
//
// fi_harmony(word) -> "back" | "front"
//
// Scan the word right-to-left for the last unambiguously back (a, o, u) or
// front (ä, ö, y) vowel. Neutral vowels (e, i) do not determine the class.
// If only neutral vowels are found, default to "front" (the conservative choice
// for borrowed words and those without clear back vowels).
import "morphology.el"
fn fi_harmony(word: String) -> String {
let n: Int = str_len(word)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(word, i, i + 1)
// Back vowels
if str_eq(c, "a") { return "back" }
if str_eq(c, "o") { return "back" }
if str_eq(c, "u") { return "back" }
// Front vowels (UTF-8; Finnish ä is U+00E4, ö is U+00F6)
// In a byte scan we may land mid-codepoint; we do a substring comparison
// by checking two-byte sequences at position i-1 when c is a lead byte.
if str_eq(c, "ä") { return "front" }
if str_eq(c, "ö") { return "front" }
if str_eq(c, "y") { return "front" }
let i = i - 1
}
// Default: front (covers neutral-only and unknown stems)
return "front"
}
// Suffix harmonization
//
// fi_suffix(base, harmony) -> String
//
// Given a back-harmony base suffix, return the harmonized form.
// Only the a/ä alternation is handled here; all other vowels stay the same.
// E.g. fi_suffix("ssa", "front") -> "ssä"
// fi_suffix("ssa", "back") -> "ssa"
fn fi_suffix(base: String, harmony: String) -> String {
if str_eq(harmony, "front") {
// Replace every 'a' with ' and every 'o' in suffix with '.
// We handle only the common patterns used in Finnish case suffixes.
if str_eq(base, "a") { return "ä" }
if str_eq(base, "ssa") { return "ssä" }
if str_eq(base, "sta") { return "stä" }
if str_eq(base, "an") { return "än" }
if str_eq(base, "aan") { return "ään" }
if str_eq(base, "lla") { return "llä" }
if str_eq(base, "lta") { return "ltä" }
if str_eq(base, "lle") { return "lle" }
if str_eq(base, "na") { return "" }
if str_eq(base, "ksi") { return "ksi" }
if str_eq(base, "tta") { return "ttä" }
if str_eq(base, "ta") { return "" }
if str_eq(base, "ja") { return "" }
if str_eq(base, "oja") { return "öjä" }
if str_eq(base, "issa") { return "issä" }
if str_eq(base, "ista") { return "istä" }
if str_eq(base, "ihin") { return "ihin" }
if str_eq(base, "illa") { return "illä" }
if str_eq(base, "ilta") { return "iltä" }
if str_eq(base, "ille") { return "ille" }
if str_eq(base, "ina") { return "inä" }
if str_eq(base, "itta") { return "ittä" }
if str_eq(base, "ko") { return "" }
if str_eq(base, "pa") { return "" }
if str_eq(base, "va") { return "" }
if str_eq(base, "ma") { return "" }
if str_eq(base, "han") { return "hän" }
if str_eq(base, "lla") { return "llä" }
// Fall back: return the back-harmony form unchanged
return base
}
// Back harmony: return as-is
return base
}
// Noun case inflection
//
// fi_noun_case(stem, gram_case, number, harmony) -> String
//
// Computes the inflected noun form from the oblique stem, case name, number
// ("singular" | "plural"), and vowel harmony class.
//
// The "stem" is the oblique/genitive stem (e.g. talo for talo, puhu for puhua).
// For most Type-1 nouns the oblique stem = dictionary form minus final vowel.
// For the singular nominative the dictionary form itself is used (passed directly).
//
// Cases and their suffixes (talo back, stem "talo"):
// nominative sg : stem (no suffix) talo
// nominative pl : stem + t talot
// genitive sg : stem + n talon
// genitive pl : stem + jen / jen talojen
// accusative sg : stem + n (= gen sg) talon
// accusative pl : stem + t (= nom pl) talot
// partitive sg : stem + a/ä taloa
// partitive pl : stem + ja/jä taloja
// inessive sg : stem + ssa/ssä talossa
// inessive pl : stem + issa/issä taloissa
// elative sg : stem + sta/stä talosta
// elative pl : stem + ista/istä taloista
// illative sg : stem + vowel + n taloon (long vowel + n)
// illative pl : stem + ihin taloihin
// adessive sg : stem + lla/llä talolla
// adessive pl : stem + illa/illä taloilla
// ablative sg : stem + lta/ltä talolta
// ablative pl : stem + ilta/iltä taloilta
// allative sg : stem + lle talolle
// allative pl : stem + ille taloille
// essive sg : stem + na/nä talona
// essive pl : stem + ina/inä taloina
// translative sg : stem + ksi taloksi
// translative pl : stem + iksi taloiksi
// instructive pl : stem + in taloin (plural only)
// abessive sg : stem + tta/ttä talotta
// abessive pl : stem + itta/ittä taloitta
// comitative pl : stem + ineen taloineen (plural only)
fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String {
let sg: Bool = str_eq(number, "singular")
if str_eq(gram_case, "nominative") {
if sg { return stem }
return stem + "t"
}
if str_eq(gram_case, "genitive") {
if sg { return stem + "n" }
return stem + "jen"
}
if str_eq(gram_case, "accusative") {
if sg { return stem + "n" }
return stem + "t"
}
if str_eq(gram_case, "partitive") {
if sg { return stem + fi_suffix("a", harmony) }
return stem + fi_suffix("ja", harmony)
}
if str_eq(gram_case, "inessive") {
if sg { return stem + fi_suffix("ssa", harmony) }
return stem + fi_suffix("issa", harmony)
}
if str_eq(gram_case, "elative") {
if sg { return stem + fi_suffix("sta", harmony) }
return stem + fi_suffix("ista", harmony)
}
if str_eq(gram_case, "illative") {
if sg {
// Singular illative: final vowel of stem is lengthened + n
// e.g. talo taloon, puu puuhun, käsi käteen
// We take the last character of the stem and double it, then add n.
let last: String = fi_str_last_char(stem)
return stem + last + "n"
}
return stem + fi_suffix("ihin", harmony)
}
if str_eq(gram_case, "adessive") {
if sg { return stem + fi_suffix("lla", harmony) }
return stem + fi_suffix("illa", harmony)
}
if str_eq(gram_case, "ablative") {
if sg { return stem + fi_suffix("lta", harmony) }
return stem + fi_suffix("ilta", harmony)
}
if str_eq(gram_case, "allative") {
// Allative is -lle for both numbers (only the stem differs)
if sg { return stem + "lle" }
return stem + "ille"
}
if str_eq(gram_case, "essive") {
if sg { return stem + fi_suffix("na", harmony) }
return stem + fi_suffix("ina", harmony)
}
if str_eq(gram_case, "translative") {
if sg { return stem + "ksi" }
return stem + "iksi"
}
if str_eq(gram_case, "instructive") {
// Instructive is plural only in modern Finnish
return stem + "in"
}
if str_eq(gram_case, "abessive") {
if sg { return stem + fi_suffix("tta", harmony) }
return stem + fi_suffix("itta", harmony)
}
if str_eq(gram_case, "comitative") {
// Comitative is plural only
return stem + "ineen"
}
// Unknown case: return stem unchanged
return stem
}
// Noun helper: str_last_char
//
// Return the last Unicode character of a string.
// Mirrors the helper in morphology.el; redefined here for standalone use.
fn fi_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Full noun inflection (convenience wrapper)
//
// fi_apply_case(noun, gram_case, number) -> String
//
// Accepts the nominative singular form (dictionary form), derives the harmony
// class, and produces the requested case form.
//
// For most regular nouns the oblique stem equals the dictionary form. The
// illative singular is handled by appending the last vowel + n.
fn fi_apply_case(noun: String, gram_case: String, number: String) -> String {
let harmony: String = fi_harmony(noun)
// For nominative singular, return the noun as-is.
if str_eq(gram_case, "nominative") {
if str_eq(number, "singular") { return noun }
return noun + "t"
}
// For all other cases, use the noun as the oblique stem.
// (Callers that need consonant-gradated stems must pass the graded stem
// directly via fi_noun_case.)
return fi_noun_case(noun, gram_case, number, harmony)
}
// Verb stem extraction
//
// fi_verb_stem(dict_form) -> String
//
// Strip the infinitive ending to get the present-tense stem.
//
// Type 1 verbs (most common): infinitive ends in -a/-ä, stem = infinitive - a/ä
// puhua puhu, juosta juos (irregular, handled separately)
// Type 2 verbs: end in -da/-dä, stem = infinitive - da/dä
// syödä syö, juoda juo
// Type 3 verbs: end in -la/-lä, -ra/-rä, -na/-nä, -sta/-stä
// tulla tul + l stem "tull" (double consonant)
// Type 4 verbs: end in -ata/-ätä
// tavata tapaa (irregular lengthening, handled as irregular)
//
// For NLG purposes we handle Type 1 and Type 2 as the most frequent.
fn fi_verb_stem(dict_form: String) -> String {
// Type 2: -da/-dä drop 2 characters
if str_ends_with(dict_form, "da") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "") {
return str_drop_last(dict_form, 2)
}
// Type 3: -lla/-llä, -rra, -nna drop last 2 chars (keeps double consonant)
if str_ends_with(dict_form, "lla") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "llä") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "rra") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "nna") {
return str_drop_last(dict_form, 2)
}
// Type 1 (and default): -a/-ä drop 1 character
if str_ends_with(dict_form, "a") {
return str_drop_last(dict_form, 1)
}
if str_ends_with(dict_form, "ä") {
return str_drop_last(dict_form, 1)
}
return dict_form
}
// Irregular verb table
//
// Returns an 18-element list encoding essential irregular paradigm forms, or an
// empty list if the verb is regular.
//
// Slot layout (0-indexed):
// 0 inf infinitive (dictionary form)
// 1 pres_1sg present 1sg
// 2 pres_2sg present 2sg
// 3 pres_3sg present 3sg
// 4 pres_1pl present 1pl
// 5 pres_2pl present 2pl
// 6 pres_3pl present 3pl
// 7 past_1sg past 1sg
// 8 past_2sg past 2sg
// 9 past_3sg past 3sg
// 10 past_1pl past 1pl
// 11 past_2pl past 2pl
// 12 past_3pl past 3pl
// 13 neg_stem negative stem (used with en/et/ei/emme/ette/eivät)
// 14 cond_stem conditional stem (for future use)
// 15 imp_2sg imperative 2sg
// 16 part_pres present participle stem
// 17 part_past past participle
fn fi_irregular_verb(dict_form: String) -> [String] {
let empty: [String] = []
// olla to be (the most irregular Finnish verb)
if str_eq(dict_form, "olla") {
let r: [String] = ["olla", "olen", "olet", "on", "olemme", "olette", "ovat",
"olin", "olit", "oli", "olimme", "olitte", "olivat",
"ole", "olis", "ole", "oleva", "ollut"]
return r
}
// voida can / to be able to
if str_eq(dict_form, "voida") {
let r: [String] = ["voida", "voin", "voit", "voi", "voimme", "voitte", "voivat",
"voin", "voit", "voi", "voimme", "voitte", "voivat",
"voi", "vois", "voi", "voiva", "voinut"]
return r
}
// mennä to go (Type 3 with irregularities)
if str_eq(dict_form, "mennä") {
let r: [String] = ["mennä", "menen", "menet", "menee", "menemme", "menette", "menevät",
"menin", "menit", "meni", "menimme", "menitte", "menivät",
"mene", "menis", "mene", "menevä", "mennyt"]
return r
}
// tulla to come (Type 3)
if str_eq(dict_form, "tulla") {
let r: [String] = ["tulla", "tulen", "tulet", "tulee", "tulemme", "tulette", "tulevat",
"tulin", "tulit", "tuli", "tulimme", "tulitte", "tulivat",
"tule", "tulis", "tule", "tuleva", "tullut"]
return r
}
// tehdä to do / make (Type 2, irregular)
if str_eq(dict_form, "tehdä") {
let r: [String] = ["tehdä", "teen", "teet", "tekee", "teemme", "teette", "tekevät",
"tein", "teit", "teki", "teimme", "teitte", "tekivät",
"tee", "tekis", "tee", "tekevä", "tehnyt"]
return r
}
// nähdä to see (Type 2, irregular)
if str_eq(dict_form, "nähdä") {
let r: [String] = ["nähdä", "näen", "näet", "näkee", "näemme", "näette", "näkevät",
"näin", "näit", "näki", "näimme", "näitte", "näkivät",
"näe", "näkis", "näe", "näkevä", "nähnyt"]
return r
}
// saada to get / to be able to (Type 2)
if str_eq(dict_form, "saada") {
let r: [String] = ["saada", "saan", "saat", "saa", "saamme", "saatte", "saavat",
"sain", "sait", "sai", "saimme", "saitte", "saivat",
"saa", "sais", "saa", "saava", "saanut"]
return r
}
// pitää must / to like (Type 1 with stem change)
if str_eq(dict_form, "pitää") {
let r: [String] = ["pitää", "pidän", "pidät", "pitää", "pidämme", "pidätte", "pitävät",
"pidin", "pidit", "piti", "pidimme", "piditte", "pitivät",
"pidä", "pitäis", "pidä", "pitävä", "pitänyt"]
return r
}
// tietää to know (Type 1 with stem change)
if str_eq(dict_form, "tietää") {
let r: [String] = ["tietää", "tiedän", "tiedät", "tietää", "tiedämme", "tiedätte", "tietävät",
"tiesin", "tiesit", "tiesi", "tiesimme", "tiesitte", "tiesivät",
"tiedä", "tietäis", "tiedä", "tietävä", "tiennyt"]
return r
}
return empty
}
// Present-tense personal endings
//
// Type-1 verb present tense endings (suffix onto stem):
// 1sg: -n 2sg: -t 3sg: stem-final vowel lengthened (no suffix)
// 1pl: -mme 2pl: -tte 3pl: -vat/-vät
//
// The 3sg form doubles the final vowel of the stem (puhu puhuu, tule tulee).
// The 3pl uses the harmony-dependent -vat/-vät suffix.
fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String {
if str_eq(number, "singular") {
if str_eq(person, "first") { return stem + "n" }
if str_eq(person, "second") { return stem + "t" }
if str_eq(person, "third") {
// 3sg: lengthen final vowel
let last: String = fi_str_last_char(stem)
return stem + last
}
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return stem + "mme" }
if str_eq(person, "second") { return stem + "tte" }
if str_eq(person, "third") { return stem + fi_suffix("vat", harmony) }
}
return stem
}
// Past-tense forms
//
// Type-1 verbs form the past by inserting -i- between the stem and the personal
// ending. The stem-final vowel may contract before -i-.
//
// puhua: puhu + i puhui + n puhuin (1sg past)
// Common contraction: stem final -a/-ä drops before -i-
// puhua stem puhu puhu + i = puhui (no drop needed, -u not -a)
// tavata contraction gives tavasi (handled as irregular or Type 4)
// For Type-1 verbs with -u/-y final stem the rule is simple concatenation.
fn fi_past_stem(stem: String) -> String {
// If stem ends in a or ä, they may contract. For Type-1 verbs where the
// infinitive is -aa/-ää the stem ends in -a/-ä; before -i- that often
// gives -oi-/-öi- (e.g. puhua: puhu puhui, but sanoa: sano sanoi).
// The heuristic: if the stem already ends in a vowel other than a/ä, just
// append i. If it ends in a/ä, convert to o/ö + i (common pattern).
let last: String = fi_str_last_char(stem)
if str_eq(last, "a") {
return str_drop_last(stem, 1) + "oi"
}
if str_eq(last, "ä") {
return str_drop_last(stem, 1) + "öi"
}
return stem + "i"
}
fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String {
let pstem: String = fi_past_stem(stem)
if str_eq(number, "singular") {
if str_eq(person, "first") { return pstem + "n" }
if str_eq(person, "second") { return pstem + "t" }
if str_eq(person, "third") { return str_drop_last(pstem, 1) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return pstem + "mme" }
if str_eq(person, "second") { return pstem + "tte" }
if str_eq(person, "third") { return pstem + fi_suffix("vat", harmony) }
}
return pstem
}
// Negative forms
//
// Finnish negation: negative auxiliary ei (conjugated for person/number) +
// verb in the connective (negative) stem = infinitive stem without personal ending.
//
// Negative auxiliary conjugation:
// 1sg: en 2sg: et 3sg: ei
// 1pl: emme 2pl: ette 3pl: eivät
//
// The negative stem for most verbs = present stem (the form without any ending).
fn fi_neg_aux(person: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(person, "first") { return "en" }
if str_eq(person, "second") { return "et" }
if str_eq(person, "third") { return "ei" }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return "emme" }
if str_eq(person, "second") { return "ette" }
if str_eq(person, "third") { return "eivät" }
}
return "ei"
}
fn fi_negative(verb: String, person: String, number: String) -> String {
let irreg: [String] = fi_irregular_verb(verb)
let aux: String = fi_neg_aux(person, number)
if native_list_len(irreg) > 0 {
let neg_stem: String = native_list_get(irreg, 13)
return aux + " " + neg_stem
}
let stem: String = fi_verb_stem(verb)
return aux + " " + stem
}
// Main conjugation entry point
//
// fi_conjugate(verb, tense, person, number) -> String
//
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let harmony: String = fi_harmony(verb)
// Check irregular table first
let irreg: [String] = fi_irregular_verb(verb)
if native_list_len(irreg) > 0 {
if str_eq(tense, "present") {
if str_eq(number, "singular") {
if str_eq(person, "first") { return native_list_get(irreg, 1) }
if str_eq(person, "second") { return native_list_get(irreg, 2) }
if str_eq(person, "third") { return native_list_get(irreg, 3) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return native_list_get(irreg, 4) }
if str_eq(person, "second") { return native_list_get(irreg, 5) }
if str_eq(person, "third") { return native_list_get(irreg, 6) }
}
}
if str_eq(tense, "past") {
if str_eq(number, "singular") {
if str_eq(person, "first") { return native_list_get(irreg, 7) }
if str_eq(person, "second") { return native_list_get(irreg, 8) }
if str_eq(person, "third") { return native_list_get(irreg, 9) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return native_list_get(irreg, 10) }
if str_eq(person, "second") { return native_list_get(irreg, 11) }
if str_eq(person, "third") { return native_list_get(irreg, 12) }
}
}
}
// Regular verbs
let stem: String = fi_verb_stem(verb)
if str_eq(tense, "present") {
return fi_present_ending(stem, person, number, harmony)
}
if str_eq(tense, "past") {
return fi_past_ending(stem, person, number, harmony)
}
return stem
}
// Question suffix
//
// Finnish questions are formed by appending -ko (back harmony) or -kö (front
// harmony) directly to the verb (or sometimes another focus word).
fn fi_question_suffix(harmony: String) -> String {
if str_eq(harmony, "front") { return "" }
return "ko"
}
// Question formation
//
// fi_make_question: append the appropriate question suffix to a verb form.
fn fi_make_question(verb_form: String, harmony: String) -> String {
return verb_form + fi_question_suffix(harmony)
}
// Convenience: inflect a noun through all 15 cases
//
// Returns a 30-element list: [case_name, sg_form, case_name, pl_form, ...]
// for all 15 cases. Plural-only cases (instructive, comitative) have an
// empty string for the singular slot.
fn fi_full_paradigm(noun: String) -> [String] {
let harmony: String = fi_harmony(noun)
let r: [String] = []
let cases: [String] = ["nominative", "genitive", "accusative", "partitive",
"inessive", "elative", "illative", "adessive",
"ablative", "allative", "essive", "translative",
"instructive", "abessive", "comitative"]
let n: Int = native_list_len(cases)
let i: Int = 0
while i < n {
let c: String = native_list_get(cases, i)
let r = native_list_append(r, c)
// Singular
if str_eq(c, "instructive") {
let r = native_list_append(r, "")
} else {
if str_eq(c, "comitative") {
let r = native_list_append(r, "")
} else {
let r = native_list_append(r, fi_noun_case(noun, c, "singular", harmony))
}
}
// Plural
let r = native_list_append(r, fi_noun_case(noun, c, "plural", harmony))
let i = i + 1
}
return r
}
+17
View File
@@ -0,0 +1,17 @@
// auto-generated by elc --emit-header — do not edit
extern fn fi_harmony(word: String) -> String
extern fn fi_suffix(base: String, harmony: String) -> String
extern fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
extern fn fi_str_last_char(s: String) -> String
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_verb_stem(dict_form: String) -> String
extern fn fi_irregular_verb(dict_form: String) -> [String]
extern fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String
extern fn fi_past_stem(stem: String) -> String
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String
extern fn fi_neg_aux(person: String, number: String) -> String
extern fn fi_negative(verb: String, person: String, number: String) -> String
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fi_question_suffix(harmony: String) -> String
extern fn fi_make_question(verb_form: String, harmony: String) -> String
extern fn fi_full_paradigm(noun: String) -> [String]
+677
View File
@@ -0,0 +1,677 @@
// morphology-fr.el - French morphology for the NLG engine.
//
// Implements fusional French verb conjugation, noun pluralization, gender
// inference, article agreement, and interrogative inversion. Designed as a
// companion to morphology.el and called by the engine when the language
// profile code is "fr".
//
// Verb tenses covered: present, future, imparfait, passé composé.
// Persons: first/second/third × singular/plural.
// Verb groups: -er (regular), -ir (regular finir-type), -re (regular vendre-type)
// + a core set of common irregular verbs.
//
// Liaison / elision notes:
// - le/la l' before vowel-initial nouns (handled in fr_agree_article)
// - est-ce que can precede any statement for yes/no questions (fr_question_inversion)
// - Inversion inserts -t- between vowel-final verb form and 3s pronoun: parle-t-il
//
// Depends on: morphology.el (str_ends_with, str_slice, str_len helpers)
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn fr_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn fr_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn fr_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn fr_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn fr_is_vowel_start(s: String) -> Bool {
let n: Int = str_len(s)
if n == 0 {
return false
}
let c: String = str_slice(s, 0, 1)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "é") { return true }
if str_eq(c, "è") { return true }
if str_eq(c, "ê") { return true }
if str_eq(c, "i") { return true }
if str_eq(c, "î") { return true }
if str_eq(c, "o") { return true }
if str_eq(c, "ô") { return true }
if str_eq(c, "u") { return true }
if str_eq(c, "û") { return true }
if str_eq(c, "h") { return true }
return false
}
// Verb group detection
//
// Returns "er" | "ir" | "re" | "irregular".
// Irregular detection is done by checking against a known list first; all other
// verbs are classified by ending.
fn fr_is_known_irregular(verb: String) -> Bool {
if str_eq(verb, "être") { return true }
if str_eq(verb, "avoir") { return true }
if str_eq(verb, "aller") { return true }
if str_eq(verb, "faire") { return true }
if str_eq(verb, "pouvoir") { return true }
if str_eq(verb, "vouloir") { return true }
if str_eq(verb, "venir") { return true }
if str_eq(verb, "dire") { return true }
if str_eq(verb, "voir") { return true }
if str_eq(verb, "prendre") { return true }
if str_eq(verb, "mettre") { return true }
if str_eq(verb, "savoir") { return true }
return false
}
fn fr_verb_group(base: String) -> String {
if fr_is_known_irregular(base) { return "irregular" }
if fr_str_ends(base, "er") { return "er" }
if fr_str_ends(base, "ir") { return "ir" }
if fr_str_ends(base, "re") { return "re" }
return "er"
}
fn fr_stem(base: String) -> String {
return fr_str_drop_last(base, 2)
}
// Person/number slot index
//
// 0 = 1s (je), 1 = 2s (tu), 2 = 3s (il/elle), 3 = 1p (nous), 4 = 2p (vous), 5 = 3p (ils/elles)
fn fr_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
if str_eq(number, "singular") { return 2 }
return 5
}
// Irregular present tense
//
// être: suis, es, est, sommes, êtes, sont
// avoir: ai, as, a, avons, avez, ont
// aller: vais, vas, va, allons, allez, vont
// faire: fais, fais, fait, faisons, faites, font
// pouvoir: peux, peux, peut, pouvons, pouvez, peuvent
// vouloir: veux, veux, veut, voulons, voulez, veulent
// venir: viens, viens, vient, venons, venez, viennent
// dire: dis, dis, dit, disons, dites, disent
// voir: vois, vois, voit, voyons, voyez, voient
// prendre: prends, prends, prend, prenons, prenez, prennent
// mettre: mets, mets, met, mettons, mettez, mettent
// savoir: sais, sais, sait, savons, savez, savent
fn fr_irregular_present(verb: String, person: String, number: String) -> String {
let slot: Int = fr_slot(person, number)
if str_eq(verb, "être") {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "etes" }
return "sont"
}
// ASCII alias used by morph_map_canonical identical conjugation.
if str_eq(verb, "etre") {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "etes" }
return "sont"
}
if str_eq(verb, "avoir") {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
if str_eq(verb, "aller") {
if slot == 0 { return "vais" }
if slot == 1 { return "vas" }
if slot == 2 { return "va" }
if slot == 3 { return "allons" }
if slot == 4 { return "allez" }
return "vont"
}
if str_eq(verb, "faire") {
if slot == 0 { return "fais" }
if slot == 1 { return "fais" }
if slot == 2 { return "fait" }
if slot == 3 { return "faisons" }
if slot == 4 { return "faites" }
return "font"
}
if str_eq(verb, "pouvoir") {
if slot == 0 { return "peux" }
if slot == 1 { return "peux" }
if slot == 2 { return "peut" }
if slot == 3 { return "pouvons" }
if slot == 4 { return "pouvez" }
return "peuvent"
}
if str_eq(verb, "vouloir") {
if slot == 0 { return "veux" }
if slot == 1 { return "veux" }
if slot == 2 { return "veut" }
if slot == 3 { return "voulons" }
if slot == 4 { return "voulez" }
return "veulent"
}
if str_eq(verb, "venir") {
if slot == 0 { return "viens" }
if slot == 1 { return "viens" }
if slot == 2 { return "vient" }
if slot == 3 { return "venons" }
if slot == 4 { return "venez" }
return "viennent"
}
if str_eq(verb, "dire") {
if slot == 0 { return "dis" }
if slot == 1 { return "dis" }
if slot == 2 { return "dit" }
if slot == 3 { return "disons" }
if slot == 4 { return "dites" }
return "disent"
}
if str_eq(verb, "voir") {
if slot == 0 { return "vois" }
if slot == 1 { return "vois" }
if slot == 2 { return "voit" }
if slot == 3 { return "voyons" }
if slot == 4 { return "voyez" }
return "voient"
}
if str_eq(verb, "prendre") {
if slot == 0 { return "prends" }
if slot == 1 { return "prends" }
if slot == 2 { return "prend" }
if slot == 3 { return "prenons" }
if slot == 4 { return "prenez" }
return "prennent"
}
if str_eq(verb, "mettre") {
if slot == 0 { return "mets" }
if slot == 1 { return "mets" }
if slot == 2 { return "met" }
if slot == 3 { return "mettons" }
if slot == 4 { return "mettez" }
return "mettent"
}
if str_eq(verb, "savoir") {
if slot == 0 { return "sais" }
if slot == 1 { return "sais" }
if slot == 2 { return "sait" }
if slot == 3 { return "savons" }
if slot == 4 { return "savez" }
return "savent"
}
return ""
}
// Regular present tense
//
// -er: -e, -es, -e, -ons, -ez, -ent
// -ir: -is, -is, -it, -issons, -issez, -issent (finir-type; stem gets -iss- in plural)
// -re: -s, -s, -(nothing), -ons, -ez, -ent
fn fr_regular_present(stem: String, vgroup: String, slot: Int) -> String {
if str_eq(vgroup, "er") {
if slot == 0 { return stem + "e" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
if str_eq(vgroup, "ir") {
// finir-type: singular uses bare stem, plural uses stem + -iss-
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "issons" }
if slot == 4 { return stem + "issez" }
return stem + "issent"
}
// -re (vendre-type)
if slot == 0 { return stem + "s" }
if slot == 1 { return stem + "s" }
if slot == 2 { return stem }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
// Regular future tense
//
// Future is formed from the infinitive (minus silent -e for -re verbs) + endings:
// -ai, -as, -a, -ons, -ez, -ont
fn fr_future_stem(base: String, vgroup: String) -> String {
// -re verbs drop the final -e before adding future endings
if str_eq(vgroup, "re") {
return fr_str_drop_last(base, 1)
}
return base
}
fn fr_regular_future(fstem: String, slot: Int) -> String {
if slot == 0 { return fstem + "ai" }
if slot == 1 { return fstem + "as" }
if slot == 2 { return fstem + "a" }
if slot == 3 { return fstem + "ons" }
if slot == 4 { return fstem + "ez" }
return fstem + "ont"
}
// Irregular future stems
//
// Returns the irregular future stem, or "" if regular.
fn fr_irregular_future_stem(verb: String) -> String {
if str_eq(verb, "être") { return "ser" }
if str_eq(verb, "avoir") { return "aur" }
if str_eq(verb, "aller") { return "ir" }
if str_eq(verb, "faire") { return "fer" }
if str_eq(verb, "pouvoir") { return "pourr" }
if str_eq(verb, "vouloir") { return "voudr" }
if str_eq(verb, "venir") { return "viendr" }
if str_eq(verb, "voir") { return "verr" }
if str_eq(verb, "savoir") { return "saur" }
return ""
}
// Regular imparfait
//
// Imparfait is formed from the nous-present stem (infinitive minus -er/-ir/-re,
// then add -iss for -ir verbs in nous-form) + endings:
// -ais, -ais, -ait, -ions, -iez, -aient
//
// For -er verbs: stem = infinitive minus -er
// For -ir verbs: stem = infinitive minus -ir (bare stem, not -iss- imparfait
// uses the basic stem, unlike present plural which uses -iss-)
// For -re verbs: stem = infinitive minus -re
// Exception: être uses ét- as the imparfait stem.
fn fr_imperfect_stem(base: String, vgroup: String) -> String {
if str_eq(base, "être") { return "ét" }
return fr_stem(base)
}
fn fr_regular_imperfect(istem: String, slot: Int) -> String {
if slot == 0 { return istem + "ais" }
if slot == 1 { return istem + "ais" }
if slot == 2 { return istem + "ait" }
if slot == 3 { return istem + "ions" }
if slot == 4 { return istem + "iez" }
return istem + "aient"
}
// Passé composé (past compound)
//
// Passé composé = auxiliary (avoir or être) + past participle.
// Most verbs use avoir; a core set of motion/state verbs use être.
//
// This function returns a two-word string "auxiliary participle".
// The caller is responsible for agreement of the past participle when être is
// used (feminine adds -e, plural adds -s); this function returns the masculine
// singular participle unconditionally.
fn fr_uses_etre(verb: String) -> Bool {
if str_eq(verb, "aller") { return true }
if str_eq(verb, "venir") { return true }
if str_eq(verb, "partir") { return true }
if str_eq(verb, "arriver") { return true }
if str_eq(verb, "entrer") { return true }
if str_eq(verb, "sortir") { return true }
if str_eq(verb, "naître") { return true }
if str_eq(verb, "mourir") { return true }
if str_eq(verb, "rester") { return true }
if str_eq(verb, "tomber") { return true }
if str_eq(verb, "monter") { return true }
if str_eq(verb, "descendre") { return true }
if str_eq(verb, "rentrer") { return true }
if str_eq(verb, "retourner") { return true }
if str_eq(verb, "passer") { return true }
return false
}
// Returns the past participle (masculine singular form).
fn fr_past_participle(verb: String) -> String {
// Irregular participles
if str_eq(verb, "être") { return "été" }
if str_eq(verb, "avoir") { return "eu" }
if str_eq(verb, "aller") { return "allé" }
if str_eq(verb, "faire") { return "fait" }
if str_eq(verb, "pouvoir") { return "pu" }
if str_eq(verb, "vouloir") { return "voulu" }
if str_eq(verb, "venir") { return "venu" }
if str_eq(verb, "dire") { return "dit" }
if str_eq(verb, "voir") { return "vu" }
if str_eq(verb, "prendre") { return "pris" }
if str_eq(verb, "mettre") { return "mis" }
if str_eq(verb, "savoir") { return "su" }
if str_eq(verb, "naître") { return "" }
if str_eq(verb, "mourir") { return "mort" }
// Regular participles by group
let vgroup: String = fr_verb_group(verb)
if str_eq(vgroup, "er") {
return fr_str_drop_last(verb, 2) + "é"
}
if str_eq(vgroup, "ir") {
return fr_str_drop_last(verb, 2) + "i"
}
// -re verbs: drop -re, add -u
return fr_str_drop_last(verb, 2) + "u"
}
// Conjugates the avoir auxiliary in the present (for passé composé).
fn fr_avoir_present(slot: Int) -> String {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
// Conjugates the être auxiliary in the present (for passé composé).
fn fr_etre_present(slot: Int) -> String {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "êtes" }
return "sont"
}
// Full conjugation entry point
//
// fr_conjugate: conjugate a French verb.
//
// verb: French infinitive (e.g. "parler", "être", "venir")
// tense: "present" | "future" | "imperfect" | "past"
// (note: "past" returns the passé composé as "aux participle")
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = fr_slot(person, number)
if str_eq(tense, "present") {
let irreg: String = fr_irregular_present(verb, person, number)
if !str_eq(irreg, "") {
return irreg
}
let vgroup: String = fr_verb_group(verb)
let stem: String = fr_stem(verb)
return fr_regular_present(stem, vgroup, slot)
}
if str_eq(tense, "future") {
let irreg_stem: String = fr_irregular_future_stem(verb)
if !str_eq(irreg_stem, "") {
return fr_regular_future(irreg_stem, slot)
}
let vgroup: String = fr_verb_group(verb)
let fstem: String = fr_future_stem(verb, vgroup)
return fr_regular_future(fstem, slot)
}
if str_eq(tense, "imperfect") {
let vgroup: String = fr_verb_group(verb)
let istem: String = fr_imperfect_stem(verb, vgroup)
return fr_regular_imperfect(istem, slot)
}
if str_eq(tense, "past") {
// Passé composé: auxiliary + past participle
let pp: String = fr_past_participle(verb)
if fr_uses_etre(verb) {
let aux: String = fr_etre_present(slot)
return aux + " " + pp
}
let aux: String = fr_avoir_present(slot)
return aux + " " + pp
}
// Unknown tense: return infinitive unchanged
return verb
}
// Noun gender inference
//
// Returns "m" (masculine), "f" (feminine), or "unknown".
//
// Heuristics (common French patterns):
// ends in -tion/-sion/-xion -> feminine (nation, passion, connexion)
// ends in -ure -> feminine (voiture, culture)
// ends in -ette -> feminine (omelette, cigarette)
// ends in -eur (abstract) -> feminine (couleur, peur, chaleur)
// ends in -eur (agent) -> masculine (acteur, serveur) can't always distinguish
// ends in -eur (no rule) -> try -teur masculine (docteur, auteur)
// ends in -ment -> masculine (sentiment, gouvernement)
// ends in -age -> masculine (voyage, fromage)
// ends in -isme -> masculine (socialisme)
// ends in -eau -> masculine (tableau, gâteau)
// ends in -er/-é -> masculine (boucher, café)
// ends in -ée -> feminine (journée, idée)
// ends in -ie -> feminine (philosophie, géographie)
// ends in -ance/-ence -> feminine (chance, science)
// ends in -té/-tié -> feminine (beauté, amitié)
// ends in -ude -> feminine (attitude, solitude)
// ends in -ade -> feminine (salade, promenade)
// ends in -ette -> feminine (already covered)
// ends in -e (generic) -> often feminine but not reliable; return "unknown"
fn fr_gender(noun: String) -> String {
// Feminine patterns (check more specific before general)
if fr_str_ends(noun, "tion") { return "f" }
if fr_str_ends(noun, "sion") { return "f" }
if fr_str_ends(noun, "xion") { return "f" }
if fr_str_ends(noun, "ure") { return "f" }
if fr_str_ends(noun, "ette") { return "f" }
if fr_str_ends(noun, "ance") { return "f" }
if fr_str_ends(noun, "ence") { return "f" }
if fr_str_ends(noun, "ité") { return "f" }
if fr_str_ends(noun, "") { return "f" }
if fr_str_ends(noun, "tié") { return "f" }
if fr_str_ends(noun, "ude") { return "f" }
if fr_str_ends(noun, "ade") { return "f" }
if fr_str_ends(noun, "ée") { return "f" }
if fr_str_ends(noun, "ie") { return "f" }
// Masculine patterns
if fr_str_ends(noun, "ment") { return "m" }
if fr_str_ends(noun, "age") { return "m" }
if fr_str_ends(noun, "isme") { return "m" }
if fr_str_ends(noun, "eau") { return "m" }
if fr_str_ends(noun, "eur") { return "m" }
if fr_str_ends(noun, "er") { return "m" }
if fr_str_ends(noun, "é") { return "m" }
return "unknown"
}
// Noun pluralization
//
// French plural rules:
// already ends in -s, -x, or -z -> unchanged
// ends in -eau -> add -x (bateau bateaux)
// ends in -eu -> add -x (jeu jeux; bleu → bleus is exception)
// ends in -al -> replace -al with -aux (animal animaux)
// ends in -ail (most) -> replace -ail with -aux (travail travaux; bail)
// otherwise -> add -s
fn fr_invariant_plural(noun: String) -> String {
// Words already ending in -s, -x, -z are unchanged in plural
let last: String = fr_str_last_char(noun)
if str_eq(last, "s") { return noun }
if str_eq(last, "x") { return noun }
if str_eq(last, "z") { return noun }
return ""
}
fn fr_pluralize(noun: String) -> String {
let inv: String = fr_invariant_plural(noun)
if !str_eq(inv, "") {
return inv
}
if fr_str_ends(noun, "eau") {
return noun + "x"
}
if fr_str_ends(noun, "eu") {
return noun + "x"
}
if fr_str_ends(noun, "al") {
return fr_str_drop_last(noun, 2) + "aux"
}
if fr_str_ends(noun, "ail") {
return fr_str_drop_last(noun, 3) + "aux"
}
return noun + "s"
}
// Article agreement
//
// fr_agree_article: return the correct French article for a noun.
//
// noun: the noun (used for gender inference and elision check)
// definite: "true" for definite (le/la/l'/les), "false" for indefinite (un/une/des)
// number: "singular" | "plural"
//
// Elision: le/la l' before a vowel- or h-initial noun (handled here).
fn fr_agree_article(noun: String, definite: String, number: String) -> String {
let gender: String = fr_gender(noun)
let is_plural: Bool = str_eq(number, "plural")
let is_def: Bool = str_eq(definite, "true")
let vowel_start: Bool = fr_is_vowel_start(noun)
if is_def {
if is_plural {
return "les"
}
// singular
if vowel_start {
return "l'"
}
if str_eq(gender, "f") {
return "la"
}
return "le"
}
// indefinite
if is_plural {
return "des"
}
if str_eq(gender, "f") {
return "une"
}
return "un"
}
// Question inversion
//
// fr_question_inversion: form a yes/no question using subject-verb inversion.
//
// subject: pronoun string: "je" | "tu" | "il" | "elle" | "nous" | "vous" | "ils" | "elles"
// verb_form: the conjugated verb form (e.g. "parle", "mange", "est")
//
// Rules:
// - Verb and subject are joined with a hyphen: "parle-t-il ?"
// - When the verb form ends in a vowel and the subject starts with a vowel
// (il, elle, ils, elles), insert euphonic -t-: "parle-t-il ?"
// - Je inversion is archaic; "est-ce que je ...?" is preferred — this function
// generates "est-ce que je <verb_form> ?" for first-person singular.
// - The result ends with " ?"
fn fr_subject_starts_vowel(subject: String) -> Bool {
if str_eq(subject, "il") { return true }
if str_eq(subject, "elle") { return true }
if str_eq(subject, "ils") { return true }
if str_eq(subject, "elles") { return true }
return false
}
fn fr_verb_ends_vowel(verb_form: String) -> Bool {
let last: String = fr_str_last_char(verb_form)
if str_eq(last, "a") { return true }
if str_eq(last, "e") { return true }
if str_eq(last, "é") { return true }
if str_eq(last, "i") { return true }
if str_eq(last, "o") { return true }
if str_eq(last, "u") { return true }
return false
}
fn fr_question_inversion(subject: String, verb_form: String) -> String {
// First-person singular: use est-ce que construction
if str_eq(subject, "je") {
return "est-ce que je " + verb_form + " ?"
}
// Determine whether to insert -t-
let need_t: Bool = false
if fr_verb_ends_vowel(verb_form) {
if fr_subject_starts_vowel(subject) {
let need_t = true
}
}
if need_t {
return verb_form + "-t-" + subject + " ?"
}
return verb_form + "-" + subject + " ?"
}
+29
View File
@@ -0,0 +1,29 @@
// auto-generated by elc --emit-header — do not edit
extern fn fr_str_ends(s: String, suf: String) -> Bool
extern fn fr_str_drop_last(s: String, n: Int) -> String
extern fn fr_str_last_char(s: String) -> String
extern fn fr_str_last2(s: String) -> String
extern fn fr_is_vowel_start(s: String) -> Bool
extern fn fr_is_known_irregular(verb: String) -> Bool
extern fn fr_verb_group(base: String) -> String
extern fn fr_stem(base: String) -> String
extern fn fr_slot(person: String, number: String) -> Int
extern fn fr_irregular_present(verb: String, person: String, number: String) -> String
extern fn fr_regular_present(stem: String, vgroup: String, slot: Int) -> String
extern fn fr_future_stem(base: String, vgroup: String) -> String
extern fn fr_regular_future(fstem: String, slot: Int) -> String
extern fn fr_irregular_future_stem(verb: String) -> String
extern fn fr_imperfect_stem(base: String, vgroup: String) -> String
extern fn fr_regular_imperfect(istem: String, slot: Int) -> String
extern fn fr_uses_etre(verb: String) -> Bool
extern fn fr_past_participle(verb: String) -> String
extern fn fr_avoir_present(slot: Int) -> String
extern fn fr_etre_present(slot: Int) -> String
extern fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fr_gender(noun: String) -> String
extern fn fr_invariant_plural(noun: String) -> String
extern fn fr_pluralize(noun: String) -> String
extern fn fr_agree_article(noun: String, definite: String, number: String) -> String
extern fn fr_subject_starts_vowel(subject: String) -> Bool
extern fn fr_verb_ends_vowel(verb_form: String) -> Bool
extern fn fr_question_inversion(subject: String, verb_form: String) -> String
+668
View File
@@ -0,0 +1,668 @@
// morphology-fro.el - Old French morphology for the NLG engine.
//
// Implements Old French verb conjugation, noun declension, and the definite
// article. Designed as a companion to morphology.el and called by the engine
// when the language profile code is "fro".
//
// Language profile: code=fro, name=Old French, morph_type=fusional,
// word_order=V2, question_strategy=inversion, script=latin,
// family=romance.
//
// Historical note: Old French (ca. 9001400 CE) is the ancestor of Modern
// French. It diverged from Vulgar Latin and retained a two-case system
// nominative (cas sujet) and oblique (cas régime) inherited ultimately from
// Latin. By around 1300 CE the case distinction had largely collapsed in
// spoken usage, surviving mainly in formal written registers until it
// disappeared altogether. This file targets the core Old French period
// (ca. 10001300).
//
// Two-case system (masculine nouns):
// Singular: nominative stem + -s (li murs = the wall [subject])
// oblique stem (le mur = the wall [object])
// Plural: nominative stem (li mur = the walls [subject])
// oblique stem + -s (les murs = the walls [object])
// Feminine nouns show no case distinction throughout.
//
// Verb conjugation covered:
// Tenses: present indicative, passé simple (past), future
// Persons: first/second/third × singular/plural (slots 0-5)
// Conjugations:
// 1st (-er): present -e/-es/-e/-ons/-ez/-ent
// passé simple -ai/-as/-a/-ames/-astes/-erent
// future stem+rai/ras/ra/rons/rez/ront
// 2nd (-ir): present -is/-is/-it/-issons/-issiez/-issent
// passé simple -is/-is/-it/-imes/-istes/-irent
// future stem+rai series
// 3rd (-re): present stem/s/t/-ons/-ez/-ent
// passé simple -is series (like 2nd)
// future stem+rai series
// Irregulars: estre (be), avoir (have), aler (go), venir (come),
// faire (do/make)
// Canonical map: "be" -> "estre"
//
// Noun declension covered:
// Masculine: two-case (nom/obl) × sg/pl as above
// Feminine: case-neutral, sg base / pl base + -s
// Gender detection: -e ending -> feminine (heuristic), else masculine
//
// Article:
// Definite masculine nom sg: li; obl sg: le; nom pl: li; obl pl: les
// Definite feminine sg: la; pl: les
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn fro_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn fro_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Maps person × number to a 0-based paradigm index.
// 0 = 1st singular (je)
// 1 = 2nd singular (tu)
// 2 = 3rd singular (il/ele)
// 3 = 1st plural (nos)
// 4 = 2nd plural (vos)
// 5 = 3rd plural (il/eles)
fn fro_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third person
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// English semantic-layer labels are resolved to Old French dictionary infinitives
// before conjugation.
fn fro_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "estre" }
if str_eq(verb, "have") { return "avoir" }
if str_eq(verb, "go") { return "aler" }
if str_eq(verb, "come") { return "venir" }
if str_eq(verb, "do") { return "faire" }
if str_eq(verb, "make") { return "faire" }
if str_eq(verb, "say") { return "dire" }
if str_eq(verb, "see") { return "veoir" }
if str_eq(verb, "want") { return "vouloir" }
if str_eq(verb, "can") { return "pooir" }
return verb
}
// Irregular verb: estre (to be)
//
// Suppletive paradigm one of the most irregular verbs in Old French.
//
// Present indicative:
// 1sg sui 2sg es 3sg est
// 1pl somes 2pl estes 3pl sont
//
// Passé simple (past):
// 1sg fui 2sg fus 3sg fu
// 1pl fumes 2pl fustes 3pl furent
//
// Future (periphrastic, based on ester- stem):
// 1sg esterai 2sg esteras 3sg estera
// 1pl esterons 2pl esterez 3pl esteront
fn fro_estre_present(slot: Int) -> String {
if slot == 0 { return "sui" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "somes" }
if slot == 4 { return "estes" }
return "sont"
}
fn fro_estre_past(slot: Int) -> String {
if slot == 0 { return "fui" }
if slot == 1 { return "fus" }
if slot == 2 { return "fu" }
if slot == 3 { return "fumes" }
if slot == 4 { return "fustes" }
return "furent"
}
fn fro_estre_future(slot: Int) -> String {
if slot == 0 { return "esterai" }
if slot == 1 { return "esteras" }
if slot == 2 { return "estera" }
if slot == 3 { return "esterons" }
if slot == 4 { return "esterez" }
return "esteront"
}
// Irregular verb: avoir (to have)
//
// Present indicative:
// 1sg ai 2sg as 3sg a
// 1pl avons 2pl avez 3pl ont
//
// Passé simple:
// 1sg oi 2sg os 3sg ot
// 1pl eumes 2pl eustes 3pl orent
//
// Future:
// 1sg avrai 2sg avras 3sg avra
// 1pl avrons 2pl avrez 3pl avront
fn fro_avoir_present(slot: Int) -> String {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
fn fro_avoir_past(slot: Int) -> String {
if slot == 0 { return "oi" }
if slot == 1 { return "os" }
if slot == 2 { return "ot" }
if slot == 3 { return "eumes" }
if slot == 4 { return "eustes" }
return "orent"
}
fn fro_avoir_future(slot: Int) -> String {
if slot == 0 { return "avrai" }
if slot == 1 { return "avras" }
if slot == 2 { return "avra" }
if slot == 3 { return "avrons" }
if slot == 4 { return "avrez" }
return "avront"
}
// Irregular verb: aler (to go)
//
// Highly suppletive present draws on Latin *vadere (vois- stem).
//
// Present indicative:
// 1sg vois 2sg vas 3sg va
// 1pl alons 2pl alez 3pl vont
//
// Passé simple (regular -er pattern on al-):
// 1sg alai 2sg alas 3sg ala
// 1pl alames 2pl alastes 3pl alerent
//
// Future (ir- stem, archaic):
// 1sg irai 2sg iras 3sg ira
// 1pl irons 2pl irez 3pl iront
fn fro_aler_present(slot: Int) -> String {
if slot == 0 { return "vois" }
if slot == 1 { return "vas" }
if slot == 2 { return "va" }
if slot == 3 { return "alons" }
if slot == 4 { return "alez" }
return "vont"
}
fn fro_aler_past(slot: Int) -> String {
if slot == 0 { return "alai" }
if slot == 1 { return "alas" }
if slot == 2 { return "ala" }
if slot == 3 { return "alames" }
if slot == 4 { return "alastes" }
return "alerent"
}
fn fro_aler_future(slot: Int) -> String {
if slot == 0 { return "irai" }
if slot == 1 { return "iras" }
if slot == 2 { return "ira" }
if slot == 3 { return "irons" }
if slot == 4 { return "irez" }
return "iront"
}
// Irregular verb: venir (to come)
//
// Present indicative (vien-/ven- alternation):
// 1sg vieng 2sg viens 3sg vient
// 1pl venons 2pl venez 3pl vienent
//
// Passé simple:
// 1sg ving 2sg vins 3sg vint
// 1pl vinsmes 2pl vinstes 3pl vindrent
//
// Future (venr- stem):
// 1sg venrai 2sg venras 3sg venra
// 1pl venrons 2pl venrez 3pl venront
fn fro_venir_present(slot: Int) -> String {
if slot == 0 { return "vieng" }
if slot == 1 { return "viens" }
if slot == 2 { return "vient" }
if slot == 3 { return "venons" }
if slot == 4 { return "venez" }
return "vienent"
}
fn fro_venir_past(slot: Int) -> String {
if slot == 0 { return "ving" }
if slot == 1 { return "vins" }
if slot == 2 { return "vint" }
if slot == 3 { return "vinsmes" }
if slot == 4 { return "vinstes" }
return "vindrent"
}
fn fro_venir_future(slot: Int) -> String {
if slot == 0 { return "venrai" }
if slot == 1 { return "venras" }
if slot == 2 { return "venra" }
if slot == 3 { return "venrons" }
if slot == 4 { return "venrez" }
return "venront"
}
// Irregular verb: faire (to do/make)
//
// Present indicative (faz/fais- alternation):
// 1sg faz 2sg fais 3sg fait
// 1pl faisons 2pl faites 3pl font
//
// Passé simple:
// 1sg fis 2sg fis 3sg fist
// 1pl fimes 2pl fistes 3pl firent
//
// Future (fer- stem):
// 1sg ferai 2sg feras 3sg fera
// 1pl ferons 2pl ferez 3pl feront
fn fro_faire_present(slot: Int) -> String {
if slot == 0 { return "faz" }
if slot == 1 { return "fais" }
if slot == 2 { return "fait" }
if slot == 3 { return "faisons" }
if slot == 4 { return "faites" }
return "font"
}
fn fro_faire_past(slot: Int) -> String {
if slot == 0 { return "fis" }
if slot == 1 { return "fis" }
if slot == 2 { return "fist" }
if slot == 3 { return "fimes" }
if slot == 4 { return "fistes" }
return "firent"
}
fn fro_faire_future(slot: Int) -> String {
if slot == 0 { return "ferai" }
if slot == 1 { return "feras" }
if slot == 2 { return "fera" }
if slot == 3 { return "ferons" }
if slot == 4 { return "ferez" }
return "feront"
}
// Conjugation class detection
//
// Old French verbs fall into three broad conjugation classes:
// 1st conjugation: infinitive ends in -er (chanter, donner)
// 2nd conjugation: infinitive ends in -ir (finir, choisir)
// 3rd conjugation: infinitive ends in -re (vendre, rendre)
//
// Returns "1", "2", or "3".
fn fro_verb_class(verb: String) -> String {
if fro_str_ends(verb, "er") { return "1" }
if fro_str_ends(verb, "ir") { return "2" }
if fro_str_ends(verb, "re") { return "3" }
return "1"
}
// fro_verb_stem: strip the infinitive suffix to expose the productive stem.
// 1st (-er): drop 2 bytes
// 2nd (-ir): drop 2 bytes
// 3rd (-re): drop 2 bytes
fn fro_verb_stem(verb: String, vclass: String) -> String {
return fro_drop(verb, 2)
}
// 1st conjugation (-er): regular endings
//
// Present indicative (stem + ending):
// 1sg -e 2sg -es 3sg -e
// 1pl -ons 2pl -ez 3pl -ent
//
// Passé simple:
// 1sg -ai 2sg -as 3sg -a
// 1pl -ames 2pl -astes 3pl -erent
//
// Future (infinitive is the base drop -r then add endings):
// Actually the future stem = infinitive minus final -r (chanter- -> chanterai)
// 1sg -ai 2sg -as 3sg -a
// 1pl -ons 2pl -ez 3pl -ont
// Combined with chanterr-: chant+er+ai = chanterai; stem for future = infinitive + "a"...
// Simpler: future base = fro_drop(verb, 1) i.e. drop final -r to keep the -e:
// chanterai, chanteras, chantera, chanterons, chanterez, chanteront
fn fro_conj1_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "e" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
fn fro_conj1_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "ai" }
if slot == 1 { return stem + "as" }
if slot == 2 { return stem + "a" }
if slot == 3 { return stem + "ames" }
if slot == 4 { return stem + "astes" }
return stem + "erent"
}
fn fro_conj1_future(verb: String, slot: Int) -> String {
// Future base = infinitive minus final -r (retains the -e): chanter -> chante-
let base: String = fro_drop(verb, 1)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// 2nd conjugation (-ir): regular endings
//
// Present indicative uses an infix -iss- in pl forms (inchoative):
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -issons 2pl stem + -issiez 3pl stem + -issent
//
// Passé simple:
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -imes 2pl stem + -istes 3pl stem + -irent
//
// Future (infinitive minus final -r):
// finir -> fini- -> finirai ...
fn fro_conj2_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "issons" }
if slot == 4 { return stem + "issiez" }
return stem + "issent"
}
fn fro_conj2_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "imes" }
if slot == 4 { return stem + "istes" }
return stem + "irent"
}
fn fro_conj2_future(verb: String, slot: Int) -> String {
let base: String = fro_drop(verb, 1)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// 3rd conjugation (-re): regular endings
//
// Present indicative:
// 1sg stem (no ending) 2sg stem + -s 3sg stem + -t
// 1pl stem + -ons 2pl stem + -ez 3pl stem + -ent
//
// Passé simple (same endings as 2nd conj):
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -imes 2pl stem + -istes 3pl stem + -irent
//
// Future (-re verbs drop -e before adding endings):
// vendre -> vendr- -> vendrai ...
fn fro_conj3_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem }
if slot == 1 { return stem + "s" }
if slot == 2 { return stem + "t" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
fn fro_conj3_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "imes" }
if slot == 4 { return stem + "istes" }
return stem + "irent"
}
fn fro_conj3_future(verb: String, slot: Int) -> String {
// Drop -re (2 bytes) to get consonant-final stem, then add -rai etc.
let base: String = fro_drop(verb, 2)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// fro_conjugate: main conjugation entry point
//
// verb: Old French infinitive (e.g. "chanter", "finir", "vendre")
// or English canonical label ("be", "go", "have", ...)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive.
fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = fro_map_canonical(verb)
let slot: Int = fro_slot(person, number)
// Irregular: estre (to be)
if str_eq(v, "estre") {
if str_eq(tense, "present") { return fro_estre_present(slot) }
if str_eq(tense, "past") { return fro_estre_past(slot) }
if str_eq(tense, "future") { return fro_estre_future(slot) }
return v
}
// Irregular: avoir (to have)
if str_eq(v, "avoir") {
if str_eq(tense, "present") { return fro_avoir_present(slot) }
if str_eq(tense, "past") { return fro_avoir_past(slot) }
if str_eq(tense, "future") { return fro_avoir_future(slot) }
return v
}
// Irregular: aler (to go)
if str_eq(v, "aler") {
if str_eq(tense, "present") { return fro_aler_present(slot) }
if str_eq(tense, "past") { return fro_aler_past(slot) }
if str_eq(tense, "future") { return fro_aler_future(slot) }
return v
}
// Irregular: venir (to come)
if str_eq(v, "venir") {
if str_eq(tense, "present") { return fro_venir_present(slot) }
if str_eq(tense, "past") { return fro_venir_past(slot) }
if str_eq(tense, "future") { return fro_venir_future(slot) }
return v
}
// Irregular: faire (to do/make)
if str_eq(v, "faire") {
if str_eq(tense, "present") { return fro_faire_present(slot) }
if str_eq(tense, "past") { return fro_faire_past(slot) }
if str_eq(tense, "future") { return fro_faire_future(slot) }
return v
}
// Regular conjugations
let vclass: String = fro_verb_class(v)
let stem: String = fro_verb_stem(v, vclass)
if str_eq(vclass, "1") {
if str_eq(tense, "present") { return fro_conj1_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj1_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj1_future(v, slot) }
return v
}
if str_eq(vclass, "2") {
if str_eq(tense, "present") { return fro_conj2_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj2_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj2_future(v, slot) }
return v
}
if str_eq(vclass, "3") {
if str_eq(tense, "present") { return fro_conj3_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj3_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj3_future(v, slot) }
return v
}
// Final fallback: return the infinitive
return v
}
// Gender detection
//
// Heuristic gender detection from the citation form (nominative singular).
// Old French gender was inherited from Latin with only two genders surviving:
// masculine and feminine (neuter collapsed into masculine/feminine by Vulgar Latin).
//
// Heuristic: citation form ending in -e -> feminine; otherwise -> masculine.
// This is imperfect but covers the majority of common nouns.
fn fro_gender(noun: String) -> String {
if fro_str_ends(noun, "e") { return "fem" }
return "masc"
}
// Noun declension
//
// Old French two-case system:
//
// Masculine (e.g. mur wall, stem = mur):
// Singular nominative (cas sujet): murs (stem + -s)
// Singular oblique (cas régime): mur (stem)
// Plural nominative: mur (stem)
// Plural oblique: murs (stem + -s)
//
// This pattern means nominative markers invert relative to Latin:
// the nominative takes -s in singular but loses it in plural,
// while the oblique works the other way round.
//
// Feminine (e.g. dame lady):
// No case distinction throughout.
// Singular: dame (citation form)
// Plural: dames (citation + -s)
//
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_decline_masc(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun + "s" }
// oblique singular: bare stem
return noun
}
// plural
if str_eq(gram_case, "nominative") { return noun }
return noun + "s"
}
fn fro_decline_fem(noun: String, number: String) -> String {
if str_eq(number, "singular") { return noun }
return noun + "s"
}
// fro_decline: main declension entry point.
//
// noun: citation form (nominative/oblique singular typically the bare stem)
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = fro_gender(noun)
if str_eq(gender, "masc") {
return fro_decline_masc(noun, gram_case, number)
}
return fro_decline_fem(noun, number)
}
// Definite article
//
// Old French definite articles are case- and gender-sensitive:
//
// Masculine:
// nom sg: li obl sg: le
// nom pl: li obl pl: les
//
// Feminine:
// sg: la pl: les
//
// gender: "masc" | "fem"
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_article(gender: String, gram_case: String, number: String) -> String {
if str_eq(gender, "masc") {
if str_eq(number, "plural") { return "les" }
if str_eq(gram_case, "nominative") { return "li" }
return "le"
}
// feminine
if str_eq(number, "plural") { return "les" }
return "la"
}
// fro_noun_phrase: noun phrase builder
//
// Assembles a declined noun with an optional definite article.
//
// noun: citation form (nominative/oblique singular stem)
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
// definite: "true" to prepend the definite article; any other value omits it
fn fro_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let gender: String = fro_gender(noun)
let declined: String = fro_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let art: String = fro_article(gender, gram_case, number)
return art + " " + declined
}
return declined
}
+38
View File
@@ -0,0 +1,38 @@
// auto-generated by elc --emit-header — do not edit
extern fn fro_str_ends(s: String, suf: String) -> Bool
extern fn fro_drop(s: String, n: Int) -> String
extern fn fro_slot(person: String, number: String) -> Int
extern fn fro_map_canonical(verb: String) -> String
extern fn fro_estre_present(slot: Int) -> String
extern fn fro_estre_past(slot: Int) -> String
extern fn fro_estre_future(slot: Int) -> String
extern fn fro_avoir_present(slot: Int) -> String
extern fn fro_avoir_past(slot: Int) -> String
extern fn fro_avoir_future(slot: Int) -> String
extern fn fro_aler_present(slot: Int) -> String
extern fn fro_aler_past(slot: Int) -> String
extern fn fro_aler_future(slot: Int) -> String
extern fn fro_venir_present(slot: Int) -> String
extern fn fro_venir_past(slot: Int) -> String
extern fn fro_venir_future(slot: Int) -> String
extern fn fro_faire_present(slot: Int) -> String
extern fn fro_faire_past(slot: Int) -> String
extern fn fro_faire_future(slot: Int) -> String
extern fn fro_verb_class(verb: String) -> String
extern fn fro_verb_stem(verb: String, vclass: String) -> String
extern fn fro_conj1_present(stem: String, slot: Int) -> String
extern fn fro_conj1_past(stem: String, slot: Int) -> String
extern fn fro_conj1_future(verb: String, slot: Int) -> String
extern fn fro_conj2_present(stem: String, slot: Int) -> String
extern fn fro_conj2_past(stem: String, slot: Int) -> String
extern fn fro_conj2_future(verb: String, slot: Int) -> String
extern fn fro_conj3_present(stem: String, slot: Int) -> String
extern fn fro_conj3_past(stem: String, slot: Int) -> String
extern fn fro_conj3_future(verb: String, slot: Int) -> String
extern fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fro_gender(noun: String) -> String
extern fn fro_decline_masc(noun: String, gram_case: String, number: String) -> String
extern fn fro_decline_fem(noun: String, number: String) -> String
extern fn fro_decline(noun: String, gram_case: String, number: String) -> String
extern fn fro_article(gender: String, gram_case: String, number: String) -> String
extern fn fro_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+507
View File
@@ -0,0 +1,507 @@
// morphology-gez.el - Ge'ez morphology for the NLG engine.
// ግዕዝ Classical Ethiopic, the liturgical language of the
// Ethiopian Orthodox Church and ancestor of Amharic/Tigrinya.
//
// Implements Ge'ez verb conjugation (perfect and imperfect, basic G-stem),
// noun declension (nominative, accusative, construct; singular and plural),
// and noun-phrase construction.
//
// Ge'ez (also: Classical Ethiopic, gǝʿǝz) is the ancient Semitic language
// of the Kingdom of Axum (ca. 300900 CE active liturgy; ca. 1st century BCE
// epigraphic attestation). It remains the liturgical language of the
// Ethiopian and Eritrean Orthodox Churches. Modern Ethiopian Semitic languages
// (Amharic, Tigrinya, Tigre) descend from Ge'ez or a closely related ancestor.
//
// Script: Ge'ez Fidel (ፊደል) an Ethiopic abugida (Unicode U+1200U+137F).
// Each Fidel character encodes a consonant + vowel combination (7 orders per
// consonant). String literals in this file use actual Unicode characters.
//
// NOTE on El runtime: the El VM currently outputs non-ASCII as numeric hashes
// (runtime limitation for non-Latin scripts). str_eq and string comparisons
// work correctly internally. When the VM adds full UTF-8 output, all Ge'ez
// strings will display correctly automatically.
//
// Language profile:
// code=gez, name=Ge'ez, morph_type=semitic, word_order=SOV,
// script=ethiopic-fidel, family=semitic/south-ethiopic-semitic
//
// Key grammatical facts:
// - SOV word order unusual for Semitic (Arabic, Hebrew, Akkadian are VSO);
// Ge'ez shares SOV with modern Ethiopian Semitic descendants
// - Semitic trilateral root system (root + vowel pattern = word)
// - Gender: masculine (default) / feminine (often marked with -t suffix)
// - Number: singular / plural; dual vestigial
// - Cases: nominative (unmarked), accusative -a (animate masc nouns),
// construct/genitive (various; simplified to base form here)
// - Plural: no single rule; common patterns: -āt (fem/animate), -ān (masc),
// broken (internal vowel change, unpredictable without lexicon)
// - Verb system:
// Perfect (suffix conjugation): completed action
// Imperfect (prefix conjugation): ongoing / future / habitual
// Four derived stems: basic (G), causative (ʾa-), intensive (doubling),
// passive (te-); this file implements basic G-stem throughout
// - No definite article (unlike Amharic which has suffixed -u/-wa/-itu etc.)
// - Copula: root kwn (ሆነ honä) "to be / become"
//
// Verb conjugation conventions:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "perfect" | "imperfect"
//
// Noun declension conventions:
// gram_case: "nom" | "acc" | "construct"
// number: "singular" | "plural"
//
// Verbs covered (root / Fidel form / transliteration):
// "kwn" / ሆነ (honä) to be / become (copula)
// "hlw" / ሀሎ (hallo) to exist / there is
// "hbl" / ሰጠ (säṭṭä) to give
// "rʾy" / አየ (ʾayyä) to see
// "qwl" / ተናገረ (tänagärä) to speak
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn gez_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn gez_str_len(s: String) -> Int {
return str_len(s)
}
fn gez_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Slot index
//
// Maps person × number to a 0-based slot for table lookups.
// Gender is handled separately for third-person disambiguation.
//
// Slot layout (6 primary cells):
// 0 = 1sg (I)
// 1 = 2sg (you sg gender note: Ge'ez distinguishes 2sg m/f in perfect)
// 2 = 3sg m (he)
// 3 = 3sg f (she)
// 4 = 1pl (we)
// 5 = 3pl (they default masc)
fn gez_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "plural") { return 4 }
return 0
}
if str_eq(person, "second") {
return 1
}
// third
if str_eq(number, "plural") { return 5 }
return 2
}
// gez_slot_g: gender-sensitive slot for third-person singular.
fn gez_slot_g(person: String, gender: String, number: String) -> Int {
let base: Int = gez_slot(person, number)
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
}
}
return base
}
// Copula: kwn / ሆነ to be / become
//
// Perfect paradigm (suffix forms):
// 3sg m: ሆነ honä (base form)
// 3sg f: ሆነት honät (-t suffix)
// 2sg m: ሆንከ honkä (-kä suffix)
// 2sg f: ሆንኪ honki (-ki suffix)
// 1sg: ሆንኩ honku (-ku suffix)
// 3pl m: ሆኑ honu (-u suffix)
// 3pl f: ሆና honā ( suffix)
// 2pl: ሆንክሙ honkǝmu (-kǝmu suffix)
// 1pl: ሆንነ honna (-na suffix)
//
// Imperfect paradigm (prefix forms):
// 3sg m: ይሆን yǝhon (yǝ- prefix)
// 3sg f: ትሆን tǝhon (tǝ- prefix)
// 2sg: ትሆን tǝhon (tǝ- prefix)
// 1sg: እሆን ʾǝhon (ʾǝ- prefix)
// 3pl m: ይሆኑ yǝhonu (yǝ- + -u suffix)
// 1pl: ንሆን nǝhon (nǝ- prefix)
fn gez_kwn_perfect(slot: Int) -> String {
if slot == 0 { return "ሆንኩ" } // 1sg honku
if slot == 1 { return "ሆንከ" } // 2sg m honkä (default; fem: ሆንኪ)
if slot == 2 { return "ሆነ" } // 3sg m honä
if slot == 3 { return "ሆነት" } // 3sg f honät
if slot == 4 { return "ሆንነ" } // 1pl honna
return "ሆኑ" // 3pl honu
}
fn gez_kwn_imperfect(slot: Int) -> String {
if slot == 0 { return "እሆን" } // 1sg ʾǝhon
if slot == 1 { return "ትሆን" } // 2sg tǝhon
if slot == 2 { return "ይሆን" } // 3sg m yǝhon
if slot == 3 { return "ትሆን" } // 3sg f tǝhon (same prefix as 2sg)
if slot == 4 { return "ንሆን" } // 1pl nǝhon
return "ይሆኑ" // 3pl yǝhonu
}
fn gez_is_copula(verb: String) -> Bool {
if str_eq(verb, "kwn") { return true }
if str_eq(verb, "ሆነ") { return true }
if str_eq(verb, "hona") { return true }
if str_eq(verb, "be") { return true }
return false
}
fn gez_conjugate_copula(tense: String, slot: Int) -> String {
if str_eq(tense, "imperfect") { return gez_kwn_imperfect(slot) }
return gez_kwn_perfect(slot)
}
// hlw / ሀሎ to exist / there is
//
// hallo is an existential copula used for "there is/are".
// In Ge'ez it is largely invariant in its classical usage (presentational).
// We provide a minimal paradigm; for existential use, hallo is returned for
// all slots in the "perfect" (existential present).
fn gez_hlw_perfect(slot: Int) -> String {
// Invariant existential for most purposes
if slot == 0 { return "ሀሎኩ" } // 1sg halloku
if slot == 1 { return "ሀሎከ" } // 2sg hallokä
if slot == 2 { return "ሀሎ" } // 3sg m hallo
if slot == 3 { return "ሀለወት" } // 3sg f halläwät
if slot == 4 { return "ሀሎነ" } // 1pl hallonä
return "ሀሉ" // 3pl hallu
}
fn gez_hlw_imperfect(slot: Int) -> String {
if slot == 0 { return "እሀሉ" } // 1sg
if slot == 1 { return "ትሀሉ" } // 2sg
if slot == 2 { return "ይሀሉ" } // 3sg m
if slot == 3 { return "ትሀሉ" } // 3sg f
if slot == 4 { return "ንሀሉ" } // 1pl
return "ይሀልዉ" // 3pl
}
// hbl / ሰጠ to give
//
// Perfect: säṭṭä (3sg m base); standard G-stem suffix paradigm.
// Imperfect: yǝsäṭ (3sg m).
fn gez_hbl_perfect(slot: Int) -> String {
if slot == 0 { return "ሰጠኩ" } // 1sg säṭṭäku
if slot == 1 { return "ሰጠከ" } // 2sg säṭṭäkä
if slot == 2 { return "ሰጠ" } // 3sg m säṭṭä
if slot == 3 { return "ሰጠት" } // 3sg f säṭṭät
if slot == 4 { return "ሰጠነ" } // 1pl säṭṭänä
return "ሰጡ" // 3pl säṭṭu
}
fn gez_hbl_imperfect(slot: Int) -> String {
if slot == 0 { return "እሰጥ" } // 1sg ʾǝsäṭ
if slot == 1 { return "ትሰጥ" } // 2sg tǝsäṭ
if slot == 2 { return "ይሰጥ" } // 3sg m yǝsäṭ
if slot == 3 { return "ትሰጥ" } // 3sg f tǝsäṭ
if slot == 4 { return "ንሰጥ" } // 1pl nǝsäṭ
return "ይሰጡ" // 3pl yǝsäṭu
}
// rʾy / አየ to see
//
// Third-weak verb (final ʾ). Perfect: ʾayyä (3sg m); imperfect: yāy (3sg m).
fn gez_ray_perfect(slot: Int) -> String {
if slot == 0 { return "አየኩ" } // 1sg ʾayyäku
if slot == 1 { return "አየከ" } // 2sg ʾayyäkä
if slot == 2 { return "አየ" } // 3sg m ʾayyä
if slot == 3 { return "አየት" } // 3sg f ʾayyät
if slot == 4 { return "አየነ" } // 1pl ʾayyänä
return "አዩ" // 3pl ʾayyu
}
fn gez_ray_imperfect(slot: Int) -> String {
if slot == 0 { return "እያይ" } // 1sg ʾǝyāy
if slot == 1 { return "ትያይ" } // 2sg tǝyāy
if slot == 2 { return "ያይ" } // 3sg m yāy
if slot == 3 { return "ትያይ" } // 3sg f tǝyāy
if slot == 4 { return "ንያይ" } // 1pl nǝyāy
return "ያዩ" // 3pl yāyu
}
// qwl / ተናገረ to speak
//
// tänagärä is actually a derived (reciprocal / D-stem) form of the root ngrǝ,
// used as the ordinary word for "to speak" in Classical Ge'ez.
// Perfect: tänagärä (3sg m); imperfect: yǝnagär (3sg m).
fn gez_qwl_perfect(slot: Int) -> String {
if slot == 0 { return "ተናገርኩ" } // 1sg tänagärku
if slot == 1 { return "ተናገርከ" } // 2sg tänagärkä
if slot == 2 { return "ተናገረ" } // 3sg m tänagärä
if slot == 3 { return "ተናገረት" } // 3sg f tänagärät
if slot == 4 { return "ተናገርነ" } // 1pl tänagärnä
return "ተናገሩ" // 3pl tänagäru
}
fn gez_qwl_imperfect(slot: Int) -> String {
if slot == 0 { return "እናገር" } // 1sg ʾǝnagär
if slot == 1 { return "ትናገር" } // 2sg tǝnagär
if slot == 2 { return "ይናገር" } // 3sg m yǝnagär
if slot == 3 { return "ትናገር" } // 3sg f tǝnagär
if slot == 4 { return "ንናገር" } // 1pl nǝnagär
return "ይናገሩ" // 3pl yǝnagäru
}
// Generic G-stem paradigm
//
// For regular strong verbs not in the lookup table.
// Ge'ez perfect suffixes: 1sg -ku, 2sg -kä, 3sg m , 3sg f -at, 1pl -nä, 3pl -u
// Ge'ez imperfect prefixes: 1sg ʾǝ-, 2sg tǝ-, 3sg m yǝ-, 3sg f tǝ-, 1pl nǝ-
fn gez_generic_perfect(base3sg: String, slot: Int) -> String {
if slot == 0 { return base3sg + "" } // -ku
if slot == 1 { return base3sg + "" } // -kä
if slot == 2 { return base3sg } //
if slot == 3 { return base3sg + "" } // -at (simplified: -t Fidel)
if slot == 4 { return base3sg + "" } // -nä
return base3sg + "" // -u (3pl)
}
fn gez_generic_imperfect(base3sg: String, slot: Int) -> String {
// base3sg is the 3sg m imperfect form (with yǝ- prefix)
// We heuristically return the stem with different prefixes.
if slot == 0 { return "" + base3sg } // ʾǝ-
if slot == 1 { return "" + base3sg } // tǝ-
if slot == 2 { return "" + base3sg } // yǝ-
if slot == 3 { return "" + base3sg } // tǝ-
if slot == 4 { return "" + base3sg } // nǝ-
return "" + base3sg + "" // yǝ- + -u (3pl)
}
// Known-verb dispatcher
fn gez_known_verb(verb: String, tense: String, slot: Int) -> String {
// kwn / ሆነ to be
if str_eq(verb, "kwn") {
return gez_conjugate_copula(tense, slot)
}
if str_eq(verb, "ሆነ") {
return gez_conjugate_copula(tense, slot)
}
if str_eq(verb, "hona") {
return gez_conjugate_copula(tense, slot)
}
// hlw / ሀሎ to exist
if str_eq(verb, "hlw") {
if str_eq(tense, "imperfect") { return gez_hlw_imperfect(slot) }
return gez_hlw_perfect(slot)
}
if str_eq(verb, "ሀሎ") {
if str_eq(tense, "imperfect") { return gez_hlw_imperfect(slot) }
return gez_hlw_perfect(slot)
}
if str_eq(verb, "hallo") {
if str_eq(tense, "imperfect") { return gez_hlw_imperfect(slot) }
return gez_hlw_perfect(slot)
}
// hbl / ሰጠ to give
if str_eq(verb, "hbl") {
if str_eq(tense, "imperfect") { return gez_hbl_imperfect(slot) }
return gez_hbl_perfect(slot)
}
if str_eq(verb, "ሰጠ") {
if str_eq(tense, "imperfect") { return gez_hbl_imperfect(slot) }
return gez_hbl_perfect(slot)
}
if str_eq(verb, "sätta") {
if str_eq(tense, "imperfect") { return gez_hbl_imperfect(slot) }
return gez_hbl_perfect(slot)
}
// rʾy / አየ to see
if str_eq(verb, "rʾy") {
if str_eq(tense, "imperfect") { return gez_ray_imperfect(slot) }
return gez_ray_perfect(slot)
}
if str_eq(verb, "አየ") {
if str_eq(tense, "imperfect") { return gez_ray_imperfect(slot) }
return gez_ray_perfect(slot)
}
if str_eq(verb, "ʾayya") {
if str_eq(tense, "imperfect") { return gez_ray_imperfect(slot) }
return gez_ray_perfect(slot)
}
// qwl / ተናገረ to speak
if str_eq(verb, "qwl") {
if str_eq(tense, "imperfect") { return gez_qwl_imperfect(slot) }
return gez_qwl_perfect(slot)
}
if str_eq(verb, "ተናገረ") {
if str_eq(tense, "imperfect") { return gez_qwl_imperfect(slot) }
return gez_qwl_perfect(slot)
}
if str_eq(verb, "tänagärä") {
if str_eq(tense, "imperfect") { return gez_qwl_imperfect(slot) }
return gez_qwl_perfect(slot)
}
return ""
}
// Main conjugation entry point
//
// gez_conjugate: conjugate a Ge'ez verb (G-stem / basic stem).
//
// verb: root (e.g. "kwn", "rʾy"), Fidel citation form (e.g. "ሆነ"),
// or transliterated 3sg perfect (e.g. "hona", "ʾayya")
// tense: "perfect" | "imperfect"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns:
// - Fidel string (Unicode) for known verbs
// - verb unchanged as safe fallback for unknown verbs
fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = gez_slot(person, number)
if gez_is_copula(verb) {
return gez_conjugate_copula(tense, slot)
}
let known: String = gez_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
return verb
}
// Noun declension
//
// gez_decline: decline a Ge'ez noun for gram_case and number.
//
// Case system (simplified for this engine):
// Nominative: base form (unmarked the Fidel form as given)
// Accusative: base + -a (for animate masculine nouns; inanimate often same)
// Construct: base form (genitive/construct; detailed vowel alternations
// are lexically irregular simplified to base here)
//
// Plural patterns (highly irregular in Ge'ez, like Arabic broken plurals):
// Common productive suffixes:
// Feminine/animate: -āt (ሀዋርያት hawāryāt apostles)
// Masculine: -ān (ነቢያን nabiyān prophets)
// Broken plurals: unpredictable must come from vocabulary layer.
// Fallback: base + "āt" ( default)
//
// noun: base singular form (Fidel or transliterated)
// gram_case: "nom" | "acc" | "construct"
// number: "singular" | "plural"
fn gez_is_fidel(noun: String) -> Bool {
// Ethiopic Unicode block starts at U+1200 ().
// We detect by checking a set of common Fidel first characters.
let n: Int = gez_str_len(noun)
if n == 0 { return false }
let first: String = str_slice(noun, 0, 1)
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
if str_eq(first, "") { return true }
return false
}
fn gez_decline(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "plural") {
// Plural: default suffix -āt (Fidel: append transliterated marker for
// non-Fidel input; for Fidel input append ት as the -āt marker)
if gez_is_fidel(noun) {
return noun + "ዎች" // simplified -woc plural marker (Ge'ez -āt )
}
return noun + "āt"
}
// Singular
if str_eq(gram_case, "acc") {
// Accusative: add -a suffix
if gez_is_fidel(noun) {
return noun + "" // accusative object marker (simplified)
}
return noun + "a"
}
// Nominative and construct: return base form
return noun
}
// Noun phrase
//
// gez_noun_phrase: produce the surface noun phrase.
//
// Ge'ez has no definite article (unlike Amharic's suffix -u/-wa/-itu).
// Definiteness is expressed through word order and context.
// The definite parameter is accepted for interface uniformity but has no
// surface effect.
//
// noun: base noun (Fidel string or transliteration)
// gram_case: "nom" | "acc" | "construct"
// number: "singular" | "plural"
// definite: "true" | "false" (no surface effect in Ge'ez)
fn gez_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return gez_decline(noun, gram_case, number)
}
// Canonical verb mapping
//
// gez_map_canonical: map cross-lingual English canonical verb labels to
// Ge'ez roots or citation forms.
fn gez_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "kwn" }
if str_eq(verb, "exist") { return "hlw" }
if str_eq(verb, "give") { return "hbl" }
if str_eq(verb, "see") { return "rʾy" }
if str_eq(verb, "speak") { return "qwl" }
if str_eq(verb, "say") { return "qwl" }
return verb
}
+26
View File
@@ -0,0 +1,26 @@
// auto-generated by elc --emit-header — do not edit
extern fn gez_str_ends(s: String, suf: String) -> Bool
extern fn gez_str_len(s: String) -> Int
extern fn gez_str_drop_last(s: String, n: Int) -> String
extern fn gez_slot(person: String, number: String) -> Int
extern fn gez_slot_g(person: String, gender: String, number: String) -> Int
extern fn gez_kwn_perfect(slot: Int) -> String
extern fn gez_kwn_imperfect(slot: Int) -> String
extern fn gez_is_copula(verb: String) -> Bool
extern fn gez_conjugate_copula(tense: String, slot: Int) -> String
extern fn gez_hlw_perfect(slot: Int) -> String
extern fn gez_hlw_imperfect(slot: Int) -> String
extern fn gez_hbl_perfect(slot: Int) -> String
extern fn gez_hbl_imperfect(slot: Int) -> String
extern fn gez_ray_perfect(slot: Int) -> String
extern fn gez_ray_imperfect(slot: Int) -> String
extern fn gez_qwl_perfect(slot: Int) -> String
extern fn gez_qwl_imperfect(slot: Int) -> String
extern fn gez_generic_perfect(base3sg: String, slot: Int) -> String
extern fn gez_generic_imperfect(base3sg: String, slot: Int) -> String
extern fn gez_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn gez_is_fidel(noun: String) -> Bool
extern fn gez_decline(noun: String, gram_case: String, number: String) -> String
extern fn gez_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn gez_map_canonical(verb: String) -> String
+637
View File
@@ -0,0 +1,637 @@
// morphology-goh.el - Old High German morphology for the NLG engine.
//
// Implements Old High German verb conjugation, noun declension, and the
// demonstrative determiner. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "goh".
//
// Language profile: code=goh, name=Old High German, morph_type=fusional,
// word_order=V2, question_strategy=inversion, script=latin,
// family=germanic.
//
// Historical note: Old High German (ca. 7501050 CE) is the earliest
// substantially attested stage of High German, preserved in texts such as the
// Hildebrandslied, the Muspilli, and Notker's translations. It is the
// ancestor of Middle High German and thence Modern German. The "High" refers
// to the geographical highlands of southern Germany, Austria, and Switzerland
// distinct from the Low German / Old Saxon dialects spoken to the north.
//
// Defining feature the Second Germanic Consonant Shift (Hochdeutsche
// Lautverschiebung), which distinguishes OHG from Gothic and Old English:
// p ff/pf (Gothic "skip" OHG "skif"; Goth "apan" → OHG "affo")
// t ss/z (Goth "watan" OHG "wazzer"; OE "tid" → OHG "zit")
// k ch (Goth "mikan" OHG "mihil")
// This shift only applies to inherited consonants and not to recent loanwords.
//
// Three genders (masculine, feminine, neuter), four cases (nominative,
// accusative, genitive, dative), and two numbers (singular, plural).
//
// Verb conjugation covered:
// Tenses: present indicative, past indicative
// Persons: first/second/third × singular/plural (slots 0-5)
// Classes: weak verbs (dental -ta past suffix the most productive class)
// Irregulars: wesan/sīn (be), habēn (have), gān (go), sehan (see),
// quethan (say), tuon (do)
// Canonical map: "be" -> "wesan"
//
// Noun declension covered:
// Strong masc a-stem (tag day): 4 cases × sg/pl
// Strong fem ō-stem (geba gift): 4 cases × sg/pl
// Strong neut a-stem (wort word): 4 cases × sg/pl
// Weak masc n-stem (boto messenger): 4 cases × sg/pl
//
// Demonstrative / definite article:
// OHG uses the demonstrative dër/diu/daz (the) which doubles as a definite
// determiner. This implementation selects the correct nominative form by
// inferred gender and passes it as a separate word before the noun.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn goh_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn goh_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Maps person × number to a 0-based paradigm index.
// 0 = 1st singular (ih)
// 1 = 2nd singular ()
// 2 = 3rd singular (ër/siu/ez)
// 3 = 1st plural (wir)
// 4 = 2nd plural (ir)
// 5 = 3rd plural (sie)
fn goh_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third person
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// English semantic-layer labels are resolved to OHG dictionary infinitives
// before conjugation.
fn goh_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wesan" }
if str_eq(verb, "have") { return "haben" }
if str_eq(verb, "go") { return "gan" }
if str_eq(verb, "see") { return "sehan" }
if str_eq(verb, "say") { return "quethan" }
if str_eq(verb, "do") { return "tuon" }
if str_eq(verb, "make") { return "tuon" }
if str_eq(verb, "come") { return "queman" }
if str_eq(verb, "give") { return "geban" }
if str_eq(verb, "know") { return "wizzan" }
if str_eq(verb, "want") { return "wellan" }
return verb
}
// Irregular verb: wesan/sīn (to be)
//
// wesan is one of the most irregular verbs in OHG. The present uses the
// bim/bin- forms (from Proto-Germanic *biju-); the past uses the was-/wāri-
// forms (from the strong past of wesan).
//
// Present indicative:
// 1sg bim/bin 2sg bist 3sg ist
// 1pl birum 2pl biruț 3pl sint
//
// Note: 2pl "biruț" the ț represents an old dental fricative; rendered here
// as "birut" (common orthographic variant in OHG manuscripts).
//
// Past indicative:
// 1sg was 2sg wāri 3sg was
// 1pl wārum 2pl wāruț 3pl wārun
fn goh_wesan_present(slot: Int) -> String {
if slot == 0 { return "bim" }
if slot == 1 { return "bist" }
if slot == 2 { return "ist" }
if slot == 3 { return "birum" }
if slot == 4 { return "birut" }
return "sint"
}
fn goh_wesan_past(slot: Int) -> String {
if slot == 0 { return "was" }
if slot == 1 { return "wari" }
if slot == 2 { return "was" }
if slot == 3 { return "warum" }
if slot == 4 { return "warut" }
return "warun"
}
// Irregular verb: habēn (to have)
//
// habēn is an athematic preterite-present verb.
//
// Present indicative:
// 1sg habem 2sg habest 3sg habet
// 1pl habemes 2pl habet 3pl habent
//
// Past (weak past -ta on stem hab-):
// 1sg habeta 2sg habetos 3sg habeta
// 1pl habetom 2pl habetot 3pl habeton
fn goh_haben_present(slot: Int) -> String {
if slot == 0 { return "habem" }
if slot == 1 { return "habest" }
if slot == 2 { return "habet" }
if slot == 3 { return "habemes" }
if slot == 4 { return "habet" }
return "habent"
}
fn goh_haben_past(slot: Int) -> String {
if slot == 0 { return "habeta" }
if slot == 1 { return "habetos" }
if slot == 2 { return "habeta" }
if slot == 3 { return "habetom" }
if slot == 4 { return "habetot" }
return "habeton"
}
// Irregular verb: gān (to go)
//
// gān is an anomalous contracted verb.
//
// Present indicative:
// 1sg gan 2sg gest 3sg get
// 1pl games 2pl gat 3pl gant
//
// Past (reduplicating/suppletive giang- forms):
// 1sg giang 2sg giangi 3sg giang
// 1pl giangum 2pl giangun 3pl giangun
fn goh_gan_present(slot: Int) -> String {
if slot == 0 { return "gan" }
if slot == 1 { return "gest" }
if slot == 2 { return "get" }
if slot == 3 { return "games" }
if slot == 4 { return "gat" }
return "gant"
}
fn goh_gan_past(slot: Int) -> String {
if slot == 0 { return "giang" }
if slot == 1 { return "giangi" }
if slot == 2 { return "giang" }
if slot == 3 { return "giangum" }
if slot == 4 { return "giangun" }
return "giangun"
}
// Irregular verb: sehan (to see)
//
// Strong class 5 (ablaut e/a). Present has i-mutation in sg forms.
//
// Present indicative:
// 1sg sihu 2sg sihist 3sg sihit
// 1pl sehemes 2pl sehet 3pl sehent
//
// Past (strong ablaut: e a):
// 1sg sah 2sg sahi 3sg sah
// 1pl sahum 2pl sahut 3pl sahun
fn goh_sehan_present(slot: Int) -> String {
if slot == 0 { return "sihu" }
if slot == 1 { return "sihist" }
if slot == 2 { return "sihit" }
if slot == 3 { return "sehemes" }
if slot == 4 { return "sehet" }
return "sehent"
}
fn goh_sehan_past(slot: Int) -> String {
if slot == 0 { return "sah" }
if slot == 1 { return "sahi" }
if slot == 2 { return "sah" }
if slot == 3 { return "sahum" }
if slot == 4 { return "sahut" }
return "sahun"
}
// Irregular verb: quethan (to say)
//
// Strong class 5 (ablaut e/a). Present sg has i-mutation (quid-).
//
// Present indicative:
// 1sg quidu 2sg quidist 3sg quidit
// 1pl quethumes 2pl quethet 3pl quethent
//
// Past (strong ablaut: e a):
// 1sg quad 2sg quadi 3sg quad
// 1pl quadum 2pl quadut 3pl quadun
fn goh_quethan_present(slot: Int) -> String {
if slot == 0 { return "quidu" }
if slot == 1 { return "quidist" }
if slot == 2 { return "quidit" }
if slot == 3 { return "quethumes" }
if slot == 4 { return "quethet" }
return "quethent"
}
fn goh_quethan_past(slot: Int) -> String {
if slot == 0 { return "quad" }
if slot == 1 { return "quadi" }
if slot == 2 { return "quad" }
if slot == 3 { return "quadum" }
if slot == 4 { return "quadut" }
return "quadun"
}
// Irregular verb: tuon (to do/make)
//
// tuon is a contracted athematic verb.
//
// Present indicative:
// 1sg tuom 2sg tuost 3sg tuot
// 1pl tuomes 2pl tuot 3pl tuont
//
// Past (weak past -ta on stem tā-):
// 1sg teta 2sg tetos 3sg teta
// 1pl tetom 2pl tetot 3pl teton
fn goh_tuon_present(slot: Int) -> String {
if slot == 0 { return "tuom" }
if slot == 1 { return "tuost" }
if slot == 2 { return "tuot" }
if slot == 3 { return "tuomes" }
if slot == 4 { return "tuot" }
return "tuont"
}
fn goh_tuon_past(slot: Int) -> String {
if slot == 0 { return "teta" }
if slot == 1 { return "tetos" }
if slot == 2 { return "teta" }
if slot == 3 { return "tetom" }
if slot == 4 { return "tetot" }
return "teton"
}
// Weak verb regular paradigm
//
// Weak verbs are the productive OHG class new verbs are regularly formed on
// this pattern. The infinitive ends in -en or -on; the weak past uses the
// dental suffix -ta/-to (a reflex of Proto-Germanic *-dō-).
//
// Class 1 weak (most common; infinitive -en, stem ends in consonant):
//
// Present indicative (stem + ending):
// 1sg -u 2sg -ist 3sg -it
// 1pl -emēs 2pl -et 3pl -ent
//
// Past indicative (stem + dental suffix):
// 1sg -ta 2sg -tōs 3sg -ta
// 1pl -tōm 2pl -tōt 3pl -tōn
//
// The stem for a regular weak verb is obtained by dropping -en (2 bytes).
fn goh_weak_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "u" }
if slot == 1 { return stem + "ist" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "emes" }
if slot == 4 { return stem + "et" }
return stem + "ent"
}
fn goh_weak_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "ta" }
if slot == 1 { return stem + "tos" }
if slot == 2 { return stem + "ta" }
if slot == 3 { return stem + "tom" }
if slot == 4 { return stem + "tot" }
return stem + "ton"
}
// goh_verb_stem: strip the infinitive ending to expose the productive stem.
// -en endings: drop 2 bytes (most weak verbs: sagēn sag-)
// -on endings: drop 2 bytes (class 2 weak: lobōn lob-)
// -an endings: drop 2 bytes (strong verbs handled as irregular; fallback)
fn goh_verb_stem(verb: String) -> String {
return goh_drop(verb, 2)
}
// goh_conjugate: main conjugation entry point
//
// verb: OHG infinitive (e.g. "sagēn", "lobōn") or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive.
fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = goh_map_canonical(verb)
let slot: Int = goh_slot(person, number)
// Irregular: wesan (to be)
if str_eq(v, "wesan") {
if str_eq(tense, "present") { return goh_wesan_present(slot) }
if str_eq(tense, "past") { return goh_wesan_past(slot) }
return v
}
// Irregular: habēn / haben (to have)
if str_eq(v, "haben") {
if str_eq(tense, "present") { return goh_haben_present(slot) }
if str_eq(tense, "past") { return goh_haben_past(slot) }
return v
}
// Also match the macron-spelled infinitive if passed directly
if str_eq(v, "haben") {
if str_eq(tense, "present") { return goh_haben_present(slot) }
if str_eq(tense, "past") { return goh_haben_past(slot) }
return v
}
// Irregular: gān / gan (to go)
if str_eq(v, "gan") {
if str_eq(tense, "present") { return goh_gan_present(slot) }
if str_eq(tense, "past") { return goh_gan_past(slot) }
return v
}
// Irregular: sehan (to see)
if str_eq(v, "sehan") {
if str_eq(tense, "present") { return goh_sehan_present(slot) }
if str_eq(tense, "past") { return goh_sehan_past(slot) }
return v
}
// Irregular: quethan (to say)
if str_eq(v, "quethan") {
if str_eq(tense, "present") { return goh_quethan_present(slot) }
if str_eq(tense, "past") { return goh_quethan_past(slot) }
return v
}
// Irregular: tuon (to do/make)
if str_eq(v, "tuon") {
if str_eq(tense, "present") { return goh_tuon_present(slot) }
if str_eq(tense, "past") { return goh_tuon_past(slot) }
return v
}
// Regular weak conjugation
let stem: String = goh_verb_stem(v)
if str_eq(tense, "present") { return goh_weak_present(stem, slot) }
if str_eq(tense, "past") { return goh_weak_past(stem, slot) }
// Final fallback: return the infinitive
return v
}
// Noun stem type detection
//
// OHG nouns are grouped by historical stem class. This engine supports four:
//
// "masc_a" strong masculine a-stem (tag, fisc, arm)
// "fem_o" strong feminine ō-stem (geba, zala, burg)
// "neut_a" strong neuter a-stem (wort, kind, tier)
// "masc_n" weak masculine n-stem (boto, hazo, namo)
//
// Detection heuristic from the citation form (nominative singular):
// ends in -o masc_n (weak masc nouns: boto, hazo)
// ends in -a fem_o (strong fem: geba, zala)
// ends in -t, -d, -n (common neuter endings) neut_a
// otherwise masc_a (default strong masculine)
//
// Callers may override by passing the stem type directly when the heuristic
// would produce the wrong class (e.g. for monosyllables with ambiguous endings).
fn goh_stem_type(noun: String) -> String {
if goh_str_ends(noun, "o") { return "masc_n" }
if goh_str_ends(noun, "a") { return "fem_o" }
if goh_str_ends(noun, "t") { return "neut_a" }
if goh_str_ends(noun, "d") { return "neut_a" }
if goh_str_ends(noun, "nd") { return "neut_a" }
return "masc_a"
}
// goh_extract_stem: derive the bare stem used as the base for all case endings.
//
// masc_a: citation form is nom sg without ending (tag = tag, fisc = fisc)
// citation IS the stem; no stripping needed
// fem_o: strip final -a (geba geb-)
// neut_a: citation form is nom/acc sg without ending (wort = wort)
// citation IS the stem; no stripping needed
// masc_n: strip final -o (boto bot-)
fn goh_extract_stem(noun: String, stype: String) -> String {
if str_eq(stype, "fem_o") { return goh_drop(noun, 1) }
if str_eq(stype, "masc_n") { return goh_drop(noun, 1) }
// masc_a and neut_a: citation IS the stem
return noun
}
// Strong masculine a-stem declension (tag day)
//
// The strong masculine a-stem is the most common OHG masculine class.
// It corresponds to the Gothic a-stem and the Latin 2nd-declension masculine.
//
// Paradigm (stem = tag-):
// Singular: nom tag acc tag gen tages dat tage
// Plural: nom taga acc taga gen tago dat tagum
fn goh_decline_masc_a_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "es" }
if str_eq(gram_case, "dative") { return stem + "e" }
return stem
}
fn goh_decline_masc_a_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "a"
}
// Strong feminine ō-stem declension (geba gift)
//
// The ō-stem feminines are the standard OHG feminine class.
// They correspond to the Gothic o-stem and Latin 1st-declension nouns.
//
// Paradigm (stem = geb-):
// Singular: nom geba acc geba gen gebā dat gebu
// Plural: nom gebā acc gebā gen gebōno dat gebōm
fn goh_decline_fem_o_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "a" }
if str_eq(gram_case, "dative") { return stem + "u" }
return stem + "a"
}
fn goh_decline_fem_o_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "ono" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "a"
}
// Strong neuter a-stem declension (wort word)
//
// Strong neuter nouns share the a-stem pattern but have identical nom/acc
// throughout (a pan-Germanic neuter feature). The plural differs from the
// masculine in the nom/acc: neuters use stem alone rather than stem + -a.
//
// Paradigm (stem = wort):
// Singular: nom wort acc wort gen wortes dat worte
// Plural: nom wort acc wort gen worto dat wortum
fn goh_decline_neut_a_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "es" }
if str_eq(gram_case, "dative") { return stem + "e" }
return stem
}
fn goh_decline_neut_a_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem
}
// Weak masculine n-stem declension (boto messenger)
//
// Weak nouns (n-stems) are characterised by the nasal -n- appearing in all
// forms except the nominative singular. They correspond to the Gothic n-stem
// and the Old English weak noun class.
//
// Paradigm (stem = bot-):
// Singular: nom boto acc boton gen boton dat boton
// Plural: nom boton acc boton gen botōno dat botōm
fn goh_decline_masc_n_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "on" }
if str_eq(gram_case, "genitive") { return stem + "on" }
if str_eq(gram_case, "dative") { return stem + "on" }
return stem + "o"
}
fn goh_decline_masc_n_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "on" }
if str_eq(gram_case, "accusative") { return stem + "on" }
if str_eq(gram_case, "genitive") { return stem + "ono" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "on"
}
// goh_decline: main declension entry point
//
// noun: OHG nominative singular form (e.g. "tag", "geba", "wort", "boto")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown stem types return the citation form
// unchanged as a safe fallback.
fn goh_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = goh_stem_type(noun)
let stem: String = goh_extract_stem(noun, stype)
if str_eq(stype, "masc_a") {
if str_eq(number, "singular") { return goh_decline_masc_a_sg(stem, gram_case) }
return goh_decline_masc_a_pl(stem, gram_case)
}
if str_eq(stype, "fem_o") {
if str_eq(number, "singular") { return goh_decline_fem_o_sg(stem, gram_case) }
return goh_decline_fem_o_pl(stem, gram_case)
}
if str_eq(stype, "neut_a") {
if str_eq(number, "singular") { return goh_decline_neut_a_sg(stem, gram_case) }
return goh_decline_neut_a_pl(stem, gram_case)
}
if str_eq(stype, "masc_n") {
if str_eq(number, "singular") { return goh_decline_masc_n_sg(stem, gram_case) }
return goh_decline_masc_n_pl(stem, gram_case)
}
// Unknown: return citation form unchanged
return noun
}
// Demonstrative article
//
// OHG uses the demonstrative pronoun dër/diu/daz as a definite determiner.
// Full declension of this pronoun is complex; this implementation provides the
// nominative forms used as determiners before nouns.
//
// Nominative forms (the most common slot for a determiner):
// Masculine sg: der Feminine sg: diu Neuter sg: daz
// Plural (all genders): die
//
// Gender is inferred from the stem type:
// masc_a masculine "der"
// fem_o feminine "diu"
// neut_a neuter "daz"
// masc_n masculine "der"
fn goh_demo_article(stype: String, number: String) -> String {
if str_eq(number, "plural") { return "die" }
if str_eq(stype, "fem_o") { return "diu" }
if str_eq(stype, "neut_a") { return "daz" }
return "der"
}
// goh_noun_phrase: noun phrase builder
//
// Assembles a declined noun with an optional OHG demonstrative article.
//
// noun: OHG nominative singular (e.g. "tag", "geba", "wort", "boto")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" to prepend the demonstrative determiner; any other value omits it
//
// Note: the demonstrative is given in its nominative singular form for
// simplicity. Full agreement would require a separate declined demonstrative
// paradigm; the NLG layer should implement that when case-agreement on the
// determiner is required.
fn goh_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let stype: String = goh_stem_type(noun)
let declined: String = goh_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let art: String = goh_demo_article(stype, number)
return art + " " + declined
}
return declined
}
+34
View File
@@ -0,0 +1,34 @@
// auto-generated by elc --emit-header — do not edit
extern fn goh_str_ends(s: String, suf: String) -> Bool
extern fn goh_drop(s: String, n: Int) -> String
extern fn goh_slot(person: String, number: String) -> Int
extern fn goh_map_canonical(verb: String) -> String
extern fn goh_wesan_present(slot: Int) -> String
extern fn goh_wesan_past(slot: Int) -> String
extern fn goh_haben_present(slot: Int) -> String
extern fn goh_haben_past(slot: Int) -> String
extern fn goh_gan_present(slot: Int) -> String
extern fn goh_gan_past(slot: Int) -> String
extern fn goh_sehan_present(slot: Int) -> String
extern fn goh_sehan_past(slot: Int) -> String
extern fn goh_quethan_present(slot: Int) -> String
extern fn goh_quethan_past(slot: Int) -> String
extern fn goh_tuon_present(slot: Int) -> String
extern fn goh_tuon_past(slot: Int) -> String
extern fn goh_weak_present(stem: String, slot: Int) -> String
extern fn goh_weak_past(stem: String, slot: Int) -> String
extern fn goh_verb_stem(verb: String) -> String
extern fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn goh_stem_type(noun: String) -> String
extern fn goh_extract_stem(noun: String, stype: String) -> String
extern fn goh_decline_masc_a_sg(stem: String, gram_case: String) -> String
extern fn goh_decline_masc_a_pl(stem: String, gram_case: String) -> String
extern fn goh_decline_fem_o_sg(stem: String, gram_case: String) -> String
extern fn goh_decline_fem_o_pl(stem: String, gram_case: String) -> String
extern fn goh_decline_neut_a_sg(stem: String, gram_case: String) -> String
extern fn goh_decline_neut_a_pl(stem: String, gram_case: String) -> String
extern fn goh_decline_masc_n_sg(stem: String, gram_case: String) -> String
extern fn goh_decline_masc_n_pl(stem: String, gram_case: String) -> String
extern fn goh_decline(noun: String, gram_case: String, number: String) -> String
extern fn goh_demo_article(stype: String, number: String) -> String
extern fn goh_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+680
View File
@@ -0,0 +1,680 @@
// morphology-got.el - Gothic morphology for the NLG engine.
//
// Implements Gothic verb conjugation and noun declension using standard
// Gothic Latin romanisation. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "got".
//
// Language profile: code=got, name=Gothic, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=gothic-latin,
// family=germanic.
//
// Historical note: Gothic is attested primarily in the 4th-century Bible
// translation by Bishop Wulfila (Ulfilas). It is the earliest substantially
// attested Germanic language and preserves many archaic features lost in later
// branches (e.g. distinct dual, mediopassive voice, four-case system).
//
// Romanisation conventions used in this file:
// þ thorn (voiced/voiceless dental fricative; like "th" in "the/thin")
// ƕ hwair (Gothic hw-, like "wh" in older English "what")
// q Gothic q (labiovelar stop, like "qu")
// ei long /iː/ (Gothic digraph)
// ai short /ɛ/ (Gothic digraph, not a diphthong)
// au short /ɔ/ (Gothic digraph, not a diphthong)
// Standard vowels: a e i u (short)
//
// Verb conjugation covered:
// Tenses: present indicative active, past indicative active
// Persons: first/second/third × singular/plural
// Classes: weak class 1 (-jan verbs), weak class 2 (-on verbs) as
// the regular productive paths
// Irregulars: wisan (be), haban (have), gaggan (go), saihwan (see),
// qiþan (say), niman (take)
// Canonical map: "be" -> "wisan"
//
// Noun declension covered:
// Cases: nominative, accusative, genitive, dative (all 4 Gothic cases)
// Stem types: a-stem masc (paradigm: dags day),
// o-stem fem (paradigm: gibo gift),
// n-stem masc (paradigm: guma man)
// Numbers: singular, plural
//
// Demonstrative / article:
// Gothic has no definite article proper. The demonstrative pronoun
// sa (masc) / so (fem) / þata (neut) functions as a near-definite
// determiner. got_noun_phrase prepends "sa" (masc) or "þo" (fem) when
// definite=true. Gender must be inferred from stem class: a-stem masc,
// o-stem fem, n-stem masc.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn got_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn got_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Gothic has singular, dual, and plural. The dual is historically attested
// for first and second person only (e.g. 1du wit, 2du jut) but is quite rare
// even in the Gothic corpus. For NLG simplicity the dual is treated as plural.
//
// 0 = 1st singular
// 1 = 2nd singular
// 2 = 3rd singular
// 3 = 1st plural
// 4 = 2nd plural
// 5 = 3rd plural
fn got_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third person
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// English semantic-layer labels are mapped to Gothic dictionary forms before
// conjugation. Dictionary forms are the infinitive.
fn got_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wisan" }
if str_eq(verb, "have") { return "haban" }
if str_eq(verb, "go") { return "gaggan" }
if str_eq(verb, "see") { return "saihwan" }
if str_eq(verb, "say") { return "qiþan" }
if str_eq(verb, "take") { return "niman" }
if str_eq(verb, "come") { return "qiman" }
if str_eq(verb, "give") { return "giban" }
if str_eq(verb, "know") { return "kunnan" }
if str_eq(verb, "want") { return "wiljan" }
return verb
}
// Irregular verb: wisan (to be)
//
// wisan is suppletive the present is formed from im / is / ist / sijum
// and the past from was / wast / was / wesum
//
// Present indicative active:
// 1sg im 2sg is 3sg ist
// 1pl sijum 2pl sijuþ 3pl sind
//
// Past indicative active:
// 1sg was 2sg wast 3sg was
// 1pl wesum 2pl wesuþ 3pl wesun
fn got_wisan_present(slot: Int) -> String {
if slot == 0 { return "im" }
if slot == 1 { return "is" }
if slot == 2 { return "ist" }
if slot == 3 { return "sijum" }
if slot == 4 { return "sijuþ" }
return "sind"
}
fn got_wisan_past(slot: Int) -> String {
if slot == 0 { return "was" }
if slot == 1 { return "wast" }
if slot == 2 { return "was" }
if slot == 3 { return "wesum" }
if slot == 4 { return "wesuþ" }
return "wesun"
}
// Irregular verb: haban (to have)
//
// Weak class 3 (-an preterite-present type).
//
// Present:
// 1sg haba 2sg habais 3sg habaiþ
// 1pl habam 2pl habaiþ 3pl haband
//
// Past (habida series weak past in -ida):
// 1sg habida 2sg habides 3sg habida
// 1pl habidum 2pl habideþ 3pl habidedun
fn got_haban_present(slot: Int) -> String {
if slot == 0 { return "haba" }
if slot == 1 { return "habais" }
if slot == 2 { return "habaiþ" }
if slot == 3 { return "habam" }
if slot == 4 { return "habaiþ" }
return "haband"
}
fn got_haban_past(slot: Int) -> String {
if slot == 0 { return "habida" }
if slot == 1 { return "habides" }
if slot == 2 { return "habida" }
if slot == 3 { return "habidum" }
if slot == 4 { return "habideþ" }
return "habidedun"
}
// Irregular verb: gaggan (to go)
//
// Strong class 7 (reduplicating); suppletive in past with iddja- forms.
//
// Present:
// 1sg gagga 2sg gaggis 3sg gaggiþ
// 1pl gagam 2pl gagiþ 3pl gaggand
//
// Past (iddja series suppletive):
// 1sg iddja 2sg iddjēs 3sg iddja
// 1pl iddjēdum 2pl iddjēduþ 3pl iddjēdun
fn got_gaggan_present(slot: Int) -> String {
if slot == 0 { return "gagga" }
if slot == 1 { return "gaggis" }
if slot == 2 { return "gaggiþ" }
if slot == 3 { return "gagam" }
if slot == 4 { return "gagiþ" }
return "gaggand"
}
fn got_gaggan_past(slot: Int) -> String {
if slot == 0 { return "iddja" }
if slot == 1 { return "iddjēs" }
if slot == 2 { return "iddja" }
if slot == 3 { return "iddjēdum" }
if slot == 4 { return "iddjēduþ" }
return "iddjēdun"
}
// Irregular verb: saihwan (to see)
//
// Strong class 5 (saiƕan / saihwan). Present stem saihw-; past stem sahw-.
//
// Present:
// 1sg saihwa 2sg saihwis 3sg saihwiþ
// 1pl saihwam 2pl saihwiþ 3pl saihwand
//
// Past (ablaut: aia, strong past):
// 1sg sahw 2sg sahwt 3sg sahw
// 1pl sehwum 2pl sehwuþ 3pl sehwun
fn got_saihwan_present(slot: Int) -> String {
if slot == 0 { return "saihwa" }
if slot == 1 { return "saihwis" }
if slot == 2 { return "saihwiþ" }
if slot == 3 { return "saihwam" }
if slot == 4 { return "saihwiþ" }
return "saihwand"
}
fn got_saihwan_past(slot: Int) -> String {
if slot == 0 { return "sahw" }
if slot == 1 { return "sahwt" }
if slot == 2 { return "sahw" }
if slot == 3 { return "sehwum" }
if slot == 4 { return "sehwuþ" }
return "sehwun"
}
// Irregular verb: qiþan (to say)
//
// Strong class 5 (i-ablaut). Present stem qiþ-; past stem qaþ-.
//
// Present:
// 1sg qiþa 2sg qiþis 3sg qiþiþ
// 1pl qiþam 2pl qiþiþ 3pl qiþand
//
// Past (ablaut: ia):
// 1sg qaþ 2sg qast 3sg qaþ
// 1pl qēþum 2pl qēþuþ 3pl qēþun
fn got_qithan_present(slot: Int) -> String {
if slot == 0 { return "qiþa" }
if slot == 1 { return "qiþis" }
if slot == 2 { return "qiþiþ" }
if slot == 3 { return "qiþam" }
if slot == 4 { return "qiþiþ" }
return "qiþand"
}
fn got_qithan_past(slot: Int) -> String {
if slot == 0 { return "qaþ" }
if slot == 1 { return "qast" }
if slot == 2 { return "qaþ" }
if slot == 3 { return "qēþum" }
if slot == 4 { return "qēþuþ" }
return "qēþun"
}
// Irregular verb: niman (to take)
//
// Strong class 4 (sonorant stem; i-ablaut in present, a-ablaut in past).
//
// Present:
// 1sg nima 2sg nimis 3sg nimiþ
// 1pl nimam 2pl nimiþ 3pl nimand
//
// Past (ablaut: ia):
// 1sg nam 2sg namt 3sg nam
// 1pl nēmum 2pl nēmuþ 3pl nēmun
fn got_niman_present(slot: Int) -> String {
if slot == 0 { return "nima" }
if slot == 1 { return "nimis" }
if slot == 2 { return "nimiþ" }
if slot == 3 { return "nimam" }
if slot == 4 { return "nimiþ" }
return "nimand"
}
fn got_niman_past(slot: Int) -> String {
if slot == 0 { return "nam" }
if slot == 1 { return "namt" }
if slot == 2 { return "nam" }
if slot == 3 { return "nēmum" }
if slot == 4 { return "nēmuþ" }
return "nēmun"
}
// Weak class 1 (-jan verbs): regular conjugation
//
// Class 1 weak verbs are the most productive Gothic verb class. The
// infinitive ends in -jan; the stem is obtained by stripping -jan.
//
// Present indicative active:
// 1sg stem + -a (nasjan -> nasja)
// 2sg stem + -is (nasjis)
// 3sg stem + -iþ (nasjiþ)
// 1pl stem + -jam (nasjam)
// 2pl stem + -jiþ (nasjiþ)
// 3pl stem + -jand (nasjand)
//
// Past indicative active (weak past -ida):
// 1sg stem + -ida (nasjida)
// 2sg stem + -ides (nasjides)
// 3sg stem + -ida (nasjida)
// 1pl stem + -idum (nasjidum)
// 2pl stem + -ideþ (nasjideþ)
// 3pl stem + -idedun (nasjidedun)
fn got_wk1_present_ending(slot: Int) -> String {
if slot == 0 { return "a" }
if slot == 1 { return "is" }
if slot == 2 { return "" }
if slot == 3 { return "jam" }
if slot == 4 { return "jiþ" }
return "jand"
}
fn got_wk1_past_ending(slot: Int) -> String {
if slot == 0 { return "ida" }
if slot == 1 { return "ides" }
if slot == 2 { return "ida" }
if slot == 3 { return "idum" }
if slot == 4 { return "ideþ" }
return "idedun"
}
fn got_wk1_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + got_wk1_present_ending(slot)
}
if str_eq(tense, "past") {
return stem + got_wk1_past_ending(slot)
}
return stem
}
// Weak class 2 (-on verbs): regular conjugation
//
// Class 2 weak verbs. Infinitive ends in -on; stem = drop -on.
// This class corresponds roughly to OE -ian / OHG -on denominatives.
//
// Present indicative active:
// 1sg stem + -o (salbon -> salbo)
// 2sg stem + -os (salbos)
// 3sg stem + -oþ (salboþ)
// 1pl stem + -om (salbom)
// 2pl stem + -oþ (salboþ)
// 3pl stem + -ond (salbond)
//
// Past indicative active (weak past -oda):
// 1sg stem + -oda (salboda)
// 2sg stem + -odes (salbodes)
// 3sg stem + -oda (salboda)
// 1pl stem + -odum (salbodum)
// 2pl stem + -odeþ (salbodeþ)
// 3pl stem + -odedun (salbodedun)
fn got_wk2_present_ending(slot: Int) -> String {
if slot == 0 { return "o" }
if slot == 1 { return "os" }
if slot == 2 { return "" }
if slot == 3 { return "om" }
if slot == 4 { return "" }
return "ond"
}
fn got_wk2_past_ending(slot: Int) -> String {
if slot == 0 { return "oda" }
if slot == 1 { return "odes" }
if slot == 2 { return "oda" }
if slot == 3 { return "odum" }
if slot == 4 { return "odeþ" }
return "odedun"
}
fn got_wk2_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + got_wk2_present_ending(slot)
}
if str_eq(tense, "past") {
return stem + got_wk2_past_ending(slot)
}
return stem
}
// Infinitive class detection
//
// Identifies the verb class from the infinitive ending so that the correct
// regular paradigm can be applied for verbs not in the irregular table.
//
// ends in -jan -> weak class 1 ("wk1")
// ends in -on -> weak class 2 ("wk2")
// otherwise -> default to weak class 1 (most productive class)
fn got_verb_class(verb: String) -> String {
if got_str_ends(verb, "jan") { return "wk1" }
if got_str_ends(verb, "on") { return "wk2" }
return "wk1"
}
// got_verb_stem: strip the infinitive suffix to expose the productive stem.
//
// wk1: strip -jan (3 bytes all ASCII)
// wk2: strip -on (2 bytes)
fn got_verb_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "wk1") { return got_str_drop_last(verb, 3) }
if str_eq(vclass, "wk2") { return got_str_drop_last(verb, 2) }
return got_str_drop_last(verb, 2)
}
// got_conjugate: main conjugation entry point
//
// verb: Gothic infinitive in Latin romanisation (e.g. "wisan", "nasjan")
// or English canonical label ("be", "go", "have")
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural" (or "dual" treated as plural)
//
// Returns the inflected form. Falls back to the infinitive for unknown
// tenses rather than crashing.
fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = got_map_canonical(verb)
let slot: Int = got_slot(person, number)
// Irregular: wisan (to be)
if str_eq(v, "wisan") {
if str_eq(tense, "present") { return got_wisan_present(slot) }
if str_eq(tense, "past") { return got_wisan_past(slot) }
return v
}
// Irregular: haban (to have)
if str_eq(v, "haban") {
if str_eq(tense, "present") { return got_haban_present(slot) }
if str_eq(tense, "past") { return got_haban_past(slot) }
return v
}
// Irregular: gaggan (to go)
if str_eq(v, "gaggan") {
if str_eq(tense, "present") { return got_gaggan_present(slot) }
if str_eq(tense, "past") { return got_gaggan_past(slot) }
return v
}
// Irregular: saihwan (to see)
if str_eq(v, "saihwan") {
if str_eq(tense, "present") { return got_saihwan_present(slot) }
if str_eq(tense, "past") { return got_saihwan_past(slot) }
return v
}
// Irregular: qiþan (to say)
if str_eq(v, "qiþan") {
if str_eq(tense, "present") { return got_qithan_present(slot) }
if str_eq(tense, "past") { return got_qithan_past(slot) }
return v
}
// Irregular: niman (to take)
if str_eq(v, "niman") {
if str_eq(tense, "present") { return got_niman_present(slot) }
if str_eq(tense, "past") { return got_niman_past(slot) }
return v
}
// Regular weak conjugation
let vclass: String = got_verb_class(v)
let stem: String = got_verb_stem(v, vclass)
if str_eq(vclass, "wk1") {
return got_wk1_conjugate(stem, tense, slot)
}
if str_eq(vclass, "wk2") {
return got_wk2_conjugate(stem, tense, slot)
}
// Final fallback: return the infinitive
return v
}
// a-stem masculine paradigm (dags day)
//
// The a-stem masculine is the largest Gothic noun class, corresponding to the
// Proto-Germanic *-a- stems (OE -as, German -e plurals).
//
// Nominative singular: dags
// Stem: dag- (strip final -s from nom sg)
//
// Singular: nom dags acc dag gen dagis dat daga
// Plural: nom dagos acc dagans gen dage dat dagam
//
// Note: some sources list acc sg as "dag" (without -n), consistent with
// Gothic nomacc distinction in masculine a-stems.
fn got_decline_a_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "s" }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "is" }
if str_eq(gram_case, "dative") { return stem + "a" }
return stem + "s"
}
fn got_decline_a_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "os" }
if str_eq(gram_case, "accusative") { return stem + "ans" }
if str_eq(gram_case, "genitive") { return stem + "e" }
if str_eq(gram_case, "dative") { return stem + "am" }
return stem + "os"
}
// o-stem feminine paradigm (gibo gift)
//
// The o-stem feminines correspond to Proto-Germanic *-ō- stems. The
// nominative singular ends in -o; nom and acc plural are identical.
//
// Singular: nom gibo acc giba gen gibos dat gibai
// Plural: nom gibos acc gibos gen gibo dat gibom
fn got_decline_o_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "os" }
if str_eq(gram_case, "dative") { return stem + "ai" }
return stem + "o"
}
fn got_decline_o_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "os" }
if str_eq(gram_case, "accusative") { return stem + "os" }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "os"
}
// n-stem masculine paradigm (guma man)
//
// The n-stems (weak nouns) are characterised by -n- appearing in all cases
// except the nominative singular. They are sometimes called "weak nouns"
// by analogy with Old English grammar.
//
// Singular: nom guma acc guman gen gumins dat gumin
// Plural: nom gumans acc gumans gen gumane dat gumam
fn got_decline_n_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "ins" }
if str_eq(gram_case, "dative") { return stem + "in" }
return stem + "a"
}
fn got_decline_n_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ans" }
if str_eq(gram_case, "accusative") { return stem + "ans" }
if str_eq(gram_case, "genitive") { return stem + "ane" }
if str_eq(gram_case, "dative") { return stem + "am" }
return stem + "ans"
}
// Stem type detection
//
// Infers the stem class from the nominative singular form.
//
// Strategy:
// ends in -s and previous char is not a vowel -> a-stem masc (dags type)
// ends in -o -> o-stem fem (gibo type)
// ends in -a -> n-stem masc (guma type)
// ends in -s and previous char is a vowel -> could be various; default a-stem
// otherwise -> default to a-stem
//
// For the common case where the caller passes the paradigm lemma directly:
// "dags" -> "a", "gibo" -> "o", "guma" -> "n"
fn got_stem_type(noun: String) -> String {
if got_str_ends(noun, "o") { return "o" }
if got_str_ends(noun, "a") { return "n" }
if got_str_ends(noun, "s") { return "a" }
// Fallback for other forms
return "a"
}
// got_extract_stem: strip the nom-sg suffix to obtain the bare stem.
//
// a-stem: strip final -s (dags -> dag)
// o-stem: strip final -o (gibo -> gib)
// n-stem: strip final -a (guma -> gum)
//
// All suffix bytes are single-byte ASCII, so str_len gives byte=char counts.
fn got_extract_stem(noun: String, stype: String) -> String {
let n: Int = str_len(noun)
// All three suffixes are 1 byte each
return str_slice(noun, 0, n - 1)
}
// Gender inference for article selection
//
// The demonstrative-article used in got_noun_phrase depends on gender.
// Gender correlates with stem class in Gothic (imperfect but useful heuristic):
// a-stem -> masculine -> sa
// o-stem -> feminine -> þo (nom sg of feminine demonstrative)
// n-stem -> masculine -> sa
//
// Neuter a-stems (like "waurd" word) exist but are not distinguished here;
// the NLG layer is expected to pass explicit gender when the distinction matters.
fn got_demo_article(stype: String) -> String {
if str_eq(stype, "o") { return "þo" }
// a-stem and n-stem both masculine
return "sa"
}
// got_decline: main declension entry point
//
// noun: nominative singular in Gothic Latin romanisation
// (e.g. "dags", "gibo", "guma")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown stem types return the nominative
// singular unchanged rather than producing garbage output.
fn got_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = got_stem_type(noun)
let stem: String = got_extract_stem(noun, stype)
if str_eq(stype, "a") {
if str_eq(number, "singular") { return got_decline_a_stem_sg(stem, gram_case) }
return got_decline_a_stem_pl(stem, gram_case)
}
if str_eq(stype, "o") {
if str_eq(number, "singular") { return got_decline_o_stem_sg(stem, gram_case) }
return got_decline_o_stem_pl(stem, gram_case)
}
if str_eq(stype, "n") {
if str_eq(number, "singular") { return got_decline_n_stem_sg(stem, gram_case) }
return got_decline_n_stem_pl(stem, gram_case)
}
// Unknown: return the nominative singular unchanged
return noun
}
// got_noun_phrase: noun phrase builder
//
// Gothic has no definite article. The demonstrative pronouns sa (masc) /
// so (fem) / þata (neut) are used in contexts where a definite-article-like
// meaning is required (closely following Greek ὁ/ἡ/τό in Wulfila's translation).
//
// When definite=true this function prepends the gender-appropriate demonstrative
// in its nominative singular form. For non-nominative cases the demonstrative
// should ideally be declined to match; this implementation prepends the nom-sg
// form as a simplification suitable for NLG output. Callers requiring full
// demonstrative agreement should call got_decline_demonstrative separately.
//
// noun: nominative singular Gothic noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" prepends the demonstrative; any other value omits it
fn got_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let declined: String = got_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let stype: String = got_stem_type(noun)
let article: String = got_demo_article(stype)
return article + " " + declined
}
return declined
}
+37
View File
@@ -0,0 +1,37 @@
// auto-generated by elc --emit-header — do not edit
extern fn got_str_ends(s: String, suf: String) -> Bool
extern fn got_str_drop_last(s: String, n: Int) -> String
extern fn got_slot(person: String, number: String) -> Int
extern fn got_map_canonical(verb: String) -> String
extern fn got_wisan_present(slot: Int) -> String
extern fn got_wisan_past(slot: Int) -> String
extern fn got_haban_present(slot: Int) -> String
extern fn got_haban_past(slot: Int) -> String
extern fn got_gaggan_present(slot: Int) -> String
extern fn got_gaggan_past(slot: Int) -> String
extern fn got_saihwan_present(slot: Int) -> String
extern fn got_saihwan_past(slot: Int) -> String
extern fn got_qithan_present(slot: Int) -> String
extern fn got_qithan_past(slot: Int) -> String
extern fn got_niman_present(slot: Int) -> String
extern fn got_niman_past(slot: Int) -> String
extern fn got_wk1_present_ending(slot: Int) -> String
extern fn got_wk1_past_ending(slot: Int) -> String
extern fn got_wk1_conjugate(stem: String, tense: String, slot: Int) -> String
extern fn got_wk2_present_ending(slot: Int) -> String
extern fn got_wk2_past_ending(slot: Int) -> String
extern fn got_wk2_conjugate(stem: String, tense: String, slot: Int) -> String
extern fn got_verb_class(verb: String) -> String
extern fn got_verb_stem(verb: String, vclass: String) -> String
extern fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn got_decline_a_stem_sg(stem: String, gram_case: String) -> String
extern fn got_decline_a_stem_pl(stem: String, gram_case: String) -> String
extern fn got_decline_o_stem_sg(stem: String, gram_case: String) -> String
extern fn got_decline_o_stem_pl(stem: String, gram_case: String) -> String
extern fn got_decline_n_stem_sg(stem: String, gram_case: String) -> String
extern fn got_decline_n_stem_pl(stem: String, gram_case: String) -> String
extern fn got_stem_type(noun: String) -> String
extern fn got_extract_stem(noun: String, stype: String) -> String
extern fn got_demo_article(stype: String) -> String
extern fn got_decline(noun: String, gram_case: String, number: String) -> String
extern fn got_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+756
View File
@@ -0,0 +1,756 @@
// morphology-grc.el - Ancient Greek morphology for the NLG engine.
//
// Implements polytonic Ancient Greek verb conjugation, noun declension, and
// the definite article. Designed as a companion to morphology.el and called
// by the engine when the language profile code is "grc".
//
// Language profile: code=grc, name=Ancient Greek, morph_type=fusional,
// word_order=SOV, question_strategy=particle, script=greek, family=hellenic.
//
// Typology note: Ancient Greek is a heavily inflected synthetic language with
// rich morphophonology. Nouns carry gender, case, and number in fused endings.
// Verbs encode tense, aspect, voice, mood, person, and number. The augment
// (ε-) marks past indicative. All polytonic diacritics are preserved in
// string literals; comparisons use exact Unicode equality.
//
// Verb conjugation covered:
// Tenses: present, imperfect, aorist, future
// Persons: first/second/third × singular/plural (slots 0-5)
// Class: thematic () verbs
// Irregulars: εἰναι (be), ἔχειν (have), λέγειν (say), ὁράω (see),
// ἔρχεσθαι (come/go)
// Canonical map: "be" -> "εἰναι"
//
// Noun declension covered:
// Cases: nominative, accusative, genitive, dative, vocative
// Declensions: 1st (-α/-η fem), 2nd masc (-ος), 2nd neut (-ον), 3rd (base)
//
// Article: full 24-form table for ὁ/ἡ/τό (gender × case × number).
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn grc_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn grc_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn grc_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn grc_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn grc_str_last3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, n - 3, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index used in all paradigm tables.
// 0 = 1st singular (ἐγώ)
// 1 = 2nd singular (σύ)
// 2 = 3rd singular (αὐτός/αὐτή/αὐτό)
// 3 = 1st plural (ἡμεῖς)
// 4 = 2nd plural (ὑμεῖς)
// 5 = 3rd plural (αὐτοί)
//
// Dual number is not handled; dual inputs fall through to plural.
fn grc_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// The semantic layer may pass English canonical labels. Map these to the
// Ancient Greek infinitive (or dictionary citation form) before conjugation.
fn grc_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "εἰναι" }
if str_eq(verb, "have") { return "ἔχειν" }
if str_eq(verb, "say") { return "λέγειν" }
if str_eq(verb, "see") { return "ὁράω" }
if str_eq(verb, "come") { return "ἔρχεσθαι" }
if str_eq(verb, "go") { return "ἔρχεσθαι" }
if str_eq(verb, "know") { return "γιγνώσκειν" }
if str_eq(verb, "write") { return "γράφειν" }
if str_eq(verb, "hear") { return "ἀκούειν" }
if str_eq(verb, "want") { return "βούλεσθαι" }
if str_eq(verb, "do") { return "ποιεῖν" }
if str_eq(verb, "make") { return "ποιεῖν" }
return verb
}
// Irregular: εἰναι (to be)
//
// Present indicative active: εἰμί εἶ ἐστί ἐσμέν ἐστέ εἰσί
// Imperfect: ἦν ἦσθα ἦν ἦμεν ἦτε ἦσαν
// Future: ἔσομαι ἔσῃ ἔσται ἐσόμεθα ἔσεσθε ἔσονται
// Aorist: no aorist of εἰμί; use imperfect as fallback
fn grc_einai_present(slot: Int) -> String {
if slot == 0 { return "εἰμί" }
if slot == 1 { return "εἶ" }
if slot == 2 { return "ἐστί" }
if slot == 3 { return "ἐσμέν" }
if slot == 4 { return "ἐστέ" }
return "εἰσί"
}
fn grc_einai_imperfect(slot: Int) -> String {
if slot == 0 { return "ἦν" }
if slot == 1 { return "ἦσθα" }
if slot == 2 { return "ἦν" }
if slot == 3 { return "ἦμεν" }
if slot == 4 { return "ἦτε" }
return "ἦσαν"
}
fn grc_einai_future(slot: Int) -> String {
if slot == 0 { return "ἔσομαι" }
if slot == 1 { return "ἔσῃ" }
if slot == 2 { return "ἔσται" }
if slot == 3 { return "ἐσόμεθα" }
if slot == 4 { return "ἔσεσθε" }
return "ἔσονται"
}
// Irregular: ἔχειν (to have)
//
// Present: ἔχω ἔχεις ἔχει ἔχομεν ἔχετε ἔχουσι
// Imperfect: εἶχον εἶχες εἶχε εἴχομεν εἴχετε εἶχον
// Aorist: ἔσχον ἔσχες ἔσχε ἔσχομεν ἔσχετε ἔσχον
// Future: ἕξω ἕξεις ἕξει ἕξομεν ἕξετε ἕξουσι
fn grc_echein_present(slot: Int) -> String {
if slot == 0 { return "ἔχω" }
if slot == 1 { return "ἔχεις" }
if slot == 2 { return "ἔχει" }
if slot == 3 { return "ἔχομεν" }
if slot == 4 { return "ἔχετε" }
return "ἔχουσι"
}
fn grc_echein_imperfect(slot: Int) -> String {
if slot == 0 { return "εἶχον" }
if slot == 1 { return "εἶχες" }
if slot == 2 { return "εἶχε" }
if slot == 3 { return "εἴχομεν" }
if slot == 4 { return "εἴχετε" }
return "εἶχον"
}
fn grc_echein_aorist(slot: Int) -> String {
if slot == 0 { return "ἔσχον" }
if slot == 1 { return "ἔσχες" }
if slot == 2 { return "ἔσχε" }
if slot == 3 { return "ἔσχομεν" }
if slot == 4 { return "ἔσχετε" }
return "ἔσχον"
}
fn grc_echein_future(slot: Int) -> String {
if slot == 0 { return "ἕξω" }
if slot == 1 { return "ἕξεις" }
if slot == 2 { return "ἕξει" }
if slot == 3 { return "ἕξομεν" }
if slot == 4 { return "ἕξετε" }
return "ἕξουσι"
}
// Irregular: λέγειν (to say)
//
// Present: λέγω λέγεις λέγει λέγομεν λέγετε λέγουσι
// Imperfect: ἔλεγον ἔλεγες ἔλεγε ἐλέγομεν ἐλέγετε ἔλεγον
// Aorist: εἶπον εἶπες εἶπε εἴπομεν εἴπετε εἶπον
// Future: λέξω λέξεις λέξει λέξομεν λέξετε λέξουσι
fn grc_legein_present(slot: Int) -> String {
if slot == 0 { return "λέγω" }
if slot == 1 { return "λέγεις" }
if slot == 2 { return "λέγει" }
if slot == 3 { return "λέγομεν" }
if slot == 4 { return "λέγετε" }
return "λέγουσι"
}
fn grc_legein_imperfect(slot: Int) -> String {
if slot == 0 { return "ἔλεγον" }
if slot == 1 { return "ἔλεγες" }
if slot == 2 { return "ἔλεγε" }
if slot == 3 { return "ἐλέγομεν" }
if slot == 4 { return "ἐλέγετε" }
return "ἔλεγον"
}
fn grc_legein_aorist(slot: Int) -> String {
if slot == 0 { return "εἶπον" }
if slot == 1 { return "εἶπες" }
if slot == 2 { return "εἶπε" }
if slot == 3 { return "εἴπομεν" }
if slot == 4 { return "εἴπετε" }
return "εἶπον"
}
fn grc_legein_future(slot: Int) -> String {
if slot == 0 { return "λέξω" }
if slot == 1 { return "λέξεις" }
if slot == 2 { return "λέξει" }
if slot == 3 { return "λέξομεν" }
if slot == 4 { return "λέξετε" }
return "λέξουσι"
}
// Irregular: ὁράω (to see)
//
// Present: ὁράω ὁράς ὁρᾷ ὁρῶμεν ὁρᾶτε ὁρῶσι
// Imperfect: ἑώρων ἑώρας ἑώρα ἑωρῶμεν ἑωρᾶτε ἑώρων
// Aorist: εἶδον εἶδες εἶδε εἴδομεν εἴδετε εἶδον
// Future: ὄψομαι ὄψῃ ὄψεται ὀψόμεθα ὄψεσθε ὄψονται
fn grc_horao_present(slot: Int) -> String {
if slot == 0 { return "ὁράω" }
if slot == 1 { return "ὁράς" }
if slot == 2 { return "ὁρᾷ" }
if slot == 3 { return "ὁρῶμεν" }
if slot == 4 { return "ὁρᾶτε" }
return "ὁρῶσι"
}
fn grc_horao_imperfect(slot: Int) -> String {
if slot == 0 { return "ἑώρων" }
if slot == 1 { return "ἑώρας" }
if slot == 2 { return "ἑώρα" }
if slot == 3 { return "ἑωρῶμεν" }
if slot == 4 { return "ἑωρᾶτε" }
return "ἑώρων"
}
fn grc_horao_aorist(slot: Int) -> String {
if slot == 0 { return "εἶδον" }
if slot == 1 { return "εἶδες" }
if slot == 2 { return "εἶδε" }
if slot == 3 { return "εἴδομεν" }
if slot == 4 { return "εἴδετε" }
return "εἶδον"
}
fn grc_horao_future(slot: Int) -> String {
if slot == 0 { return "ὄψομαι" }
if slot == 1 { return "ὄψῃ" }
if slot == 2 { return "ὄψεται" }
if slot == 3 { return "ὀψόμεθα" }
if slot == 4 { return "ὄψεσθε" }
return "ὄψονται"
}
// Irregular: ἔρχεσθαι (to come / to go)
//
// Present: ἔρχομαι ἔρχῃ ἔρχεται ἐρχόμεθα ἔρχεσθε ἔρχονται
// Imperfect: ἠρχόμην ἤρχου ἤρχετο ἠρχόμεθα ἤρχεσθε ἤρχοντο
// Aorist: ἦλθον ἦλθες ἦλθε ἤλθομεν ἤλθετε ἦλθον
// Future: εἶμι εἶ εἶσι ἴμεν ἴτε ἴασι
fn grc_erchesthai_present(slot: Int) -> String {
if slot == 0 { return "ἔρχομαι" }
if slot == 1 { return "ἔρχῃ" }
if slot == 2 { return "ἔρχεται" }
if slot == 3 { return "ἐρχόμεθα" }
if slot == 4 { return "ἔρχεσθε" }
return "ἔρχονται"
}
fn grc_erchesthai_imperfect(slot: Int) -> String {
if slot == 0 { return "ἠρχόμην" }
if slot == 1 { return "ἤρχου" }
if slot == 2 { return "ἤρχετο" }
if slot == 3 { return "ἠρχόμεθα" }
if slot == 4 { return "ἤρχεσθε" }
return "ἤρχοντο"
}
fn grc_erchesthai_aorist(slot: Int) -> String {
if slot == 0 { return "ἦλθον" }
if slot == 1 { return "ἦλθες" }
if slot == 2 { return "ἦλθε" }
if slot == 3 { return "ἤλθομεν" }
if slot == 4 { return "ἤλθετε" }
return "ἦλθον"
}
fn grc_erchesthai_future(slot: Int) -> String {
if slot == 0 { return "εἶμι" }
if slot == 1 { return "εἶ" }
if slot == 2 { return "εἶσι" }
if slot == 3 { return "ἴμεν" }
if slot == 4 { return "ἴτε" }
return "ἴασι"
}
// Thematic () present active endings
//
// Slot: 0 1 2 3 4 5
// -εις -ει -ομεν -ετε -ουσι
//
// These are attached to the present stem.
fn grc_thematic_present_ending(slot: Int) -> String {
if slot == 0 { return "ω" }
if slot == 1 { return "εις" }
if slot == 2 { return "ει" }
if slot == 3 { return "ομεν" }
if slot == 4 { return "ετε" }
return "ουσι"
}
// Thematic imperfect active endings
//
// Imperfect = augment (ἐ-) + stem + secondary endings
// Slot: 0 1 2 3 4 5
// -ον -ες -ομεν -ετε -ον
fn grc_thematic_imperfect_ending(slot: Int) -> String {
if slot == 0 { return "ον" }
if slot == 1 { return "ες" }
if slot == 2 { return "ε" }
if slot == 3 { return "ομεν" }
if slot == 4 { return "ετε" }
return "ον"
}
// Thematic future active endings
//
// Future = stem + σ + thematic vowel + primary endings.
// For most verbs: stem + σ serves as the future stem, then present endings.
// Slot: 0 1 2 3 4 5
// -σω -σεις -σει -σομεν -σετε -σουσι
fn grc_thematic_future_ending(slot: Int) -> String {
if slot == 0 { return "σω" }
if slot == 1 { return "σεις" }
if slot == 2 { return "σει" }
if slot == 3 { return "σομεν" }
if slot == 4 { return "σετε" }
return "σουσι"
}
// Weak (first) aorist active endings
//
// Weak aorist = augment (ἐ-) + stem + σ + aorist endings
// Slot: 0 1 2 3 4 5
// -σα -σας -σε -σαμεν -σατε -σαν
fn grc_weak_aorist_ending(slot: Int) -> String {
if slot == 0 { return "σα" }
if slot == 1 { return "σας" }
if slot == 2 { return "σε" }
if slot == 3 { return "σαμεν" }
if slot == 4 { return "σατε" }
return "σαν"
}
// Stem extraction
//
// Strip the citation-form ending to recover the present stem.
// -ειν -> strip 3 chars (λύειν -> λύ-)
// -> strip 1 char (λύω -> λύ-)
// -αω -> strip 2 chars (ὁράω -> ὁρ-; handled as irregular above)
// -εω -> strip 2 chars (contracted -εω verbs)
// otherwise: return as-is (already a stem or unrecognised form)
fn grc_present_stem(verb: String) -> String {
if grc_str_ends(verb, "ειν") {
return grc_str_drop_last(verb, 3)
}
if grc_str_ends(verb, "αω") {
return grc_str_drop_last(verb, 2)
}
if grc_str_ends(verb, "εω") {
return grc_str_drop_last(verb, 2)
}
if grc_str_ends(verb, "ω") {
return grc_str_drop_last(verb, 1)
}
return verb
}
// grc_conjugate: main conjugation entry point
//
// verb: Greek infinitive/citation form or English canonical label
// tense: "present" | "imperfect" | "aorist" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the citation form for unknown
// tenses or unrecognised verbs rather than crashing.
fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = grc_map_canonical(verb)
let slot: Int = grc_slot(person, number)
// Irregulars
if str_eq(v, "εἰναι") {
if str_eq(tense, "present") { return grc_einai_present(slot) }
if str_eq(tense, "imperfect") { return grc_einai_imperfect(slot) }
if str_eq(tense, "aorist") { return grc_einai_imperfect(slot) }
if str_eq(tense, "future") { return grc_einai_future(slot) }
return v
}
if str_eq(v, "ἔχειν") {
if str_eq(tense, "present") { return grc_echein_present(slot) }
if str_eq(tense, "imperfect") { return grc_echein_imperfect(slot) }
if str_eq(tense, "aorist") { return grc_echein_aorist(slot) }
if str_eq(tense, "future") { return grc_echein_future(slot) }
return v
}
if str_eq(v, "λέγειν") {
if str_eq(tense, "present") { return grc_legein_present(slot) }
if str_eq(tense, "imperfect") { return grc_legein_imperfect(slot) }
if str_eq(tense, "aorist") { return grc_legein_aorist(slot) }
if str_eq(tense, "future") { return grc_legein_future(slot) }
return v
}
if str_eq(v, "ὁράω") {
if str_eq(tense, "present") { return grc_horao_present(slot) }
if str_eq(tense, "imperfect") { return grc_horao_imperfect(slot) }
if str_eq(tense, "aorist") { return grc_horao_aorist(slot) }
if str_eq(tense, "future") { return grc_horao_future(slot) }
return v
}
if str_eq(v, "ἔρχεσθαι") {
if str_eq(tense, "present") { return grc_erchesthai_present(slot) }
if str_eq(tense, "imperfect") { return grc_erchesthai_imperfect(slot) }
if str_eq(tense, "aorist") { return grc_erchesthai_aorist(slot) }
if str_eq(tense, "future") { return grc_erchesthai_future(slot) }
return v
}
// Regular thematic conjugation
let stem: String = grc_present_stem(v)
if str_eq(tense, "present") {
return stem + grc_thematic_present_ending(slot)
}
if str_eq(tense, "imperfect") {
// Augment: prefix ἐ- to the stem
return "" + stem + grc_thematic_imperfect_ending(slot)
}
if str_eq(tense, "future") {
return stem + grc_thematic_future_ending(slot)
}
if str_eq(tense, "aorist") {
// Weak (sigmatic) aorist: ἐ- + stem + σ endings
return "" + stem + grc_weak_aorist_ending(slot)
}
// Unknown tense: return citation form unchanged
return v
}
// Declension detection
//
// Infer Greek declension class from the nominative singular ending.
//
// ends in -ος -> 2nd declension masculine
// ends in -ον -> 2nd declension neuter
// ends in -α -> 1st declension feminine (alpha-stem)
// ends in -> 1st declension feminine (eta-stem)
// otherwise -> 3rd declension (consonant stem; too varied for full tables)
fn grc_declension(noun: String) -> String {
if grc_str_ends(noun, "ος") { return "2m" }
if grc_str_ends(noun, "ον") { return "2n" }
if grc_str_ends(noun, "α") { return "1a" }
if grc_str_ends(noun, "η") { return "1e" }
return "3"
}
// 2nd declension masculine: -ος nouns
//
// Stem: remove -ος (2 chars)
// Singular: nom -ος gen -ου dat -ῳ acc -ον voc
// Plural: nom -οι gen -ων dat -οις acc -ους voc -οι
fn grc_decline_2m(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "ος" }
if str_eq(gram_case, "genitive") { return stem + "ου" }
if str_eq(gram_case, "dative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "ον" }
if str_eq(gram_case, "vocative") { return stem + "ε" }
return stem + "ος"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "οι" }
if str_eq(gram_case, "genitive") { return stem + "ων" }
if str_eq(gram_case, "dative") { return stem + "οις" }
if str_eq(gram_case, "accusative") { return stem + "ους" }
if str_eq(gram_case, "vocative") { return stem + "οι" }
return stem + "οι"
}
// 2nd declension neuter: -ον nouns
//
// Stem: remove -ον (2 chars)
// Singular: nom/acc/voc -ον gen -ου dat -ῳ
// Plural: nom/acc/voc -α gen -ων dat -οις
fn grc_decline_2n(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "ον" }
if str_eq(gram_case, "genitive") { return stem + "ου" }
if str_eq(gram_case, "dative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "ον" }
if str_eq(gram_case, "vocative") { return stem + "ον" }
return stem + "ον"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "α" }
if str_eq(gram_case, "genitive") { return stem + "ων" }
if str_eq(gram_case, "dative") { return stem + "οις" }
if str_eq(gram_case, "accusative") { return stem + "α" }
if str_eq(gram_case, "vocative") { return stem + "α" }
return stem + "α"
}
// 1st declension feminine: alpha-stem (-α) nouns
//
// Stem: remove -α (1 char)
// Singular: nom -α gen -ας dat -ᾳ acc -αν voc -α
// Plural: nom -αι gen -ων dat -αις acc -ας voc -αι
fn grc_decline_1a(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "α" }
if str_eq(gram_case, "genitive") { return stem + "ας" }
if str_eq(gram_case, "dative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "αν" }
if str_eq(gram_case, "vocative") { return stem + "α" }
return stem + "α"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "αι" }
if str_eq(gram_case, "genitive") { return stem + "ων" }
if str_eq(gram_case, "dative") { return stem + "αις" }
if str_eq(gram_case, "accusative") { return stem + "ας" }
if str_eq(gram_case, "vocative") { return stem + "αι" }
return stem + "αι"
}
// 1st declension feminine: eta-stem () nouns
//
// Stem: remove (1 char)
// Singular: nom gen -ης dat -ῃ acc -ην voc
// Plural: nom -αι gen -ων dat -αις acc -ας voc -αι
fn grc_decline_1e(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "η" }
if str_eq(gram_case, "genitive") { return stem + "ης" }
if str_eq(gram_case, "dative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "ην" }
if str_eq(gram_case, "vocative") { return stem + "η" }
return stem + "η"
}
// plural (same as alpha-stem in the plural)
if str_eq(gram_case, "nominative") { return stem + "αι" }
if str_eq(gram_case, "genitive") { return stem + "ων" }
if str_eq(gram_case, "dative") { return stem + "αις" }
if str_eq(gram_case, "accusative") { return stem + "ας" }
if str_eq(gram_case, "vocative") { return stem + "αι" }
return stem + "αι"
}
// grc_decline: main declension entry point
//
// noun: nominative singular Greek noun (e.g. "λόγος", "ἔργον", "χώρα")
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "vocative"
// number: "singular" | "plural"
//
// 3rd declension consonant stems are too unpredictable without a full lexicon;
// for those the nominative is returned unchanged as a safe fallback.
fn grc_decline(noun: String, gram_case: String, number: String) -> String {
let decl: String = grc_declension(noun)
if str_eq(decl, "2m") {
let stem: String = grc_str_drop_last(noun, 2)
return grc_decline_2m(stem, gram_case, number)
}
if str_eq(decl, "2n") {
let stem: String = grc_str_drop_last(noun, 2)
return grc_decline_2n(stem, gram_case, number)
}
if str_eq(decl, "1a") {
let stem: String = grc_str_drop_last(noun, 1)
return grc_decline_1a(stem, gram_case, number)
}
if str_eq(decl, "1e") {
let stem: String = grc_str_drop_last(noun, 1)
return grc_decline_1e(stem, gram_case, number)
}
// 3rd declension: return the base form unchanged
return noun
}
// grc_article: definite article
//
// The Ancient Greek definite article ὁ/ἡ/τό is fully declined for gender,
// case, and number. There is no indefinite article.
//
// gender: "masculine" | "feminine" | "neuter"
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "vocative"
// number: "singular" | "plural"
//
// Masc Fem Neut
// Nom sg: τό
// Gen sg: τοῦ τῆς τοῦ
// Dat sg: τῷ τῇ τῷ
// Acc sg: τόν τήν τό
// Voc sg: (none; ὦ used as exclamatory particle, not article)
// Nom pl: οἱ αἱ τά
// Gen pl: τῶν τῶν τῶν
// Dat pl: τοῖς ταῖς τοῖς
// Acc pl: τούς τάς τά
// Voc pl: (same as nom pl in practice)
fn grc_article_masculine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "genitive") { return "τοῦ" }
if str_eq(gram_case, "dative") { return "τῷ" }
if str_eq(gram_case, "accusative") { return "τόν" }
if str_eq(gram_case, "vocative") { return "" }
return ""
}
// plural
if str_eq(gram_case, "nominative") { return "οἱ" }
if str_eq(gram_case, "genitive") { return "τῶν" }
if str_eq(gram_case, "dative") { return "τοῖς" }
if str_eq(gram_case, "accusative") { return "τούς" }
if str_eq(gram_case, "vocative") { return "οἱ" }
return "οἱ"
}
fn grc_article_feminine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "genitive") { return "τῆς" }
if str_eq(gram_case, "dative") { return "τῇ" }
if str_eq(gram_case, "accusative") { return "τήν" }
if str_eq(gram_case, "vocative") { return "" }
return ""
}
// plural
if str_eq(gram_case, "nominative") { return "αἱ" }
if str_eq(gram_case, "genitive") { return "τῶν" }
if str_eq(gram_case, "dative") { return "ταῖς" }
if str_eq(gram_case, "accusative") { return "τάς" }
if str_eq(gram_case, "vocative") { return "αἱ" }
return "αἱ"
}
fn grc_article_neuter(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "τό" }
if str_eq(gram_case, "genitive") { return "τοῦ" }
if str_eq(gram_case, "dative") { return "τῷ" }
if str_eq(gram_case, "accusative") { return "τό" }
if str_eq(gram_case, "vocative") { return "τό" }
return "τό"
}
// plural
if str_eq(gram_case, "nominative") { return "τά" }
if str_eq(gram_case, "genitive") { return "τῶν" }
if str_eq(gram_case, "dative") { return "τοῖς" }
if str_eq(gram_case, "accusative") { return "τά" }
if str_eq(gram_case, "vocative") { return "τά" }
return "τά"
}
fn grc_article(gender: String, gram_case: String, number: String) -> String {
if str_eq(gender, "masculine") { return grc_article_masculine(gram_case, number) }
if str_eq(gender, "feminine") { return grc_article_feminine(gram_case, number) }
// neuter
return grc_article_neuter(gram_case, number)
}
// Infer gender from noun ending
//
// Used by grc_noun_phrase when no explicit gender is provided.
// -ος -> masculine (default 2nd), -ον -> neuter, -α/-η -> feminine.
fn grc_infer_gender(noun: String) -> String {
if grc_str_ends(noun, "ος") { return "masculine" }
if grc_str_ends(noun, "ον") { return "neuter" }
if grc_str_ends(noun, "α") { return "feminine" }
if grc_str_ends(noun, "η") { return "feminine" }
return "masculine"
}
// grc_noun_phrase: noun phrase builder
//
// Produces a declined noun with optional definite article prepended.
// Gender is inferred from the noun ending when not supplied explicitly.
//
// noun: nominative singular Greek noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "vocative"
// number: "singular" | "plural"
// definite: "true" | "false" (article is prepended when "true")
fn grc_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let declined: String = grc_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let gender: String = grc_infer_gender(noun)
let art: String = grc_article(gender, gram_case, number)
return art + " " + declined
}
return declined
}
+45
View File
@@ -0,0 +1,45 @@
// auto-generated by elc --emit-header — do not edit
extern fn grc_str_ends(s: String, suf: String) -> Bool
extern fn grc_str_drop_last(s: String, n: Int) -> String
extern fn grc_str_last_char(s: String) -> String
extern fn grc_str_last2(s: String) -> String
extern fn grc_str_last3(s: String) -> String
extern fn grc_slot(person: String, number: String) -> Int
extern fn grc_map_canonical(verb: String) -> String
extern fn grc_einai_present(slot: Int) -> String
extern fn grc_einai_imperfect(slot: Int) -> String
extern fn grc_einai_future(slot: Int) -> String
extern fn grc_echein_present(slot: Int) -> String
extern fn grc_echein_imperfect(slot: Int) -> String
extern fn grc_echein_aorist(slot: Int) -> String
extern fn grc_echein_future(slot: Int) -> String
extern fn grc_legein_present(slot: Int) -> String
extern fn grc_legein_imperfect(slot: Int) -> String
extern fn grc_legein_aorist(slot: Int) -> String
extern fn grc_legein_future(slot: Int) -> String
extern fn grc_horao_present(slot: Int) -> String
extern fn grc_horao_imperfect(slot: Int) -> String
extern fn grc_horao_aorist(slot: Int) -> String
extern fn grc_horao_future(slot: Int) -> String
extern fn grc_erchesthai_present(slot: Int) -> String
extern fn grc_erchesthai_imperfect(slot: Int) -> String
extern fn grc_erchesthai_aorist(slot: Int) -> String
extern fn grc_erchesthai_future(slot: Int) -> String
extern fn grc_thematic_present_ending(slot: Int) -> String
extern fn grc_thematic_imperfect_ending(slot: Int) -> String
extern fn grc_thematic_future_ending(slot: Int) -> String
extern fn grc_weak_aorist_ending(slot: Int) -> String
extern fn grc_present_stem(verb: String) -> String
extern fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn grc_declension(noun: String) -> String
extern fn grc_decline_2m(stem: String, gram_case: String, number: String) -> String
extern fn grc_decline_2n(stem: String, gram_case: String, number: String) -> String
extern fn grc_decline_1a(stem: String, gram_case: String, number: String) -> String
extern fn grc_decline_1e(stem: String, gram_case: String, number: String) -> String
extern fn grc_decline(noun: String, gram_case: String, number: String) -> String
extern fn grc_article_masculine(gram_case: String, number: String) -> String
extern fn grc_article_feminine(gram_case: String, number: String) -> String
extern fn grc_article_neuter(gram_case: String, number: String) -> String
extern fn grc_article(gender: String, gram_case: String, number: String) -> String
extern fn grc_infer_gender(noun: String) -> String
extern fn grc_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+633
View File
@@ -0,0 +1,633 @@
// morphology-he.el - Hebrew morphology for the NLG engine.
// עברית עיצוב מורפולוגי למנוע ייצור שפה טבעית
//
// Implements Modern Hebrew verb conjugation (Pa'al binyan, participle-based
// present tense), noun pluralization, and definite-article prefixing.
//
// Hebrew is a Semitic language with a trilateral root system shared with
// Arabic. Like Arabic, words are built from three-consonant roots by
// applying vowel patterns (mishkalim) around the root.
//
// Modern Hebrew (the target) profile:
// code=he, name=Hebrew, morph_type=semitic, word_order=SVO,
// question_strategy=intonation, script=hebrew, family=semitic
//
// Key grammatical facts:
// - Grammatical gender: masculine (m) / feminine (f)
// - Number: singular / plural (dual exists but is marginal in Modern Hebrew)
// - Verbs: built from 3-letter roots via binyanim (verb patterns)
// Pa'al (פָּעַל) is covered here the most common pattern for everyday verbs
// - Present tense is participle-based (בינוני / binyan) with gender/number
// agreement suffixes on the verb; it doubles as an adjective form
// - Copula: present tense uses zero copula (omit "to be" entirely);
// past tense uses היה/הייתה/היו (haya/hayta/hayu)
// - Definite article: prefix ה (ha-) attached directly to the noun;
// consonant doubling after ה is simplified to just "ha" prefix here
// - Questions: rising intonation (no morphological change); optional
// particle האם (ha'im) at sentence start; this engine uses intonation
// strategy (question mark added by realizer)
// - Script: Hebrew Unicode (right-to-left). The El runtime currently
// outputs non-ASCII as numeric hashes (runtime limitation), but str_eq
// and string literals work correctly internally. When the VM adds UTF-8
// output support, all Hebrew strings will display correctly automatically.
//
// Verbs covered (by infinitive, transliterated and Hebrew):
// "lihyot" / לִהְיוֹת to be (copula, zero-present, haya-past)
// "haya" / הָיָה to be (dictionary alias lihyot)
// "be" English canonical lihyot
// "lir'ot" / לִרְאוֹת to see
// "le'exol" / לֶאֱכוֹל to eat
// "ledaber" / לְדַבֵּר to speak
// "lalechet" / לָלֶכֶת to go
//
// Conventions used throughout:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "present" | "past" | "future"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last, str_concat/+)
// String helpers
import "morphology.el"
fn he_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn he_str_len(s: String) -> Int {
return str_len(s)
}
fn he_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn he_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Slot index
//
// Maps person × gender × number to a 0-based slot for table lookups.
// Modern Hebrew uses 12 paradigm cells (3 persons × 2 genders × 2 numbers),
// but first person is gender-neutral in Modern Hebrew for verbs, so we
// collapse 1s-m and 1s-f to the same slot, and likewise 1p.
//
// Slot layout:
// 0 = 3ms (הוא hu he)
// 1 = 3fs (היא hi she)
// 2 = 2ms (אתה ata you m sg)
// 3 = 2fs (את at you f sg)
// 4 = 1s (אני ani I, gender-neutral in Modern Hebrew)
// 5 = 3mp (הם hem they m pl)
// 6 = 3fp (הן hen they f pl)
// 7 = 2mp (אתם atem you m pl)
// 8 = 2fp (אתן aten you f pl)
// 9 = 1p (אנחנו anakhnu we, gender-neutral)
fn he_slot(person: String, gender: String, number: String) -> Int {
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 1 }
return 0
}
// plural
if str_eq(gender, "f") { return 6 }
return 5
}
if str_eq(person, "second") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
return 2
}
// plural
if str_eq(gender, "f") { return 8 }
return 7
}
// first person gender-neutral in Modern Hebrew verb conjugation
if str_eq(number, "plural") { return 9 }
return 4
}
// Present-tense (participle / binyan) agreement suffixes
//
// Modern Hebrew present tense is built from the Pa'al participle stem.
// The participle agrees with the subject in gender and number.
//
// For Pa'al (CoCeC pattern), the present-tense forms are:
// Masc sg: base participle stem (e.g. רוֹאֶה ro'e sees) [no suffix]
// Fem sg: stem + ת (-t / -et) depending on root
// Masc pl: stem + ים (-im) (often with vowel change in the root)
// Fem pl: stem + ות (-ot)
//
// Rather than attempting to derive these from the root (which requires knowing
// the full vowel pattern), we store the four present-tense forms explicitly
// for each covered verb. This is the standard approach for a finite NLG
// system covering a curated verb set.
//
// Present forms are indexed by a 2-bit form code:
// 0 = masc sg (ms)
// 1 = fem sg (fs)
// 2 = masc pl (mp)
// 3 = fem pl (fp)
fn he_present_form_code(slot: Int) -> Int {
// Slots 0, 2, 4, 7 masc sg form (1s and 2ms use same form as 3ms)
if slot == 0 { return 0 } // 3ms
if slot == 1 { return 1 } // 3fs
if slot == 2 { return 0 } // 2ms masc sg
if slot == 3 { return 1 } // 2fs fem sg
if slot == 4 { return 0 } // 1s masc sg (gender-neutral, default masc)
if slot == 5 { return 2 } // 3mp masc pl
if slot == 6 { return 3 } // 3fp fem pl
if slot == 7 { return 2 } // 2mp masc pl
if slot == 8 { return 3 } // 2fp fem pl
return 2 // 1p (slot 9) masc pl (gender-neutral default)
}
// Copula: היה haya to be
//
// Present copula: ZERO (omitted from surface form).
// Past copula: היה haya (3ms), הייתה hayta (3fs/2fs), היו hayu (3pl),
// הייתי hayiti (1s), הייתם hayitem (2mp), הייתן hayiten (2fp),
// היינו hayinu (1p), היית hayita (2ms).
// Future copula: יהיה yihye (3ms), תהיה tihye (3fs/2), אהיה ehye (1s),
// יהיו yihyu (3pl/2pl), נהיה nihye (1p) included for completeness.
fn he_copula_past(slot: Int) -> String {
if slot == 0 { return "היה" } // 3ms haya
if slot == 1 { return "הייתה" } // 3fs hayta
if slot == 2 { return "היית" } // 2ms hayita
if slot == 3 { return "הייתה" } // 2fs hayta (same as 3fs in Modern Hebrew)
if slot == 4 { return "הייתי" } // 1s hayiti
if slot == 5 { return "היו" } // 3mp hayu
if slot == 6 { return "היו" } // 3fp hayu (same as 3mp in Modern Hebrew)
if slot == 7 { return "הייתם" } // 2mp hayitem
if slot == 8 { return "הייתן" } // 2fp hayiten
return "היינו" // 1p hayinu
}
fn he_copula_future(slot: Int) -> String {
if slot == 0 { return "יהיה" } // 3ms yihye
if slot == 1 { return "תהיה" } // 3fs tihye
if slot == 2 { return "תהיה" } // 2ms tihye
if slot == 3 { return "תהיי" } // 2fs tihyi
if slot == 4 { return "אהיה" } // 1s ehye
if slot == 5 { return "יהיו" } // 3mp yihyu
if slot == 6 { return "יהיו" } // 3fp yihyu
if slot == 7 { return "תהיו" } // 2mp tihyu
if slot == 8 { return "תהיו" } // 2fp tihyu
return "נהיה" // 1p nihye
}
// he_is_copula: detect whether the input verb means "to be".
fn he_is_copula(verb: String) -> Bool {
if str_eq(verb, "lihyot") { return true }
if str_eq(verb, "haya") { return true }
if str_eq(verb, "be") { return true }
if str_eq(verb, "היה") { return true }
if str_eq(verb, "לִהְיוֹת") { return true }
return false
}
// he_conjugate_copula: conjugate the copula for the given tense/slot.
fn he_conjugate_copula(tense: String, slot: Int) -> String {
// Present copula: zero in Modern Hebrew
if str_eq(tense, "present") { return "" }
if str_eq(tense, "past") { return he_copula_past(slot) }
if str_eq(tense, "future") { return he_copula_future(slot) }
// Default: zero copula
return ""
}
// Pa'al present-tense forms: verb-by-verb table
//
// Each verb stores four present-tense participle forms:
// [masc sg, fem sg, masc pl, fem pl]
// indexed by he_present_form_code(slot).
//
// Transliterations for reference:
// lir'ot (לִרְאוֹת to see): ro'e / ro'a / ro'im / ro'ot
// le'exol (לֶאֱכוֹל to eat): oxel / oxelet / oxlim / oxlot
// ledaber (לְדַבֵּר to speak): medaber / medaberet / medabrim / medabrot
// lalechet (לָלֶכֶת to go): holech / holechet / holchim / holchot
fn he_present_lir_ot(form: Int) -> String {
if form == 0 { return "רוֹאֶה" } // ro'e masc sg
if form == 1 { return "רוֹאָה" } // ro'a fem sg
if form == 2 { return "רוֹאִים" } // ro'im masc pl
return "רוֹאוֹת" // ro'ot fem pl (form == 3)
}
fn he_present_le_exol(form: Int) -> String {
if form == 0 { return "אוֹכֵל" } // oxel masc sg
if form == 1 { return "אוֹכֶלֶת" } // oxelet fem sg
if form == 2 { return "אוֹכְלִים" } // oxlim masc pl
return "אוֹכְלוֹת" // oxlot fem pl
}
fn he_present_ledaber(form: Int) -> String {
if form == 0 { return "מְדַבֵּר" } // medaber masc sg
if form == 1 { return "מְדַבֶּרֶת" } // medaberet fem sg
if form == 2 { return "מְדַבְּרִים" } // medabrim masc pl
return "מְדַבְּרוֹת" // medabrot fem pl
}
fn he_present_lalechet(form: Int) -> String {
if form == 0 { return "הוֹלֵךְ" } // holech masc sg
if form == 1 { return "הוֹלֶכֶת" } // holechet fem sg
if form == 2 { return "הוֹלְכִים" } // holchim masc pl
return "הוֹלְכוֹת" // holchot fem pl
}
// Pa'al past-tense forms: verb-by-verb table
//
// Past tense in Pa'al uses suffixes on a past stem (the 3ms form is the stem).
// Suffix pattern (slot suffix appended to stem consonants):
// slot 0 (3ms): base (e.g. ראה ra'a)
// slot 1 (3fs): -ta (ראתה ra'ata)
// slot 2 (2ms): -ta (ראית ra'ita)
// slot 3 (2fs): -t (ראית ra'it same spelling as 2ms in Modern Hebrew)
// slot 4 (1s): -ti (ראיתי ra'iti)
// slot 5 (3mp): -u (ראו ra'u)
// slot 6 (3fp): -u (ראו ra'u same as 3mp in Modern Hebrew)
// slot 7 (2mp): -tem (ראיתם ra'item)
// slot 8 (2fp): -ten (ראיתן ra'iten)
// slot 9 (1p): -nu (ראינו ra'inu)
//
// We store full past paradigms for each verb suffix application to the base
// requires knowing each verb's past stem vowel pattern.
fn he_past_lir_ot(slot: Int) -> String {
if slot == 0 { return "רָאָה" } // ra'a 3ms
if slot == 1 { return "רָאֲתָה" } // ra'ata 3fs
if slot == 2 { return "רָאִיתָ" } // ra'ita 2ms
if slot == 3 { return "רָאִית" } // ra'it 2fs
if slot == 4 { return "רָאִיתִי" } // ra'iti 1s
if slot == 5 { return "רָאוּ" } // ra'u 3mp
if slot == 6 { return "רָאוּ" } // ra'u 3fp
if slot == 7 { return "רְאִיתֶם" } // re'item 2mp
if slot == 8 { return "רְאִיתֶן" } // re'iten 2fp
return "רָאִינוּ" // ra'inu 1p
}
fn he_past_le_exol(slot: Int) -> String {
if slot == 0 { return "אָכַל" } // axal 3ms
if slot == 1 { return "אָכְלָה" } // axla 3fs
if slot == 2 { return "אָכַלְתָּ" } // axalta 2ms
if slot == 3 { return "אָכַלְתְּ" } // axalt 2fs
if slot == 4 { return "אָכַלְתִּי" }// axalti 1s
if slot == 5 { return "אָכְלוּ" } // axlu 3mp
if slot == 6 { return "אָכְלוּ" } // axlu 3fp
if slot == 7 { return "אֲכַלְתֶּם" }// axaltem 2mp
if slot == 8 { return "אֲכַלְתֶּן" }// axalten 2fp
return "אָכַלְנוּ" // axalnu 1p
}
fn he_past_ledaber(slot: Int) -> String {
if slot == 0 { return "דִּבֵּר" } // diber 3ms (Pi'el past)
if slot == 1 { return "דִּבְּרָה" } // dibra 3fs
if slot == 2 { return "דִּבַּרְתָּ" }// dibarta 2ms
if slot == 3 { return "דִּבַּרְתְּ" }// dibart 2fs
if slot == 4 { return "דִּבַּרְתִּי" }// diberti 1s
if slot == 5 { return "דִּבְּרוּ" } // dibru 3mp
if slot == 6 { return "דִּבְּרוּ" } // dibru 3fp
if slot == 7 { return "דִּבַּרְתֶּם" }// dibertem 2mp
if slot == 8 { return "דִּבַּרְתֶּן" }// dibertn 2fp
return "דִּבַּרְנוּ" // dibernu 1p
}
fn he_past_lalechet(slot: Int) -> String {
if slot == 0 { return "הָלַךְ" } // halax 3ms
if slot == 1 { return "הָלְכָה" } // halxa 3fs
if slot == 2 { return "הָלַכְתָּ" } // halaxta 2ms
if slot == 3 { return "הָלַכְתְּ" } // halaxt 2fs
if slot == 4 { return "הָלַכְתִּי" }// halaxti 1s
if slot == 5 { return "הָלְכוּ" } // halxu 3mp
if slot == 6 { return "הָלְכוּ" } // halxu 3fp
if slot == 7 { return "הֲלַכְתֶּם" }// halaxtem 2mp
if slot == 8 { return "הֲלַכְתֶּן" }// halaxten 2fp
return "הָלַכְנוּ" // halaxnu 1p
}
// Future-tense forms: verb-by-verb table
//
// Future tense in Pa'al uses prefix + root + suffix (yiqtol pattern).
// We store full paradigms for each covered verb.
fn he_future_lir_ot(slot: Int) -> String {
if slot == 0 { return "יִרְאֶה" } // yir'e 3ms
if slot == 1 { return "תִּרְאֶה" } // tir'e 3fs
if slot == 2 { return "תִּרְאֶה" } // tir'e 2ms
if slot == 3 { return "תִּרְאִי" } // tir'i 2fs
if slot == 4 { return "אֶרְאֶה" } // er'e 1s
if slot == 5 { return "יִרְאוּ" } // yir'u 3mp
if slot == 6 { return "תִּרְאֶינָה" }// tir'ena 3fp
if slot == 7 { return "תִּרְאוּ" } // tir'u 2mp
if slot == 8 { return "תִּרְאֶינָה" }// tir'ena 2fp
return "נִרְאֶה" // nir'e 1p
}
fn he_future_le_exol(slot: Int) -> String {
if slot == 0 { return "יֹאכַל" } // yoxal 3ms
if slot == 1 { return "תֹּאכַל" } // toxal 3fs
if slot == 2 { return "תֹּאכַל" } // toxal 2ms
if slot == 3 { return "תֹּאכְלִי" } // toxli 2fs
if slot == 4 { return "אֹכַל" } // oxal 1s
if slot == 5 { return "יֹאכְלוּ" } // yoxlu 3mp
if slot == 6 { return "תֹּאכַלְנָה" }// toxalna 3fp
if slot == 7 { return "תֹּאכְלוּ" } // toxlu 2mp
if slot == 8 { return "תֹּאכַלְנָה" }// toxalna 2fp
return "נֹאכַל" // noxal 1p
}
fn he_future_ledaber(slot: Int) -> String {
if slot == 0 { return "יְדַבֵּר" } // yedaber 3ms
if slot == 1 { return "תְּדַבֵּר" } // tedaber 3fs
if slot == 2 { return "תְּדַבֵּר" } // tedaber 2ms
if slot == 3 { return "תְּדַבְּרִי" }// tedabri 2fs
if slot == 4 { return "אֲדַבֵּר" } // adaber 1s
if slot == 5 { return "יְדַבְּרוּ" }// yedabru 3mp
if slot == 6 { return "תְּדַבֵּרְנָה" }// tedaberna 3fp
if slot == 7 { return "תְּדַבְּרוּ" }// tedabru 2mp
if slot == 8 { return "תְּדַבֵּרְנָה" }// tedaberna 2fp
return "נְדַבֵּר" // nedaber 1p
}
fn he_future_lalechet(slot: Int) -> String {
if slot == 0 { return "יֵלֵךְ" } // yelex 3ms
if slot == 1 { return "תֵּלֵךְ" } // telex 3fs
if slot == 2 { return "תֵּלֵךְ" } // telex 2ms
if slot == 3 { return "תֵּלְכִי" } // telxi 2fs
if slot == 4 { return "אֵלֵךְ" } // elex 1s
if slot == 5 { return "יֵלְכוּ" } // yelxu 3mp
if slot == 6 { return "תֵּלַכְנָה" } // telaxna 3fp
if slot == 7 { return "תֵּלְכוּ" } // telxu 2mp
if slot == 8 { return "תֵּלַכְנָה" } // telaxna 2fp
return "נֵלֵךְ" // nelex 1p
}
// Known-verb dispatcher
//
// he_known_verb: return the inflected form for a known verb, or "" if the
// verb is not in the lookup table. Accepts both transliterated and Hebrew
// script infinitives.
fn he_known_verb(verb: String, tense: String, slot: Int) -> String {
// lir'ot / לִרְאוֹת to see
if str_eq(verb, "lir'ot") {
if str_eq(tense, "present") { return he_present_lir_ot(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lir_ot(slot) }
if str_eq(tense, "future") { return he_future_lir_ot(slot) }
return he_present_lir_ot(he_present_form_code(slot))
}
if str_eq(verb, "לִרְאוֹת") {
if str_eq(tense, "present") { return he_present_lir_ot(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lir_ot(slot) }
if str_eq(tense, "future") { return he_future_lir_ot(slot) }
return he_present_lir_ot(he_present_form_code(slot))
}
// le'exol / לֶאֱכוֹל to eat
if str_eq(verb, "le'exol") {
if str_eq(tense, "present") { return he_present_le_exol(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_le_exol(slot) }
if str_eq(tense, "future") { return he_future_le_exol(slot) }
return he_present_le_exol(he_present_form_code(slot))
}
if str_eq(verb, "לֶאֱכוֹל") {
if str_eq(tense, "present") { return he_present_le_exol(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_le_exol(slot) }
if str_eq(tense, "future") { return he_future_le_exol(slot) }
return he_present_le_exol(he_present_form_code(slot))
}
// ledaber / לְדַבֵּר to speak
if str_eq(verb, "ledaber") {
if str_eq(tense, "present") { return he_present_ledaber(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_ledaber(slot) }
if str_eq(tense, "future") { return he_future_ledaber(slot) }
return he_present_ledaber(he_present_form_code(slot))
}
if str_eq(verb, "לְדַבֵּר") {
if str_eq(tense, "present") { return he_present_ledaber(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_ledaber(slot) }
if str_eq(tense, "future") { return he_future_ledaber(slot) }
return he_present_ledaber(he_present_form_code(slot))
}
// lalechet / לָלֶכֶת to go
if str_eq(verb, "lalechet") {
if str_eq(tense, "present") { return he_present_lalechet(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lalechet(slot) }
if str_eq(tense, "future") { return he_future_lalechet(slot) }
return he_present_lalechet(he_present_form_code(slot))
}
if str_eq(verb, "לָלֶכֶת") {
if str_eq(tense, "present") { return he_present_lalechet(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lalechet(slot) }
if str_eq(tense, "future") { return he_future_lalechet(slot) }
return he_present_lalechet(he_present_form_code(slot))
}
// Verb not in table
return ""
}
// Main conjugation entry point
//
// he_conjugate: conjugate a Hebrew verb.
//
// verb: infinitive (transliterated or Hebrew script)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
//
// Returns:
// - "" for present copula (zero copula caller omits the verb)
// - inflected form for all other cases
// - the infinitive unchanged for unknown verbs (safe fallback)
fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String {
let slot: Int = he_slot(person, gender, number)
// Handle copula first
if he_is_copula(verb) {
return he_conjugate_copula(tense, slot)
}
// Try the known-verb table
let known: String = he_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
// Unknown verb: return the infinitive as a safe placeholder.
// The caller can detect this when the output equals the input.
return verb
}
// Noun pluralization
//
// he_pluralize: form the plural of a Hebrew noun.
//
// Rules (simplified for Modern Hebrew):
// Masculine nouns (and those not ending in -a or -et):
// Add ים- (-im)
// Feminine nouns ending in -a (transliteration) or the Hebrew letter ה:
// Replace final ה with ות (-ot)
// Feminine nouns ending in -et (transliteration) or ת-:
// Replace -et / ת with ות (-ot)
// Fallback:
// Add ות (-ot) covers other feminine patterns
//
// Notes:
// - Many Hebrew nouns have irregular plurals (e.g. ספר/ספרים, בית/בתים).
// The Engram vocabulary layer should supply these directly.
// - This function handles the productive, regular pattern.
fn he_pluralize(noun: String, gender: String) -> String {
if str_eq(gender, "m") {
// Masculine: add -im
return noun + "ים"
}
// Feminine noun ending in Hebrew ה (he) most common -a ending in script
if he_str_ends(noun, "ה") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ות"
}
// Feminine noun ending in ת (tav) covers -et, -at, -it endings
if he_str_ends(noun, "ת") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ות"
}
// Transliteration check: -a ending (e.g. "yalda" "yaldot")
if he_str_ends(noun, "a") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ot"
}
// Transliteration check: -et ending (e.g. "yaldet" "yaldot")
if he_str_ends(noun, "et") {
let stem: String = he_str_drop_last(noun, 2)
return stem + "ot"
}
// Fallback: add -ot
return noun + "ות"
}
// Definite noun phrases
//
// he_definite_prefix: attach the definite article ה (ha-) to a noun.
//
// The definite article in Hebrew is the prefix ה (ha-) attached directly
// to the noun without a space. In formal/Biblical Hebrew the following
// consonant receives a dagesh (doubling), but in Modern Hebrew pronunciation
// the doubling is generally not applied. We implement the simplified form:
// definite noun = "ה" + noun (script form)
// For transliterated nouns: "ha" + noun
//
// Callers pass Hebrew script nouns; transliterated nouns are handled by
// checking whether the noun starts with a Hebrew code-point range.
fn he_is_hebrew_script(noun: String) -> Bool {
// Hebrew Unicode block: U+05D0 (א) through U+05EA (ת).
// We check the first character: if str_len > 0 and it is a Hebrew letter,
// the noun is in Hebrew script. We use a set of common first letters as
// a heuristic; the alternative (numeric code-point comparison) is not
// available in El. This covers the vast majority of practical cases.
let n: Int = str_len(noun)
if n == 0 { return false }
let first: String = str_slice(noun, 0, 1)
// Common Hebrew first letters in the Unicode block
if str_eq(first, "א") { return true }
if str_eq(first, "ב") { return true }
if str_eq(first, "ג") { return true }
if str_eq(first, "ד") { return true }
if str_eq(first, "ה") { return true }
if str_eq(first, "ו") { return true }
if str_eq(first, "ז") { return true }
if str_eq(first, "ח") { return true }
if str_eq(first, "ט") { return true }
if str_eq(first, "י") { return true }
if str_eq(first, "כ") { return true }
if str_eq(first, "ל") { return true }
if str_eq(first, "מ") { return true }
if str_eq(first, "נ") { return true }
if str_eq(first, "ס") { return true }
if str_eq(first, "ע") { return true }
if str_eq(first, "פ") { return true }
if str_eq(first, "צ") { return true }
if str_eq(first, "ק") { return true }
if str_eq(first, "ר") { return true }
if str_eq(first, "ש") { return true }
if str_eq(first, "ת") { return true }
return false
}
fn he_definite_prefix(noun: String) -> String {
if he_is_hebrew_script(noun) {
return "ה" + noun
}
// Transliterated noun: prepend "ha"
return "ha" + noun
}
// he_noun_phrase: build a full noun phrase with definiteness and number.
//
// noun: base (singular) noun string (Hebrew script or transliteration)
// number: "singular" | "plural"
// gender: "m" | "f"
// definite: "true" | "false"
//
// Returns the surface noun phrase string.
fn he_noun_phrase(noun: String, number: String, gender: String, definite: String) -> String {
// Step 1: apply number (pluralize if needed)
let stem: String = noun
if str_eq(number, "plural") {
let stem = he_pluralize(noun, gender)
}
// Step 2: apply definiteness
if str_eq(definite, "true") {
return he_definite_prefix(stem)
}
return stem
}
// Canonical verb mapping
//
// he_map_canonical: map cross-lingual canonical English verb labels to
// their Hebrew equivalents before dispatching to he_conjugate.
//
// This mirrors morph_map_canonical in morphology.el but for Hebrew.
// Called by the morphology dispatcher before he_conjugate.
//
// Canonical labels: "be" | "have" | "do" | "go" | "see" | "eat" | "speak"
fn he_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "lihyot" }
if str_eq(verb, "see") { return "lir'ot" }
if str_eq(verb, "eat") { return "le'exol" }
if str_eq(verb, "speak") { return "ledaber" }
if str_eq(verb, "say") { return "ledaber" }
if str_eq(verb, "go") { return "lalechet" }
// Unknown canonical: return as-is; he_conjugate will fall back to infinitive
return verb
}
+30
View File
@@ -0,0 +1,30 @@
// auto-generated by elc --emit-header — do not edit
extern fn he_str_ends(s: String, suf: String) -> Bool
extern fn he_str_len(s: String) -> Int
extern fn he_str_drop_last(s: String, n: Int) -> String
extern fn he_str_last_char(s: String) -> String
extern fn he_slot(person: String, gender: String, number: String) -> Int
extern fn he_present_form_code(slot: Int) -> Int
extern fn he_copula_past(slot: Int) -> String
extern fn he_copula_future(slot: Int) -> String
extern fn he_is_copula(verb: String) -> Bool
extern fn he_conjugate_copula(tense: String, slot: Int) -> String
extern fn he_present_lir_ot(form: Int) -> String
extern fn he_present_le_exol(form: Int) -> String
extern fn he_present_ledaber(form: Int) -> String
extern fn he_present_lalechet(form: Int) -> String
extern fn he_past_lir_ot(slot: Int) -> String
extern fn he_past_le_exol(slot: Int) -> String
extern fn he_past_ledaber(slot: Int) -> String
extern fn he_past_lalechet(slot: Int) -> String
extern fn he_future_lir_ot(slot: Int) -> String
extern fn he_future_le_exol(slot: Int) -> String
extern fn he_future_ledaber(slot: Int) -> String
extern fn he_future_lalechet(slot: Int) -> String
extern fn he_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn he_pluralize(noun: String, gender: String) -> String
extern fn he_is_hebrew_script(noun: String) -> Bool
extern fn he_definite_prefix(noun: String) -> String
extern fn he_noun_phrase(noun: String, number: String, gender: String, definite: String) -> String
extern fn he_map_canonical(verb: String) -> String
+631
View File
@@ -0,0 +1,631 @@
// morphology-hi.el - Hindi morphology for the NLG engine.
//
// Implements Hindi noun declension (direct and oblique cases), postpositional
// particles, verb stem extraction, tense conjugation, and genitive agreement.
//
// Hindi is a fusional/agglutinative hybrid, SOV, pro-drop, with Devanagari script.
//
// Key facts:
// - 2 genders: masculine (m), feminine (f)
// - 2 numbers: singular (sg), plural (pl)
// - 2 main cases: direct (nominative/accusative) and oblique (before postpositions)
// - Verbs agree with subject in gender and number
// - No articles
//
// Conventions used throughout:
// gender: "m" | "f"
// number: "sg" | "pl"
// case: "direct" | "oblique"
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last)
// String helpers
import "morphology.el"
fn hi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn hi_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn hi_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Gender inference
//
// Heuristic from the noun's final Devanagari character.
// Masculine: typically ends in (aa), or a consonant cluster
// Feminine: typically ends in (ii), ि (i-matra), or other endings
//
// This is an approximation gender is inherent to each noun and not always
// predictable. The caller should supply gender explicitly when known.
//
// Common feminine endings: "" (long ii), "ि" (short i matra in some forms),
// "" (like बहन), "" (like ), "" (like )
// Common masculine endings: "" (aa matra), consonants generally
fn hi_gender(noun: String) -> String {
// Long ii ending strongly feminine
if hi_str_ends(noun, "") { return "f" }
// aa matra ending strongly masculine
if hi_str_ends(noun, "") { return "m" }
// Known feminine words by ending
if hi_str_ends(noun, "") {
// बहन (sister), दुक (shop) feminine; but also many masc.
// Default feminine for -न common feminine nouns
return "f"
}
if hi_str_ends(noun, "") { return "f" } // त, त,
if hi_str_ends(noun, "") { return "f" } // (variant spellings)
if hi_str_ends(noun, "") { return "m" } // आक etc.
// Specific common words
if str_eq(noun, "लड़का") { return "m" }
if str_eq(noun, "लड़की") { return "f" }
if str_eq(noun, "आदमी") { return "m" }
if str_eq(noun, "औरत") { return "f" }
if str_eq(noun, "घर") { return "m" }
if str_eq(noun, "मेज़") { return "f" }
if str_eq(noun, "किताब") { return "f" }
if str_eq(noun, "पानी") { return "m" }
if str_eq(noun, "दूध") { return "m" }
if str_eq(noun, "हाथ") { return "m" }
if str_eq(noun, "आँख") { return "f" }
if str_eq(noun, "बच्चा") { return "m" }
if str_eq(noun, "बच्ची") { return "f" }
if str_eq(noun, "काम") { return "m" }
if str_eq(noun, "बात") { return "f" }
if str_eq(noun, "दिन") { return "m" }
if str_eq(noun, "रात") { return "f" }
if str_eq(noun, "देश") { return "m" }
if str_eq(noun, "भाषा") { return "f" }
if str_eq(noun, "जगह") { return "f" }
if str_eq(noun, "समय") { return "m" }
if str_eq(noun, "साल") { return "m" }
// Default to masculine
return "m"
}
// Masculine noun declension
//
// Two patterns:
// (A) Ends in -आ ( matra): लड़क, बच्च, ड़ vowel-final type
// direct sg: base (लड़क)
// direct pl: stem + (लड़के)
// oblique sg: stem + (लड़के)
// oblique pl: stem + (लड़क)
//
// (B) Consonant-final or other: आदम, घर, थ, ि invariant type
// direct sg: base
// direct pl: base (no change in direct plural for most)
// oblique sg: base (no change)
// oblique pl: base + (घरं, ं, ि)
// Exception: आदम (masc ending ): acts like (B) in direct but
// oblique pl uses drop the ī: आदमि
fn hi_masc_aa_stem(noun: String) -> String {
// Strip trailing (aa matra, 3 UTF-8 bytes in Devanagari but El strings
// are Unicode so we drop the last character which is )
return hi_str_drop_last(noun, 1)
}
fn hi_noun_direct_m(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A: aa-final masculine
if str_eq(number, "sg") { return noun }
// pl: replace with
return hi_masc_aa_stem(noun) + ""
}
// Pattern B: invariant direct forms
return noun
}
fn hi_noun_oblique_m(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A
let stem: String = hi_masc_aa_stem(noun)
if str_eq(number, "sg") { return stem + "" }
return stem + "ों"
}
// Pattern B: consonant-final or ī-final
// आदम and similar ī-final masculines: oblique pl inserts
if hi_str_ends(noun, "") {
if str_eq(number, "sg") { return noun }
// oblique pl: drop , add ि
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियों"
}
// Default consonant-final masculines
if str_eq(number, "sg") { return noun }
return noun + "ों"
}
// Feminine noun declension
//
// Two patterns:
// (A) Ends in -ई/- (long ii): लड़क, बच्च
// direct sg: base (लड़क)
// direct pl: stem + ि (लड़कि)
// oblique sg: base (लड़क)
// oblique pl: stem + ि (लड़कि)
//
// (B) Consonant-final or other: मेज़, त, िब, त, औरत,
// direct sg: base (मेज़)
// direct pl: base + ें (मेज़ें, िबें)
// oblique sg: base
// oblique pl: base + (मेज़ं, ि)
fn hi_noun_direct_f(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A: ii-final feminine
if str_eq(number, "sg") { return noun }
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियाँ"
}
// Pattern B: other feminine
if str_eq(number, "sg") { return noun }
// Direct plural: add ें
return noun + "ें"
}
fn hi_noun_oblique_f(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A
if str_eq(number, "sg") { return noun }
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियों"
}
// Pattern B
if str_eq(number, "sg") { return noun }
return noun + "ों"
}
// Unified noun declension entry points
// hi_noun_direct: direct case form (nominative/bare accusative).
fn hi_noun_direct(noun: String, gender: String, number: String) -> String {
if str_eq(gender, "m") { return hi_noun_direct_m(noun, number) }
if str_eq(gender, "f") { return hi_noun_direct_f(noun, number) }
return noun
}
// hi_noun_oblique: oblique case form (before postpositions).
fn hi_noun_oblique(noun: String, gender: String, number: String) -> String {
if str_eq(gender, "m") { return hi_noun_oblique_m(noun, number) }
if str_eq(gender, "f") { return hi_noun_oblique_f(noun, number) }
return noun
}
// Postpositional particles
//
// Hindi marks grammatical relations with postpositions that follow the oblique
// noun. The genitive postposition agrees with the possessed noun's gender and
// number use hi_agree_genitive for that case.
//
// case values:
// "nominative" no postposition (direct case subject)
// "accusative_animate" (ko) animate direct objects
// "dative" (ko)
// "genitive" /क/के use hi_agree_genitive instead
// "locative_in" में (meṃ in)
// "locative_on" पर (par on)
// "instrumental" से (se with/by)
// "ablative" से (se from)
// "comitative" के (ke saath with/together with)
// "benefactive" के ि (ke liye for)
fn hi_postposition(gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "accusative_animate") { return "को" }
if str_eq(gram_case, "dative") { return "को" }
if str_eq(gram_case, "genitive") { return "का" } // base; use hi_agree_genitive
if str_eq(gram_case, "locative_in") { return "में" }
if str_eq(gram_case, "locative_on") { return "पर" }
if str_eq(gram_case, "instrumental") { return "से" }
if str_eq(gram_case, "ablative") { return "से" }
if str_eq(gram_case, "comitative") { return "के साथ" }
if str_eq(gram_case, "benefactive") { return "के लिए" }
return ""
}
// hi_agree_genitive: return the genitive postposition (/क/के) that agrees
// with the gender and number of the POSSESSED noun (not the possessor).
//
// possessed_gender: "m" | "f"
// possessed_number: "sg" | "pl"
//
// m sg (kaa)
// f sg (kii)
// m pl के (ke)
// f pl (kii)
fn hi_agree_genitive(possessed_gender: String, possessed_number: String) -> String {
if str_eq(possessed_gender, "f") { return "की" }
if str_eq(possessed_number, "pl") { return "के" }
return "का"
}
// Verb stem extraction
//
// Most Hindi infinitives end in -न (naa). Stripping it gives the verb stem.
// Examples: , , करन कर, देखन देख
//
// Irregulars that do not follow this pattern return a hardcoded stem.
fn hi_verb_stem(infinitive: String) -> String {
// Irregular stems
if str_eq(infinitive, "होना") { return "हो" }
if str_eq(infinitive, "करना") { return "कर" }
if str_eq(infinitive, "जाना") { return "जा" }
if str_eq(infinitive, "आना") { return "" }
if str_eq(infinitive, "देना") { return "दे" }
if str_eq(infinitive, "लेना") { return "ले" }
if str_eq(infinitive, "देखना") { return "देख" }
if str_eq(infinitive, "कहना") { return "कह" }
if str_eq(infinitive, "जानना") { return "जान" }
if str_eq(infinitive, "चाहना") { return "चाह" }
if str_eq(infinitive, "खाना") { return "खा" }
if str_eq(infinitive, "पीना") { return "पी" }
if str_eq(infinitive, "सोना") { return "सो" }
if str_eq(infinitive, "लिखना") { return "लिख" }
if str_eq(infinitive, "पढ़ना") { return "पढ़" }
if str_eq(infinitive, "बोलना") { return "बोल" }
if str_eq(infinitive, "चलना") { return "चल" }
if str_eq(infinitive, "बैठना") { return "बैठ" }
if str_eq(infinitive, "उठना") { return "उठ" }
if str_eq(infinitive, "मिलना") { return "मिल" }
if str_eq(infinitive, "रहना") { return "रह" }
if str_eq(infinitive, "सुनना") { return "सुन" }
if str_eq(infinitive, "समझना") { return "समझ" }
if str_eq(infinitive, "मानना") { return "मान" }
if str_eq(infinitive, "बनाना") { return "बना" }
if str_eq(infinitive, "लाना") { return "ला" }
if str_eq(infinitive, "भेजना") { return "भेज" }
if str_eq(infinitive, "खोलना") { return "खोल" }
if str_eq(infinitive, "बंद करना") { return "बंद कर" }
// Generic: strip trailing (last character)
if hi_str_ends(infinitive, "ना") {
return hi_str_drop_last(infinitive, 1)
// Note: is two Unicode chars ( + ) but as a suffix pattern
// we want to strip the last char '' and keep 'न' however the
// actual Devanagari suffix is a single syllable grapheme cluster.
// str_drop_last drops the last Unicode codepoint.
// = (na) + (aa-matra): str_drop_last(s,1) drops , leaving at end.
// We need to drop 2 codepoints ( and = ) to get the true stem.
}
return infinitive
}
// hi_verb_stem_clean: correctly strips -न (2 Unicode codepoints: + matra)
fn hi_verb_stem_clean(infinitive: String) -> String {
// Irregulars first
if str_eq(infinitive, "होना") { return "हो" }
if str_eq(infinitive, "करना") { return "कर" }
if str_eq(infinitive, "जाना") { return "जा" }
if str_eq(infinitive, "आना") { return "" }
if str_eq(infinitive, "देना") { return "दे" }
if str_eq(infinitive, "लेना") { return "ले" }
if str_eq(infinitive, "देखना") { return "देख" }
if str_eq(infinitive, "कहना") { return "कह" }
if str_eq(infinitive, "जानना") { return "जान" }
if str_eq(infinitive, "चाहना") { return "चाह" }
if str_eq(infinitive, "खाना") { return "खा" }
if str_eq(infinitive, "पीना") { return "पी" }
if str_eq(infinitive, "सोना") { return "सो" }
if str_eq(infinitive, "लिखना") { return "लिख" }
if str_eq(infinitive, "पढ़ना") { return "पढ़" }
if str_eq(infinitive, "बोलना") { return "बोल" }
if str_eq(infinitive, "चलना") { return "चल" }
if str_eq(infinitive, "बैठना") { return "बैठ" }
if str_eq(infinitive, "उठना") { return "उठ" }
if str_eq(infinitive, "मिलना") { return "मिल" }
if str_eq(infinitive, "रहना") { return "रह" }
if str_eq(infinitive, "सुनना") { return "सुन" }
if str_eq(infinitive, "समझना") { return "समझ" }
if str_eq(infinitive, "मानना") { return "मान" }
if str_eq(infinitive, "बनाना") { return "बना" }
if str_eq(infinitive, "लाना") { return "ला" }
if str_eq(infinitive, "भेजना") { return "भेज" }
if str_eq(infinitive, "खोलना") { return "खोल" }
// Strip = 2 codepoints
if hi_str_ends(infinitive, "ना") {
return hi_str_drop_last(infinitive, 2)
}
return infinitive
}
// Present tense habitual conjugation
//
// Structure: stem + aspect-suffix + auxiliary
//
// Aspect suffixes (agree with SUBJECT gender/number):
// m sg: (taa)
// f sg: (tii)
// m pl: ते (te)
// f pl: (tii)
//
// Auxiliary /है/हैं (am/is/are) agreed with person and number:
// 1sg: हूँ (huuṃ)
// 2sg: (ho) [informal], हैं (haiṃ) [formal]
// 3sg: है (hai)
// 1pl: हैं (haiṃ)
// 2pl: (ho) [informal], हैं (haiṃ) [formal]
// 3pl: हैं (haiṃ)
fn hi_present_aspect(gender: String, number: String) -> String {
if str_eq(gender, "f") { return "ती" }
if str_eq(number, "pl") { return "ते" }
return "ता"
}
fn hi_aux_present(person: String, number: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "हूँ" }
return "हैं"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "हो" }
return "हो"
}
// third person
if str_eq(number, "sg") { return "है" }
return "हैं"
}
// Past tense conjugation
//
// Simple past suffix agrees with subject (when object has no ).
// stem + past-suffix:
// m sg: (aa) e.g. (khaayaa)
// f sg: (ii) e.g. (khaaii)
// m pl: (e) e.g. (khaae)
// f pl: ईं (iiṃ) e.g. ईं (khaaiṃ)
//
// Many verb stems ending in a vowel undergo contraction the irregulars table
// handles known cases; otherwise stem + suffix is concatenated.
fn hi_past_suffix(gender: String, number: String) -> String {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "" }
return ""
}
// feminine
if str_eq(number, "sg") { return "" }
return "ईं"
}
// Past tense irregulars full past form returned for known verb+gender+number.
fn hi_past_irregular(stem: String, gender: String, number: String) -> String {
// (ho): /थ/थे/थ
if str_eq(stem, "हो") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "था" }
return "थे"
}
if str_eq(number, "sg") { return "थी" }
return "थीं"
}
// (jaa): गय/गई/गए/गईं
if str_eq(stem, "जा") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "गया" }
return "गए"
}
if str_eq(number, "sg") { return "गई" }
return "गईं"
}
// करन (kar): ि/क/किए/क
if str_eq(stem, "कर") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "किया" }
return "किए"
}
if str_eq(number, "sg") { return "की" }
return "कीं"
}
// देन (de): ि/द/दिए/द
if str_eq(stem, "दे") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "दिया" }
return "दिए"
}
if str_eq(number, "sg") { return "दी" }
return "दीं"
}
// लेन (le): ि/ल/लिए/ल
if str_eq(stem, "ले") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "लिया" }
return "लिए"
}
if str_eq(number, "sg") { return "ली" }
return "लीं"
}
// आन (aa): आय/आई/आए/आईं
if str_eq(stem, "") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "आया" }
return "आए"
}
if str_eq(number, "sg") { return "आई" }
return "आईं"
}
// (khaa): /खई/खए/खईं
if str_eq(stem, "खा") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "खाया" }
return "खाए"
}
if str_eq(number, "sg") { return "खाई" }
return "खाईं"
}
// (pii): ि/प/पिए/प
if str_eq(stem, "पी") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "पिया" }
return "पिए"
}
if str_eq(number, "sg") { return "पी" }
return "पीं"
}
// No irregular match
return ""
}
// Future tense conjugation
//
// Future: stem + future-suffix (fused person+gender+number)
//
// 1sg m: ऊँग (uuṃgaa) 1sg f: ऊँग (uuṃgii)
// 2sg m: ओगे (oge) 2sg f: ओग (ogii)
// 3sg m: एग (egaa) 3sg f: एग (egii)
// 1pl m: एंगे (eṃge) 1pl f: एंग (eṃgii)
// 2pl m: ओगे (oge) 2pl f: ओग (ogii)
// 3pl m: एंगे (eṃge) 3pl f: एंग (eṃgii)
fn hi_future_suffix(person: String, number: String, gender: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") {
if str_eq(gender, "f") { return "ऊँगी" }
return "ऊँगा"
}
if str_eq(gender, "f") { return "एंगी" }
return "एंगे"
}
if str_eq(person, "2") {
if str_eq(gender, "f") { return "ओगी" }
return "ओगे"
}
// third person
if str_eq(number, "sg") {
if str_eq(gender, "f") { return "एगी" }
return "एगा"
}
if str_eq(gender, "f") { return "एंगी" }
return "एंगे"
}
// Tense suffix (aspect suffix only, without auxiliary)
//
// For callers that need just the aspect suffix to construct compound tenses.
fn hi_tense_suffix(tense: String, gender: String, number: String) -> String {
if str_eq(tense, "present") { return hi_present_aspect(gender, number) }
if str_eq(tense, "past") { return hi_past_suffix(gender, number) }
// future uses hi_future_suffix (includes person)
return ""
}
// (honaa to be) special handling
//
// is deeply irregular. Its forms are used as auxiliaries throughout.
//
// present: हूँ/ह/है/हैं (via hi_aux_present)
// past: /थ/थे/थ (via hi_past_irregular)
// future: ऊँग/हओगे/हएग etc. (stem + future suffix)
fn hi_hona_present(person: String, number: String) -> String {
return hi_aux_present(person, number)
}
fn hi_hona_past(gender: String, number: String) -> String {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "था" }
return "थे"
}
if str_eq(number, "sg") { return "थी" }
return "थीं"
}
// Full verb conjugation
//
// hi_conjugate: conjugate a Hindi verb.
//
// verb: Hindi infinitive in Devanagari (e.g. "खाना", "जाना", "करना")
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3"
// gender: "m" | "f" (subject gender)
// number: "sg" | "pl"
//
// Returns the complete inflected verb string (including auxiliary where needed).
fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String {
let stem: String = hi_verb_stem_clean(verb)
// fully irregular
if str_eq(verb, "होना") {
if str_eq(tense, "present") { return hi_hona_present(person, number) }
if str_eq(tense, "past") { return hi_hona_past(gender, number) }
// future: ऊँग etc.
return "हो" + hi_future_suffix(person, number, gender)
}
if str_eq(tense, "present") {
let aspect: String = hi_present_aspect(gender, number)
let aux: String = hi_aux_present(person, number)
return stem + aspect + " " + aux
}
if str_eq(tense, "past") {
let irreg: String = hi_past_irregular(stem, gender, number)
if !str_eq(irreg, "") {
return irreg
}
return stem + hi_past_suffix(gender, number)
}
if str_eq(tense, "future") {
return stem + hi_future_suffix(person, number, gender)
}
// Unknown tense: return infinitive
return verb
}
// Noun phrase construction helpers
// hi_noun_with_post: return the oblique noun form followed by its postposition.
// Use for all cases that require an oblique form (dative, locative, etc.).
//
// noun: Devanagari noun string
// gender: "m" | "f"
// number: "sg" | "pl"
// case: postposition case key (see hi_postposition)
fn hi_noun_with_post(noun: String, gender: String, number: String, gram_case: String) -> String {
let post: String = hi_postposition(gram_case)
if str_eq(post, "") {
// Nominative: use direct form
return hi_noun_direct(noun, gender, number)
}
let oblique: String = hi_noun_oblique(noun, gender, number)
return oblique + " " + post
}
// hi_genitive_phrase: "X का/की/के Y" possessive phrase.
// possessor and possessed are bare noun strings; the function computes all
// inflections and agreement automatically.
//
// possessor_gender/number: gender and number of the possessor noun
// possessed_gender/number: gender and number of the possessed noun (drives agreement)
fn hi_genitive_phrase(possessor: String, possessor_gender: String, possessor_number: String,
possessed: String, possessed_gender: String, possessed_number: String) -> String {
let obl: String = hi_noun_oblique(possessor, possessor_gender, possessor_number)
let gen: String = hi_agree_genitive(possessed_gender, possessed_number)
let poss: String = hi_noun_direct(possessed, possessed_gender, possessed_number)
return obl + " " + gen + " " + poss
}
+27
View File
@@ -0,0 +1,27 @@
// auto-generated by elc --emit-header — do not edit
extern fn hi_str_ends(s: String, suf: String) -> Bool
extern fn hi_str_drop_last(s: String, n: Int) -> String
extern fn hi_str_last_char(s: String) -> String
extern fn hi_gender(noun: String) -> String
extern fn hi_masc_aa_stem(noun: String) -> String
extern fn hi_noun_direct_m(noun: String, number: String) -> String
extern fn hi_noun_oblique_m(noun: String, number: String) -> String
extern fn hi_noun_direct_f(noun: String, number: String) -> String
extern fn hi_noun_oblique_f(noun: String, number: String) -> String
extern fn hi_noun_direct(noun: String, gender: String, number: String) -> String
extern fn hi_noun_oblique(noun: String, gender: String, number: String) -> String
extern fn hi_postposition(gram_case: String) -> String
extern fn hi_agree_genitive(possessed_gender: String, possessed_number: String) -> String
extern fn hi_verb_stem(infinitive: String) -> String
extern fn hi_verb_stem_clean(infinitive: String) -> String
extern fn hi_present_aspect(gender: String, number: String) -> String
extern fn hi_aux_present(person: String, number: String) -> String
extern fn hi_past_suffix(gender: String, number: String) -> String
extern fn hi_past_irregular(stem: String, gender: String, number: String) -> String
extern fn hi_future_suffix(person: String, number: String, gender: String) -> String
extern fn hi_tense_suffix(tense: String, gender: String, number: String) -> String
extern fn hi_hona_present(person: String, number: String) -> String
extern fn hi_hona_past(gender: String, number: String) -> String
extern fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn hi_noun_with_post(noun: String, gender: String, number: String, gram_case: String) -> String
extern fn hi_genitive_phrase(possessor: String, possessor_gender: String, possessor_number: String, possessed: String, possessed_gender: String, possessed_number: String) -> String
+450
View File
@@ -0,0 +1,450 @@
// morphology-ja.el - Japanese morphology: verb conjugation and noun particles.
//
// Japanese is SOV, agglutinative, pro-drop. Morphology is suffix-chain based.
//
// Key facts:
// - Nouns do not inflect; grammatical relations are marked by postpositional
// particles attached after the noun.
// - Verbs inflect for tense, polarity, and politeness.
// - Two main verb groups: Ichidan (vowel-stem, Group 2) and Godan (consonant-
// stem, Group 1), plus a small set of irregular verbs.
// - Politeness levels: plain (dictionary/casual) and polite (masu-form).
// - Questions are formed by appending the sentence-final particle (ka).
//
// Depends on: (no dependencies - standalone morphology module)
// Verb group classification
//
// Returns "ichidan" | "godan" | "irregular"
//
// Irregular verbs are checked first. Ichidan verbs end in -る and have a
// vowel immediately before the る. Everything else is Godan.
//
// Note: this is a heuristic classifier for romanized input. For production use
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
import "morphology.el"
fn ja_verb_group(dict_form: String) -> String {
// Irregular verbs (exact match on dictionary form)
if str_eq(dict_form, "する") { return "irregular" }
if str_eq(dict_form, "くる") { return "irregular" }
if str_eq(dict_form, "くる") { return "irregular" }
if str_eq(dict_form, "いる") { return "irregular" }
if str_eq(dict_form, "ある") { return "irregular" }
if str_eq(dict_form, "") { return "irregular" }
// Romanized irregulars
if str_eq(dict_form, "suru") { return "irregular" }
if str_eq(dict_form, "kuru") { return "irregular" }
if str_eq(dict_form, "iru") { return "irregular" }
if str_eq(dict_form, "aru") { return "irregular" }
if str_eq(dict_form, "da") { return "irregular" }
// Ichidan: ends in -る (-ru) and has a vowel before it.
// We test using native kana ending and the romanized -eru / -iru pattern.
if str_ends_with(dict_form, "") {
// Any kana-form ending in that is not in the irregular list is
// heuristically classified as Ichidan when the preceding mora ends in
// a vowel sound. For the romanized path we check -eru and -iru endings.
return "ichidan"
}
if str_ends_with(dict_form, "eru") { return "ichidan" }
if str_ends_with(dict_form, "iru") { return "ichidan" }
return "godan"
}
// Ichidan stem extraction
//
// Strip the final -る (-ru) from an Ichidan dictionary form.
// 食べる 食べ taberu tabe
fn ja_ichidan_stem(dict_form: String) -> String {
if str_ends_with(dict_form, "") {
let n: Int = str_len(dict_form)
// str_slice operates on byte offsets; る is 3 UTF-8 bytes.
// We trust the runtime to handle Unicode correctly via str_drop_last.
return str_drop_last(dict_form, 1)
}
if str_ends_with(dict_form, "ru") {
let n: Int = str_len(dict_form)
return str_slice(dict_form, 0, n - 2)
}
return dict_form
}
// Godan stem changes
//
// Godan verbs mutate their final kana to a different row before attaching a
// suffix. The "row" parameter selects which stem form is needed:
//
// "i" - the i-row (連用形 ren'yōkei): for polite forms, te-form base
// "a" - the a-row (未然形 mizenkei): for negative
// "te" - te/ta-form (special sound changes apply)
//
// Final kana i-row:
// Final kana a-row:
// Te/ta-form:
fn ja_godan_stem_change(dict_form: String, row: String) -> String {
let n: Int = str_len(dict_form)
if n == 0 { return dict_form }
// i-row (polite stem / ren'yōkei)
if str_eq(row, "i") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "ki" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "gi" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "shi" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "chi" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "ni" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "bi" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "mi" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "ri" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "i" }
return dict_form
}
// a-row (negative stem / mizenkei)
if str_eq(row, "a") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "ka" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "ga" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "sa" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "ta" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "na" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "ba" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "ma" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "ra" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "wa" }
return dict_form
}
// te/ta-row (te-form and plain past; special euphonic changes)
// Sound changes: い, (voiced), し, つ/る/うっ, ぬ/ぶ/む
if str_eq(row, "te") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "i" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "i" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "shi" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "tt" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "tt" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "tt" }
return dict_form
}
return dict_form
}
// Verb conjugation
//
// ja_conjugate(dict_form, form) -> String
//
// form values:
// "present" - plain non-past (dictionary form)
// "past" - plain past
// "negative" - plain negative
// "volitional" - plain volitional (let's )
// "polite" - polite non-past (masu-form)
// "polite-past" - polite past (mashita-form)
// "polite-neg" - polite negative (masen-form)
// "te" - te-form (connective / gerund)
fn ja_conjugate(dict_form: String, form: String) -> String {
let group: String = ja_verb_group(dict_form)
// Irregular verbs
if str_eq(group, "irregular") {
// する / suru (to do)
if str_eq(dict_form, "する") {
if str_eq(form, "present") { return "する" }
if str_eq(form, "past") { return "した" }
if str_eq(form, "negative") { return "しない" }
if str_eq(form, "volitional") { return "しよう" }
if str_eq(form, "polite") { return "します" }
if str_eq(form, "polite-past") { return "しました" }
if str_eq(form, "polite-neg") { return "しません" }
if str_eq(form, "te") { return "して" }
return dict_form
}
if str_eq(dict_form, "suru") {
if str_eq(form, "present") { return "suru" }
if str_eq(form, "past") { return "shita" }
if str_eq(form, "negative") { return "shinai" }
if str_eq(form, "volitional") { return "shiyou" }
if str_eq(form, "polite") { return "shimasu" }
if str_eq(form, "polite-past") { return "shimashita" }
if str_eq(form, "polite-neg") { return "shimasen" }
if str_eq(form, "te") { return "shite" }
return dict_form
}
// くる / kuru (to come)
if str_eq(dict_form, "くる") {
if str_eq(form, "present") { return "くる" }
if str_eq(form, "past") { return "きた" }
if str_eq(form, "negative") { return "こない" }
if str_eq(form, "volitional") { return "こよう" }
if str_eq(form, "polite") { return "きます" }
if str_eq(form, "polite-past") { return "きました" }
if str_eq(form, "polite-neg") { return "きません" }
if str_eq(form, "te") { return "きて" }
return dict_form
}
if str_eq(dict_form, "kuru") {
if str_eq(form, "present") { return "kuru" }
if str_eq(form, "past") { return "kita" }
if str_eq(form, "negative") { return "konai" }
if str_eq(form, "volitional") { return "koyou" }
if str_eq(form, "polite") { return "kimasu" }
if str_eq(form, "polite-past") { return "kimashita" }
if str_eq(form, "polite-neg") { return "kimasen" }
if str_eq(form, "te") { return "kite" }
return dict_form
}
// いる / iru (to be / exist, animate)
if str_eq(dict_form, "いる") {
if str_eq(form, "present") { return "いる" }
if str_eq(form, "past") { return "いた" }
if str_eq(form, "negative") { return "いない" }
if str_eq(form, "volitional") { return "いよう" }
if str_eq(form, "polite") { return "います" }
if str_eq(form, "polite-past") { return "いました" }
if str_eq(form, "polite-neg") { return "いません" }
if str_eq(form, "te") { return "いて" }
return dict_form
}
if str_eq(dict_form, "iru") {
if str_eq(form, "present") { return "iru" }
if str_eq(form, "past") { return "ita" }
if str_eq(form, "negative") { return "inai" }
if str_eq(form, "volitional") { return "iyou" }
if str_eq(form, "polite") { return "imasu" }
if str_eq(form, "polite-past") { return "imashita" }
if str_eq(form, "polite-neg") { return "imasen" }
if str_eq(form, "te") { return "ite" }
return dict_form
}
// ある / aru (to be / exist, inanimate)
if str_eq(dict_form, "ある") {
if str_eq(form, "present") { return "ある" }
if str_eq(form, "past") { return "あった" }
if str_eq(form, "negative") { return "ない" }
if str_eq(form, "volitional") { return "あろう" }
if str_eq(form, "polite") { return "あります" }
if str_eq(form, "polite-past") { return "ありました" }
if str_eq(form, "polite-neg") { return "ありません" }
if str_eq(form, "te") { return "あって" }
return dict_form
}
if str_eq(dict_form, "aru") {
if str_eq(form, "present") { return "aru" }
if str_eq(form, "past") { return "atta" }
if str_eq(form, "negative") { return "nai" }
if str_eq(form, "volitional") { return "arou" }
if str_eq(form, "polite") { return "arimasu" }
if str_eq(form, "polite-past") { return "arimashita" }
if str_eq(form, "polite-neg") { return "arimasen" }
if str_eq(form, "te") { return "atte" }
return dict_form
}
// / da (copula)
if str_eq(dict_form, "") {
if str_eq(form, "present") { return "" }
if str_eq(form, "past") { return "だった" }
if str_eq(form, "negative") { return "ではない" }
if str_eq(form, "volitional") { return "だろう" }
if str_eq(form, "polite") { return "です" }
if str_eq(form, "polite-past") { return "でした" }
if str_eq(form, "polite-neg") { return "ではありません" }
if str_eq(form, "te") { return "" }
return dict_form
}
if str_eq(dict_form, "da") {
if str_eq(form, "present") { return "da" }
if str_eq(form, "past") { return "datta" }
if str_eq(form, "negative") { return "dewanai" }
if str_eq(form, "volitional") { return "darou" }
if str_eq(form, "polite") { return "desu" }
if str_eq(form, "polite-past") { return "deshita" }
if str_eq(form, "polite-neg") { return "dewaarimarsen" }
if str_eq(form, "te") { return "de" }
return dict_form
}
// Unknown irregular fall through to base form
return dict_form
}
// Ichidan verbs
if str_eq(group, "ichidan") {
let stem: String = ja_ichidan_stem(dict_form)
if str_eq(form, "present") { return dict_form }
if str_eq(form, "past") { return stem + "" }
if str_eq(form, "negative") { return stem + "ない" }
if str_eq(form, "volitional") { return stem + "よう" }
if str_eq(form, "polite") { return stem + "ます" }
if str_eq(form, "polite-past") { return stem + "ました" }
if str_eq(form, "polite-neg") { return stem + "ません" }
if str_eq(form, "te") { return stem + "" }
return dict_form
}
// Godan verbs
// Godan plain present: dictionary form unchanged
if str_eq(form, "present") { return dict_form }
// Godan polite forms use the i-row stem + masu endings
if str_eq(form, "polite") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ます"
}
if str_eq(form, "polite-past") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ました"
}
if str_eq(form, "polite-neg") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ません"
}
// Godan plain negative uses the a-row stem + nai
if str_eq(form, "negative") {
let astem: String = ja_godan_stem_change(dict_form, "a")
return astem + "ない"
}
// Godan volitional: i-row + ou ( ending おう, others ろう via i-stem)
if str_eq(form, "volitional") {
// う-verbs: drop final and add おう
if str_ends_with(dict_form, "") {
return str_drop_last(dict_form, 1) + "おう"
}
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ろう"
}
// Godan te-form: euphonic te-row stem + (voiced ending いで)
if str_eq(form, "te") {
let tstem: String = ja_godan_stem_change(dict_form, "te")
// Voiced consonants () use instead of
if str_ends_with(dict_form, "") { return tstem + "いで" }
if str_ends_with(dict_form, "gu") { return tstem + "ide" }
// Nasal assimilation: ぬ/ぶ/む んで
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "nu") { return tstem + "nde" }
if str_ends_with(dict_form, "bu") { return tstem + "nde" }
if str_ends_with(dict_form, "mu") { return tstem + "nde" }
// して
if str_ends_with(dict_form, "") { return tstem + "して" }
if str_ends_with(dict_form, "su") { return tstem + "shite" }
// いて (tstem already has )
if str_ends_with(dict_form, "") { return tstem + "" }
if str_ends_with(dict_form, "ku") { return tstem + "te" }
// つ/る/う って
return tstem + ""
}
// Godan plain past: same stem changes as te-form, then た/だ
if str_eq(form, "past") {
let tstem: String = ja_godan_stem_change(dict_form, "te")
if str_ends_with(dict_form, "") { return tstem + "いだ" }
if str_ends_with(dict_form, "gu") { return tstem + "ida" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "nu") { return tstem + "nda" }
if str_ends_with(dict_form, "bu") { return tstem + "nda" }
if str_ends_with(dict_form, "mu") { return tstem + "nda" }
if str_ends_with(dict_form, "") { return tstem + "した" }
if str_ends_with(dict_form, "su") { return tstem + "shita" }
if str_ends_with(dict_form, "") { return tstem + "" }
if str_ends_with(dict_form, "ku") { return tstem + "ta" }
return tstem + ""
}
return dict_form
}
// Case particles
//
// Japanese nouns do not inflect; case is indicated by a postpositional particle
// placed directly after the noun.
fn ja_particle(gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "accusative") { return "" }
if str_eq(gram_case, "dative") { return "" }
if str_eq(gram_case, "genitive") { return "" }
if str_eq(gram_case, "topic") { return "" }
if str_eq(gram_case, "instrumental") { return "" }
if str_eq(gram_case, "locative") { return "" }
if str_eq(gram_case, "ablative") { return "から" }
if str_eq(gram_case, "direction") { return "" }
if str_eq(gram_case, "comitative") { return "" }
return ""
}
// Noun phrase construction
//
// ja_noun_phrase: attach the correct case particle to a noun.
// The particle immediately follows the noun with no space.
fn ja_noun_phrase(noun: String, gram_case: String) -> String {
let p: String = ja_particle(gram_case)
if str_eq(p, "") {
return noun
}
return noun + p
}
// Question particle
//
// Japanese questions are formed by appending (ka) to the end of a sentence.
fn ja_question_particle() -> String {
return ""
}
// Sentence-final particle attachment
//
// ja_make_question: append the question particle to a sentence.
fn ja_make_question(sentence: String) -> String {
return sentence + ja_question_particle()
}
+9
View File
@@ -0,0 +1,9 @@
// auto-generated by elc --emit-header — do not edit
extern fn ja_verb_group(dict_form: String) -> String
extern fn ja_ichidan_stem(dict_form: String) -> String
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
extern fn ja_conjugate(dict_form: String, form: String) -> String
extern fn ja_particle(gram_case: String) -> String
extern fn ja_noun_phrase(noun: String, gram_case: String) -> String
extern fn ja_question_particle() -> String
extern fn ja_make_question(sentence: String) -> String
+872
View File
@@ -0,0 +1,872 @@
// morphology-la.el - Latin morphology for the NLG engine.
//
// Implements fusional Latin verb conjugation and noun declension. Designed
// as a companion to morphology.el and called by the engine when the language
// profile code is "la".
//
// Language profile: code=la, name=Latin, morph_type=fusional, word_order=SOV,
// question_strategy=intonation, script=latin, family=italic.
//
// Verb conjugation covered:
// Tenses: present, past (perfect active), future
// Persons: first/second/third x singular/plural (slots 0-5)
// Conjugations: 1st (-are), 2nd (-ere long), 3rd (-ere short), 4th (-ire)
// Irregulars: esse (be), ire (go), velle (want), posse (can)
// Canonical map: "be" -> "esse"
//
// Noun declension covered:
// Cases: nominative, accusative, genitive, dative, ablative
// Declensions: 1st (-a fem), 2nd masc (-us), 2nd neut (-um), 3rd (-is),
// 4th (-us), 5th (-es)
//
// Latin has no articles. la_noun_phrase returns the declined noun directly.
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn la_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn la_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn la_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn la_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn la_str_last3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, n - 3, n)
}
// Person/number slot
//
// Maps person x number to a 0-based slot index used in paradigm tables.
// 0 = 1st singular (ego)
// 1 = 2nd singular (tu)
// 2 = 3rd singular (is/ea/id)
// 3 = 1st plural (nos)
// 4 = 2nd plural (vos)
// 5 = 3rd plural (ei/eae/ea)
fn la_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Conjugation class detection
//
// Latin verbs fall into four conjugation classes identified by the infinitive
// ending. The 2nd and 3rd conjugations both end in -ere; the 2nd has a long
// e (stem vowel retained before -re), the 3rd has a short e (thematic vowel).
// Heuristic: if the verb stem before -ere ends in a consonant cluster or a
// single consonant preceded by a short vowel, treat as 3rd; otherwise 2nd.
// In practice the caller passes dictionary infinitives, so we check known 2nd
// conjugation markers (stem ends in -e before -re, i.e. the penult is e) and
// known 4th (-ire) vs 3rd (-ere with consonant stem).
//
// Simplified detection strategy:
// ends in -are -> "1"
// ends in -ire -> "4"
// ends in -ere -> check penultimate vowel pattern:
// stem + "ere" where stem ends in a vowel-like syllable -> "2"
// otherwise -> "3"
//
// For the small working vocabulary, we rely on the ending length heuristic:
// -ere with 2nd conj: the infinitive without -re has a long e as last char,
// meaning the four chars before the final "re" are "e-consonant" or "ee".
// Simpler: ends in -ere and has stem length >= 3 with last stem char a vowel
// -> 2nd; else 3rd. This correctly handles monere (2nd) vs dicere (3rd).
fn la_verb_class(verb: String) -> String {
if la_str_ends(verb, "are") { return "1" }
if la_str_ends(verb, "ire") { return "4" }
if la_str_ends(verb, "ere") {
// Strip -ere, look at the last char of the stem
let stem: String = la_str_drop_last(verb, 3)
let slen: Int = str_len(stem)
if slen == 0 { return "3" }
let last: String = str_slice(stem, slen - 1, slen)
// Stem ends in a vowel -> 2nd conjugation (monere, videre, habere)
if str_eq(last, "a") { return "2" }
if str_eq(last, "e") { return "2" }
if str_eq(last, "i") { return "2" }
if str_eq(last, "o") { return "2" }
if str_eq(last, "u") { return "2" }
// Stem ends in a consonant -> 3rd conjugation (dicere, edere, ducere)
return "3"
}
// Default: treat as 3rd if ending is unknown
return "3"
}
// la_stem: strip the infinitive ending to get the present stem.
//
// 1st: -are -> stem (amāre -> am-)
// 2nd: -ere -> stem + e (monēre -> mone-) [keep the stem vowel]
// 3rd: -ere -> stem (dicere -> dic-)
// 4th: -ire -> stem + i (audire -> audi-)
fn la_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "1") { return la_str_drop_last(verb, 3) }
if str_eq(vclass, "2") { return la_str_drop_last(verb, 2) } // drop -re; keep -e
if str_eq(vclass, "3") { return la_str_drop_last(verb, 3) }
if str_eq(vclass, "4") { return la_str_drop_last(verb, 2) } // drop -re; keep -i
return la_str_drop_last(verb, 3)
}
// la_perfect_stem: derive the perfect active stem.
//
// Latin perfect stems are highly irregular in general. For regular verbs the
// most common pattern is stem + v- (1st/4th) or stem + u- (2nd/3rd), but this
// is not universal. We use the following heuristic approximations:
//
// 1st conj: present stem + av- (amavi, portavi)
// 2nd conj: present stem + u- (monui, habui) -- drops the -e from stem
// 3rd conj: present stem + - (doubled root; context varies; use stem + i)
// 4th conj: present stem + iv- (audivi)
//
// This will be wrong for many common verbs; callers can override by adding the
// verb to the la_irregular_perfect table.
fn la_perfect_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "1") {
// amāre -> am + av = amav
let pstem: String = la_str_drop_last(verb, 3)
return pstem + "av"
}
if str_eq(vclass, "2") {
// monēre -> mone -> drop e -> mon + u = monu
let pstem: String = la_str_drop_last(verb, 3)
return pstem + "u"
}
if str_eq(vclass, "3") {
// dicere -> dic + (perfect varies; approximate with stem + -i stem)
let pstem: String = la_str_drop_last(verb, 3)
return pstem
}
if str_eq(vclass, "4") {
// audire -> audi + iv = audiv
let pstem: String = la_str_drop_last(verb, 2)
return pstem + "v"
}
return la_str_drop_last(verb, 3)
}
// Perfect active endings (all conjugations share these)
//
// Slot: 0=1sg 1=2sg 2=3sg 3=1pl 4=2pl 5=3pl
// -i, -isti, -it, -imus, -istis, -erunt
fn la_perfect_ending(slot: Int) -> String {
if slot == 0 { return "i" }
if slot == 1 { return "isti" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "istis" }
return "erunt"
}
// Present tense endings
//
// 1st: -o, -as, -at, -amus, -atis, -ant
// 2nd: -eo, -es, -et, -emus, -etis, -ent
// 3rd: -o, -is, -it, -imus, -itis, -unt
// 4th: -io, -is, -it, -imus, -itis, -iunt
//
// Note: for 1st conj the stem already ends in the thematic vowel (am- not ama-
// at slot 0 because the -o absorbs it). The 2nd conj stem retains its -e and
// the ending is appended directly.
fn la_present_ending(vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
if slot == 0 { return "o" }
if slot == 1 { return "as" }
if slot == 2 { return "at" }
if slot == 3 { return "amus" }
if slot == 4 { return "atis" }
return "ant"
}
if str_eq(vclass, "2") {
if slot == 0 { return "o" }
if slot == 1 { return "s" } // stem ends -e; -es becomes mones
if slot == 2 { return "t" } // monet
if slot == 3 { return "mus" } // monemus
if slot == 4 { return "tis" } // monetis
return "nt" // monent
}
if str_eq(vclass, "3") {
if slot == 0 { return "o" }
if slot == 1 { return "is" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "itis" }
return "unt"
}
// 4th (-ire)
if slot == 0 { return "o" }
if slot == 1 { return "s" } // audi + s = audis
if slot == 2 { return "t" } // audit
if slot == 3 { return "mus" } // audimus
if slot == 4 { return "tis" } // auditis
return "unt" // audiunt
}
// la_present_form: build the present tense form for a regular verb.
//
// Special slot-0 handling per class:
// 1st: am- + o = amo (strip thematic -a from stem first)
// 2nd: mone- + o = moneo
// 3rd: dic- + o = dico
// 4th: audi- + o = audio (the -i stays as part of the stem)
fn la_present_form(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
if slot == 0 {
// stem ends in the thematic -a which is absorbed by -o
return la_str_drop_last(stem, 1) + "o"
}
return stem + la_present_ending(vclass, slot)
}
if str_eq(vclass, "2") {
// stem ends in -e; all endings attach directly
return stem + la_present_ending(vclass, slot)
}
if str_eq(vclass, "3") {
if slot == 0 {
return stem + "o"
}
return stem + la_present_ending(vclass, slot)
}
// 4th: stem ends in -i
if slot == 0 {
return stem + "o" // audio
}
if slot == 5 {
return stem + "unt" // audiunt (special: i + unt)
}
return stem + la_present_ending(vclass, slot)
}
// Future tense
//
// 1st/2nd conjugations: present stem + bo/bis/bit/bimus/bitis/bunt
// (1st: thematic -a dropped before -bo; 2nd: -e kept, ebo -> moneo... actually
// for 2nd the future is mone + bo = monebo; stem keeps -e)
//
// 3rd/4th conjugations: present stem + am/es/et/emus/etis/ent
// (No thematic vowel linking; -am directly on consonant stem)
fn la_future_ending_12(slot: Int) -> String {
if slot == 0 { return "bo" }
if slot == 1 { return "bis" }
if slot == 2 { return "bit" }
if slot == 3 { return "bimus" }
if slot == 4 { return "bitis" }
return "bunt"
}
fn la_future_ending_34(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "es" }
if slot == 2 { return "et" }
if slot == 3 { return "emus" }
if slot == 4 { return "etis" }
return "ent"
}
fn la_future_form(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
// Drop thematic -a then add -bo etc: am- + bo = amabo? No: ama + bo = amabo
// Actually for 1st conj the stem IS the thematic-vowel stem (ama-),
// and the future is ama + bo = amabo.
return stem + la_future_ending_12(slot)
}
if str_eq(vclass, "2") {
// mone + bo = monebo
return stem + la_future_ending_12(slot)
}
if str_eq(vclass, "3") {
// dic + am = dicam
return stem + la_future_ending_34(slot)
}
// 4th: audi + am = audiam
return stem + la_future_ending_34(slot)
}
// Irregular verb tables
//
// esse (be), ire (go), velle (want), posse (can/be able)
fn la_esse_present(slot: Int) -> String {
if slot == 0 { return "sum" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sumus" }
if slot == 4 { return "estis" }
return "sunt"
}
fn la_esse_past(slot: Int) -> String {
if slot == 0 { return "fui" }
if slot == 1 { return "fuisti" }
if slot == 2 { return "fuit" }
if slot == 3 { return "fuimus" }
if slot == 4 { return "fuistis" }
return "fuerunt"
}
fn la_esse_future(slot: Int) -> String {
if slot == 0 { return "ero" }
if slot == 1 { return "eris" }
if slot == 2 { return "erit" }
if slot == 3 { return "erimus" }
if slot == 4 { return "eritis" }
return "erunt"
}
fn la_ire_present(slot: Int) -> String {
if slot == 0 { return "eo" }
if slot == 1 { return "is" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "itis" }
return "eunt"
}
fn la_ire_past(slot: Int) -> String {
if slot == 0 { return "ii" }
if slot == 1 { return "isti" } // contracted: iisti -> isti
if slot == 2 { return "iit" }
if slot == 3 { return "iimus" }
if slot == 4 { return "istis" }
return "ierunt"
}
fn la_ire_future(slot: Int) -> String {
if slot == 0 { return "ibo" }
if slot == 1 { return "ibis" }
if slot == 2 { return "ibit" }
if slot == 3 { return "ibimus" }
if slot == 4 { return "ibitis" }
return "ibunt"
}
fn la_velle_present(slot: Int) -> String {
if slot == 0 { return "volo" }
if slot == 1 { return "vis" }
if slot == 2 { return "vult" }
if slot == 3 { return "volumus" }
if slot == 4 { return "vultis" }
return "volunt"
}
fn la_velle_past(slot: Int) -> String {
if slot == 0 { return "volui" }
if slot == 1 { return "voluisti" }
if slot == 2 { return "voluit" }
if slot == 3 { return "voluimus" }
if slot == 4 { return "voluistis" }
return "voluerunt"
}
fn la_velle_future(slot: Int) -> String {
if slot == 0 { return "volam" }
if slot == 1 { return "voles" }
if slot == 2 { return "volet" }
if slot == 3 { return "volemus" }
if slot == 4 { return "voletis" }
return "volent"
}
fn la_posse_present(slot: Int) -> String {
if slot == 0 { return "possum" }
if slot == 1 { return "potes" }
if slot == 2 { return "potest" }
if slot == 3 { return "possumus" }
if slot == 4 { return "potestis" }
return "possunt"
}
fn la_posse_past(slot: Int) -> String {
if slot == 0 { return "potui" }
if slot == 1 { return "potuisti" }
if slot == 2 { return "potuit" }
if slot == 3 { return "potuimus" }
if slot == 4 { return "potuistis" }
return "potuerunt"
}
fn la_posse_future(slot: Int) -> String {
if slot == 0 { return "potero" }
if slot == 1 { return "poteris" }
if slot == 2 { return "poterit" }
if slot == 3 { return "poterimus" }
if slot == 4 { return "poteritis" }
return "poterunt"
}
// Irregular perfect stems for common verbs
//
// Returns the perfect stem for common irregular verbs, or "" if not found.
// When a perfect stem is returned, la_perfect_ending() is appended directly.
//
// Verb Perfect stem Example: 3sg
// edere ed- edit
// dicere dix- dixit
// ducere dux- duxit
// facere fec- fecit
// capere cep- cepit
// venire ven- venit (same as present; context clarifies)
// videre vid- vidit
// esse -> handled separately
// ire -> handled separately
fn la_irregular_perfect_stem(verb: String) -> String {
if str_eq(verb, "edere") { return "ed" }
if str_eq(verb, "dicere") { return "dix" }
if str_eq(verb, "ducere") { return "dux" }
if str_eq(verb, "facere") { return "fec" }
if str_eq(verb, "capere") { return "cep" }
if str_eq(verb, "venire") { return "ven" }
if str_eq(verb, "videre") { return "vid" }
if str_eq(verb, "bibere") { return "bib" }
if str_eq(verb, "currere") { return "cucurr" }
if str_eq(verb, "legere") { return "leg" }
if str_eq(verb, "scribere") { return "scrips" }
if str_eq(verb, "vivere") { return "vix" }
if str_eq(verb, "cadere") { return "cecid" }
if str_eq(verb, "ponere") { return "posu" }
if str_eq(verb, "querere") { return "quaesiv" }
return ""
}
// Canonical verb mapping
//
// The semantic layer passes English canonical labels ("be", "go", "want").
// Map these to Latin infinitives before conjugation.
fn la_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "esse" }
if str_eq(verb, "go") { return "ire" }
if str_eq(verb, "want") { return "velle" }
if str_eq(verb, "can") { return "posse" }
if str_eq(verb, "eat") { return "edere" }
if str_eq(verb, "say") { return "dicere" }
if str_eq(verb, "see") { return "videre" }
if str_eq(verb, "make") { return "facere" }
if str_eq(verb, "come") { return "venire" }
if str_eq(verb, "read") { return "legere" }
if str_eq(verb, "write") { return "scribere" }
if str_eq(verb, "run") { return "currere" }
if str_eq(verb, "live") { return "vivere" }
if str_eq(verb, "love") { return "amare" }
return verb
}
// la_conjugate: main conjugation entry point
//
// verb: Latin infinitive (e.g. "amare", "esse") or English canonical
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the base (infinitive) when a form
// is not implemented rather than crashing.
fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String {
// Map canonical English labels to Latin infinitives
let v: String = la_map_canonical(verb)
let slot: Int = la_slot(person, number)
// Irregulars
if str_eq(v, "esse") {
if str_eq(tense, "present") { return la_esse_present(slot) }
if str_eq(tense, "past") { return la_esse_past(slot) }
if str_eq(tense, "future") { return la_esse_future(slot) }
return v
}
if str_eq(v, "ire") {
if str_eq(tense, "present") { return la_ire_present(slot) }
if str_eq(tense, "past") { return la_ire_past(slot) }
if str_eq(tense, "future") { return la_ire_future(slot) }
return v
}
if str_eq(v, "velle") {
if str_eq(tense, "present") { return la_velle_present(slot) }
if str_eq(tense, "past") { return la_velle_past(slot) }
if str_eq(tense, "future") { return la_velle_future(slot) }
return v
}
if str_eq(v, "posse") {
if str_eq(tense, "present") { return la_posse_present(slot) }
if str_eq(tense, "past") { return la_posse_past(slot) }
if str_eq(tense, "future") { return la_posse_future(slot) }
return v
}
// Regular conjugation
let vclass: String = la_verb_class(v)
let stem: String = la_stem(v, vclass)
if str_eq(tense, "present") {
return la_present_form(stem, vclass, slot)
}
if str_eq(tense, "past") {
// Check for a known irregular perfect stem first
let irreg_perf: String = la_irregular_perfect_stem(v)
if !str_eq(irreg_perf, "") {
return irreg_perf + la_perfect_ending(slot)
}
// Regular perfect stem derivation
let perf_stem: String = la_perfect_stem(v, vclass)
return perf_stem + la_perfect_ending(slot)
}
if str_eq(tense, "future") {
return la_future_form(stem, vclass, slot)
}
// Unknown tense: return infinitive
return v
}
// Declension detection
//
// Infer Latin declension from the nominative singular ending.
//
// ends in -a -> 1st declension (feminine)
// ends in -us -> 2nd or 4th; disambiguate: if genitive suffix pattern
// suggests 4th (-us gen) we use 4th, otherwise 2nd masc.
// Heuristic: monosyllabic -us nouns and known 4th-decl words
// use 4th; longer -us words default to 2nd.
// ends in -um -> 2nd declension neuter
// ends in -is -> 3rd declension (genitive -is is also 3rd nom for some words;
// treat nom -is as 3rd)
// ends in -es -> 5th declension
// ends in -er -> 2nd masc (puer, ager)
// otherwise -> 3rd declension (consonant stems: rex, miles, etc.)
fn la_declension(noun: String) -> String {
if la_str_ends(noun, "a") { return "1" }
if la_str_ends(noun, "um") { return "2n" }
if la_str_ends(noun, "er") { return "2m" }
if la_str_ends(noun, "us") {
// 4th declension heuristic: check known 4th decl nouns
if str_eq(noun, "manus") { return "4" }
if str_eq(noun, "usus") { return "4" }
if str_eq(noun, "fructus") { return "4" }
if str_eq(noun, "gradus") { return "4" }
if str_eq(noun, "cursus") { return "4" }
if str_eq(noun, "sensus") { return "4" }
if str_eq(noun, "spiritus") { return "4" }
if str_eq(noun, "portus") { return "4" }
if str_eq(noun, "domus") { return "4" }
if str_eq(noun, "impetus") { return "4" }
// Default: 2nd masc
return "2m"
}
if la_str_ends(noun, "es") { return "5" }
if la_str_ends(noun, "is") { return "3" }
// Consonant-stem 3rd declension (rex, canis, leo, etc.)
return "3"
}
// 1st declension: -a nouns (mostly feminine)
//
// Stem: remove final -a
// Singular: nom -a gen -ae dat -ae acc -am abl -a
// Plural: nom -ae gen -arum dat -is acc -as abl -is
fn la_decline_1(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "ae" }
if str_eq(gram_case, "dative") { return stem + "ae" }
if str_eq(gram_case, "accusative") { return stem + "am" }
if str_eq(gram_case, "ablative") { return stem + "a" }
// vocative same as nominative for 1st decl
return stem + "a"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "ae" }
if str_eq(gram_case, "genitive") { return stem + "arum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "as" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "ae"
}
// 2nd declension masculine: -us nouns
//
// Stem: remove final -us
// Singular: nom -us gen -i dat -o acc -um abl -o
// Plural: nom -i gen -orum dat -is acc -os abl -is
fn la_decline_2m(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "i" }
if str_eq(gram_case, "dative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "o" }
return stem + "us"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "i" }
if str_eq(gram_case, "genitive") { return stem + "orum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "os" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "i"
}
// 2nd declension neuter: -um nouns
//
// Stem: remove final -um
// Singular: nom/acc -um gen -i dat/abl -o
// Plural: nom/acc -a gen -orum dat/abl -is
fn la_decline_2n(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "um" }
if str_eq(gram_case, "genitive") { return stem + "i" }
if str_eq(gram_case, "dative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "o" }
return stem + "um"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "orum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "a"
}
// 3rd declension: consonant and i-stem nouns
//
// The 3rd declension is highly varied; the nominative singular is usually
// irregular (the stem is seen in the genitive). The caller passes the
// nominative singular form; we use it as-is for nominative and treat it as
// the stem for other cases (approximation for productive NLG use).
//
// Singular: nom (unchanged) gen -is dat -i acc -em abl -e
// Plural: nom -es gen -um dat -ibus acc -es abl -ibus
//
// For i-stems (is ending in nom sg) we keep the full form as the stem and
// add endings directly to the base minus -is.
fn la_decline_3(noun: String, gram_case: String, number: String) -> String {
// For 3rd decl the nom sg is given; the stem for oblique cases is
// derived by stripping -is if the nom ends in -is, otherwise we use
// the noun as the stem for the oblique and append endings.
let oblique_stem: String = ""
if la_str_ends(noun, "is") {
let oblique_stem = la_str_drop_last(noun, 2)
} else {
let oblique_stem = noun
}
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "genitive") { return oblique_stem + "is" }
if str_eq(gram_case, "dative") { return oblique_stem + "i" }
if str_eq(gram_case, "accusative") { return oblique_stem + "em" }
if str_eq(gram_case, "ablative") { return oblique_stem + "e" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return oblique_stem + "es" }
if str_eq(gram_case, "genitive") { return oblique_stem + "um" }
if str_eq(gram_case, "dative") { return oblique_stem + "ibus" }
if str_eq(gram_case, "accusative") { return oblique_stem + "es" }
if str_eq(gram_case, "ablative") { return oblique_stem + "ibus" }
return oblique_stem + "es"
}
// 4th declension: -us nouns (mostly masculine)
//
// Stem: remove final -us
// Singular: nom -us gen -us dat -ui acc -um abl -u
// Plural: nom -us gen -uum dat -ibus acc -us abl -ibus
fn la_decline_4(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "us" }
if str_eq(gram_case, "dative") { return stem + "ui" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "u" }
return stem + "us"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "uum" }
if str_eq(gram_case, "dative") { return stem + "ibus" }
if str_eq(gram_case, "accusative") { return stem + "us" }
if str_eq(gram_case, "ablative") { return stem + "ibus" }
return stem + "us"
}
// 5th declension: -es nouns (mostly feminine)
//
// Stem: remove final -es
// Singular: nom -es gen -ei dat -ei acc -em abl -e
// Plural: nom -es gen -erum dat -ebus acc -es abl -ebus
fn la_decline_5(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "es" }
if str_eq(gram_case, "genitive") { return stem + "ei" }
if str_eq(gram_case, "dative") { return stem + "ei" }
if str_eq(gram_case, "accusative") { return stem + "em" }
if str_eq(gram_case, "ablative") { return stem + "e" }
return stem + "es"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "es" }
if str_eq(gram_case, "genitive") { return stem + "erum" }
if str_eq(gram_case, "dative") { return stem + "ebus" }
if str_eq(gram_case, "accusative") { return stem + "es" }
if str_eq(gram_case, "ablative") { return stem + "ebus" }
return stem + "es"
}
// 2nd declension -er nouns
//
// Nouns like "puer" retain -e throughout; nouns like "ager" drop it in oblique.
// Heuristic: if the stem (drop -er) + er would be the original, check if the
// penultimate char before -er is also r (like "puer": stem "pu" + "er" = puer)
// vs "ager": stem "agr" drops e. We default to retaining -e (puer pattern)
// for simplicity; "ager" type is less common.
//
// Singular: nom -er gen -i dat -o acc -um abl -o
// Plural: nom -i gen -orum dat -is acc -os abl -is
fn la_decline_2er(noun: String, gram_case: String, number: String) -> String {
// Oblique stem: drop -er then add "r" to get true stem? For simplicity
// use the form with -e retained (puer pattern): stem = noun minus "r".
// This correctly handles "puer" -> "pueri", "puero" etc.
// For ager-type the caller would need to pass the stem form; we approximate.
let stem: String = la_str_drop_last(noun, 1) // drop final -r -> "pue"
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "genitive") { return stem + "ri" } // pueri
if str_eq(gram_case, "dative") { return stem + "ro" } // puero
if str_eq(gram_case, "accusative") { return stem + "rum" } // puerum
if str_eq(gram_case, "ablative") { return stem + "ro" } // puero
return noun
}
// plural
if str_eq(gram_case, "nominative") { return stem + "ri" } // pueri
if str_eq(gram_case, "genitive") { return stem + "rorum" } // puerorum
if str_eq(gram_case, "dative") { return stem + "ris" } // pueris
if str_eq(gram_case, "accusative") { return stem + "ros" } // pueros
if str_eq(gram_case, "ablative") { return stem + "ris" } // pueris
return stem + "ri"
}
// la_decline: main declension entry point
//
// noun: nominative singular Latin noun (e.g. "feles", "aqua", "dominus")
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "ablative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the nominative singular when a
// form is not implemented.
fn la_decline(noun: String, gram_case: String, number: String) -> String {
let decl: String = la_declension(noun)
if str_eq(decl, "1") {
let stem: String = la_str_drop_last(noun, 1)
return la_decline_1(stem, gram_case, number)
}
if str_eq(decl, "2m") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_2m(stem, gram_case, number)
}
if str_eq(decl, "2n") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_2n(stem, gram_case, number)
}
if str_eq(decl, "2er") {
return la_decline_2er(noun, gram_case, number)
}
if str_eq(decl, "3") {
return la_decline_3(noun, gram_case, number)
}
if str_eq(decl, "4") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_4(stem, gram_case, number)
}
if str_eq(decl, "5") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_5(stem, gram_case, number)
}
// Unknown declension: return nominative unchanged
return noun
}
// la_noun_phrase: noun phrase builder
//
// Latin has no definite or indefinite articles. The declined noun form is the
// complete noun phrase. The definite parameter is accepted for interface
// compatibility with other language modules but has no effect.
//
// noun: nominative singular Latin noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "ablative"
// number: "singular" | "plural"
// definite: ignored (Latin has no articles)
fn la_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return la_decline(noun, gram_case, number)
}
+41
View File
@@ -0,0 +1,41 @@
// auto-generated by elc --emit-header — do not edit
extern fn la_str_ends(s: String, suf: String) -> Bool
extern fn la_str_drop_last(s: String, n: Int) -> String
extern fn la_str_last_char(s: String) -> String
extern fn la_str_last2(s: String) -> String
extern fn la_str_last3(s: String) -> String
extern fn la_slot(person: String, number: String) -> Int
extern fn la_verb_class(verb: String) -> String
extern fn la_stem(verb: String, vclass: String) -> String
extern fn la_perfect_stem(verb: String, vclass: String) -> String
extern fn la_perfect_ending(slot: Int) -> String
extern fn la_present_ending(vclass: String, slot: Int) -> String
extern fn la_present_form(stem: String, vclass: String, slot: Int) -> String
extern fn la_future_ending_12(slot: Int) -> String
extern fn la_future_ending_34(slot: Int) -> String
extern fn la_future_form(stem: String, vclass: String, slot: Int) -> String
extern fn la_esse_present(slot: Int) -> String
extern fn la_esse_past(slot: Int) -> String
extern fn la_esse_future(slot: Int) -> String
extern fn la_ire_present(slot: Int) -> String
extern fn la_ire_past(slot: Int) -> String
extern fn la_ire_future(slot: Int) -> String
extern fn la_velle_present(slot: Int) -> String
extern fn la_velle_past(slot: Int) -> String
extern fn la_velle_future(slot: Int) -> String
extern fn la_posse_present(slot: Int) -> String
extern fn la_posse_past(slot: Int) -> String
extern fn la_posse_future(slot: Int) -> String
extern fn la_irregular_perfect_stem(verb: String) -> String
extern fn la_map_canonical(verb: String) -> String
extern fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn la_declension(noun: String) -> String
extern fn la_decline_1(stem: String, gram_case: String, number: String) -> String
extern fn la_decline_2m(stem: String, gram_case: String, number: String) -> String
extern fn la_decline_2n(stem: String, gram_case: String, number: String) -> String
extern fn la_decline_3(noun: String, gram_case: String, number: String) -> String
extern fn la_decline_4(stem: String, gram_case: String, number: String) -> String
extern fn la_decline_5(stem: String, gram_case: String, number: String) -> String
extern fn la_decline_2er(noun: String, gram_case: String, number: String) -> String
extern fn la_decline(noun: String, gram_case: String, number: String) -> String
extern fn la_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+556
View File
@@ -0,0 +1,556 @@
// morphology-non.el - Old Norse morphology for the NLG engine.
//
// Implements Old Norse verb conjugation and noun declension for the ca. 700-1100 CE
// period. Designed as a companion to morphology.el and called by the engine when
// the language profile code is "non".
//
// Language profile: code=non, name=Old Norse, morph_type=fusional, word_order=V2,
// question_strategy=inversion, script=latin, family=germanic.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third x singular/plural (slots 0-5)
// Classes: weak (-a infinitive: -aði past), and a set of common strong/irregular verbs
// Irregulars: vera (be), hafa (have), ganga (go), sjá (see), segja (say),
// koma (come)
// Canonical map: "be" -> "vera"
//
// Noun declension covered:
// Strong masculine a-stem (like "armr")
// Strong feminine ō-stem (like "gör")
// Strong neuter a-stem (like "land")
// Cases: nominative, accusative, genitive, dative
// Numbers: singular, plural
// Definite suffix: appended to the declined form (masc -inn/-ins/-inum, neut -it)
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn non_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn non_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn non_last(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular (ek)
// 1 = 2nd singular (þú)
// 2 = 3rd singular (hann/hon/þat)
// 3 = 1st plural (vér)
// 4 = 2nd plural (þér)
// 5 = 3rd plural (þeir/þær/þau)
fn non_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Irregular verb tables
//
// Each irregular verb has a present and past paradigm of six forms (slots 0-5).
fn non_vera_present(slot: Int) -> String {
if slot == 0 { return "em" }
if slot == 1 { return "ert" }
if slot == 2 { return "er" }
if slot == 3 { return "erum" }
if slot == 4 { return "eruð" }
return "eru"
}
fn non_vera_past(slot: Int) -> String {
if slot == 0 { return "var" }
if slot == 1 { return "vart" }
if slot == 2 { return "var" }
if slot == 3 { return "vórum" }
if slot == 4 { return "vóruð" }
return "vóru"
}
fn non_hafa_present(slot: Int) -> String {
if slot == 0 { return "hefi" }
if slot == 1 { return "hefr" }
if slot == 2 { return "hefr" }
if slot == 3 { return "höfum" }
if slot == 4 { return "hafið" }
return "hafa"
}
fn non_hafa_past(slot: Int) -> String {
if slot == 0 { return "hafða" }
if slot == 1 { return "hafðir" }
if slot == 2 { return "hafði" }
if slot == 3 { return "höfðum" }
if slot == 4 { return "höfðuð" }
return "höfðu"
}
fn non_ganga_present(slot: Int) -> String {
if slot == 0 { return "geng" }
if slot == 1 { return "gengr" }
if slot == 2 { return "gengr" }
if slot == 3 { return "göngum" }
if slot == 4 { return "gangið" }
return "ganga"
}
fn non_ganga_past(slot: Int) -> String {
if slot == 0 { return "gekk" }
if slot == 1 { return "gekkt" }
if slot == 2 { return "gekk" }
if slot == 3 { return "gengum" }
if slot == 4 { return "genguð" }
return "gengu"
}
fn non_sja_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "sér" }
if slot == 2 { return "sér" }
if slot == 3 { return "séum" }
if slot == 4 { return "séið" }
return "sjá"
}
fn non_sja_past(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "sást" }
if slot == 2 { return "" }
if slot == 3 { return "sám" }
if slot == 4 { return "sáð" }
return "sáu"
}
fn non_segja_present(slot: Int) -> String {
if slot == 0 { return "segi" }
if slot == 1 { return "segir" }
if slot == 2 { return "segir" }
if slot == 3 { return "segjum" }
if slot == 4 { return "segið" }
return "segja"
}
fn non_segja_past(slot: Int) -> String {
if slot == 0 { return "sagði" }
if slot == 1 { return "sagðir" }
if slot == 2 { return "sagði" }
if slot == 3 { return "sögðum" }
if slot == 4 { return "sögðuð" }
return "sögðu"
}
fn non_koma_present(slot: Int) -> String {
if slot == 0 { return "kem" }
if slot == 1 { return "kemr" }
if slot == 2 { return "kemr" }
if slot == 3 { return "komum" }
if slot == 4 { return "komið" }
return "koma"
}
fn non_koma_past(slot: Int) -> String {
if slot == 0 { return "kom" }
if slot == 1 { return "komt" }
if slot == 2 { return "kom" }
if slot == 3 { return "komum" }
if slot == 4 { return "komuð" }
return "komu"
}
// Canonical verb mapping
//
// Maps English semantic labels to Old Norse infinitives so the semantic layer
// can request forms without knowing the target language lexeme.
fn non_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "vera" }
if str_eq(verb, "have") { return "hafa" }
if str_eq(verb, "go") { return "ganga" }
if str_eq(verb, "see") { return "sjá" }
if str_eq(verb, "say") { return "segja" }
if str_eq(verb, "come") { return "koma" }
return verb
}
// Weak verb conjugation
//
// Weak verbs (infinitive ending in -a) form the productive conjugation class.
//
// Present tense (stem = drop final -a from infinitive):
// Sg: 1st stem + -a 2nd stem + -ar 3rd stem + -ar
// Pl: 1st stem + -um 2nd stem + -ið 3rd stem + -a
//
// Past tense (suffix -aði- inserted after stem):
// Sg: 1st stem + -aði 2nd stem + -aðir 3rd stem + -aði
// Pl: 1st stem + -uðum 2nd stem + -uðuð 3rd stem + -uðu
fn non_weak_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "a" }
if slot == 1 { return stem + "ar" }
if slot == 2 { return stem + "ar" }
if slot == 3 { return stem + "um" }
if slot == 4 { return stem + "" }
return stem + "a"
}
fn non_weak_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "aði" }
if slot == 1 { return stem + "aðir" }
if slot == 2 { return stem + "aði" }
if slot == 3 { return stem + "uðum" }
if slot == 4 { return stem + "uðuð" }
return stem + "uðu"
}
// non_conjugate: main conjugation entry point
//
// verb: Old Norse infinitive (e.g. "kalla", "vera") or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive rather
// than crashing.
fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = non_map_canonical(verb)
let slot: Int = non_slot(person, number)
// Irregulars
if str_eq(v, "vera") {
if str_eq(tense, "present") { return non_vera_present(slot) }
if str_eq(tense, "past") { return non_vera_past(slot) }
return v
}
if str_eq(v, "hafa") {
if str_eq(tense, "present") { return non_hafa_present(slot) }
if str_eq(tense, "past") { return non_hafa_past(slot) }
return v
}
if str_eq(v, "ganga") {
if str_eq(tense, "present") { return non_ganga_present(slot) }
if str_eq(tense, "past") { return non_ganga_past(slot) }
return v
}
if str_eq(v, "sjá") {
if str_eq(tense, "present") { return non_sja_present(slot) }
if str_eq(tense, "past") { return non_sja_past(slot) }
return v
}
if str_eq(v, "segja") {
if str_eq(tense, "present") { return non_segja_present(slot) }
if str_eq(tense, "past") { return non_segja_past(slot) }
return v
}
if str_eq(v, "koma") {
if str_eq(tense, "present") { return non_koma_present(slot) }
if str_eq(tense, "past") { return non_koma_past(slot) }
return v
}
// Regular weak verb (-a infinitive)
//
// If the verb ends in -a, strip it to get the stem and apply weak endings.
// Otherwise fall back to returning the base form unchanged.
if non_str_ends(v, "a") {
let stem: String = non_drop(v, 1)
if str_eq(tense, "present") { return non_weak_present(stem, slot) }
if str_eq(tense, "past") { return non_weak_past(stem, slot) }
return v
}
// Unknown verb form: return the infinitive unchanged
return v
}
// Strong masculine a-stem declension ("armr" arm)
//
// The paradigm below uses "armr" as the exemplar. The caller passes the full
// nominative singular (including the -r ending); the oblique stem is derived
// by stripping -r (or -ur) from the nominative.
//
// Singular: nom armr acc arm gen arms dat armi
// Plural: nom armar acc arma gen arma dat örmum
//
// The dative plural -örmum involves u-umlaut of the root vowel. Because umlaut
// depends on the root vowel in complex ways, the module stores the dative plural
// stem separately: for "armr" it is "örm". For unknown nouns we approximatehere
// by returning the nominative plural (armar) rather than crashing.
fn non_decline_masc(noun: String, gram_case: String, number: String) -> String {
// Derive oblique stem by dropping the nominative -r ending if present.
let stem: String = noun
if non_str_ends(noun, "r") {
let stem = non_drop(noun, 1)
}
// Hard-code the known exemplar "armr" fully, including the umlauted dat pl.
if str_eq(noun, "armr") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "armr" }
if str_eq(gram_case, "accusative") { return "arm" }
if str_eq(gram_case, "genitive") { return "arms" }
if str_eq(gram_case, "dative") { return "armi" }
return "armr"
}
if str_eq(gram_case, "nominative") { return "armar" }
if str_eq(gram_case, "accusative") { return "arma" }
if str_eq(gram_case, "genitive") { return "arma" }
if str_eq(gram_case, "dative") { return "örmum" }
return "armar"
}
// Generic strong masculine a-stem: use stripped stem + endings.
// Dative plural umlaut is approximated as nominative plural (safe fallback).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "r" }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "s" }
if str_eq(gram_case, "dative") { return stem + "i" }
return stem + "r"
}
if str_eq(gram_case, "nominative") { return stem + "ar" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "a" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "ar"
}
// Strong feminine ō-stem declension ("gör" gear)
//
// ō-stem feminines have a distinct set of endings. The stem is the nominative
// singular form itself (no case suffix in nom sg for this class, though some
// nouns add -ar in nom/acc pl).
//
// Singular: nom gör acc görvar gen görvar dat görvi
// Plural: nom görvar acc görvar gen görva dat görvum
fn non_decline_fem(noun: String, gram_case: String, number: String) -> String {
// Use "gör" as the fully specified exemplar.
if str_eq(noun, "gör") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "gör" }
if str_eq(gram_case, "accusative") { return "görvar" }
if str_eq(gram_case, "genitive") { return "görvar" }
if str_eq(gram_case, "dative") { return "görvi" }
return "gör"
}
if str_eq(gram_case, "nominative") { return "görvar" }
if str_eq(gram_case, "accusative") { return "görvar" }
if str_eq(gram_case, "genitive") { return "görva" }
if str_eq(gram_case, "dative") { return "görvum" }
return "görvar"
}
// Generic ō-stem feminine: noun base + endings (approximate).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun + "var" }
if str_eq(gram_case, "genitive") { return noun + "var" }
if str_eq(gram_case, "dative") { return noun + "vi" }
return noun
}
if str_eq(gram_case, "nominative") { return noun + "var" }
if str_eq(gram_case, "accusative") { return noun + "var" }
if str_eq(gram_case, "genitive") { return noun + "va" }
if str_eq(gram_case, "dative") { return noun + "vum" }
return noun + "var"
}
// Strong neuter a-stem declension ("land" land)
//
// Neuter a-stems show no ending in nom/acc sg and u-umlaut in the dative plural.
//
// Singular: nom land acc land gen lands dat landi
// Plural: nom lönd acc lönd gen landa dat löndum
fn non_decline_neut(noun: String, gram_case: String, number: String) -> String {
if str_eq(noun, "land") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "land" }
if str_eq(gram_case, "accusative") { return "land" }
if str_eq(gram_case, "genitive") { return "lands" }
if str_eq(gram_case, "dative") { return "landi" }
return "land"
}
if str_eq(gram_case, "nominative") { return "lönd" }
if str_eq(gram_case, "accusative") { return "lönd" }
if str_eq(gram_case, "genitive") { return "landa" }
if str_eq(gram_case, "dative") { return "löndum" }
return "lönd"
}
// Generic strong neuter a-stem (no umlaut approximated falls back to base).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "s" }
if str_eq(gram_case, "dative") { return noun + "i" }
return noun
}
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun
}
// Gender detection heuristic
//
// Old Norse gender must ideally be supplied by the lexicon. As a heuristic:
// ends in -r (after removing) and root looks consonant-final -> masculine
// ends in no case suffix (just the root vowel) -> try feminine
// monosyllabic root without final r -> neuter
//
// The module uses a simplified approach: known exemplars are routed to their
// specific paradigm; everything else defaults to strong masculine.
fn non_detect_gender(noun: String) -> String {
if str_eq(noun, "land") { return "neuter" }
if str_eq(noun, "gör") { return "feminine" }
// Default: masculine
return "masculine"
}
// non_decline: main declension entry point
//
// noun: nominative singular Old Norse noun (e.g. "armr", "land", "gör")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to nominative singular on unknown input.
fn non_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = non_detect_gender(noun)
if str_eq(gender, "masculine") { return non_decline_masc(noun, gram_case, number) }
if str_eq(gender, "feminine") { return non_decline_fem(noun, gram_case, number) }
if str_eq(gender, "neuter") { return non_decline_neut(noun, gram_case, number) }
// Fallback
return noun
}
// Definite suffix table
//
// Old Norse expresses definiteness via a suffix enclitic attached to the end of
// the inflected noun form (not a separate article). The suffix agrees with gender
// and case.
//
// Masculine singular: nom -inn gen -ins dat -inum acc -inn
// Neuter singular: nom -it gen -ins dat -inu acc -it
// Feminine singular: nom -in gen -innar dat -inni acc -ina
// Plural (all genders): nom/acc -inir (masc), -in (neut), -inar (fem)
// simplified to -in for plural in this module.
fn non_def_suffix_masc(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "inn" }
if str_eq(gram_case, "genitive") { return "ins" }
if str_eq(gram_case, "dative") { return "inum" }
if str_eq(gram_case, "accusative") { return "inn" }
return "inn"
}
// plural
if str_eq(gram_case, "nominative") { return "inir" }
if str_eq(gram_case, "accusative") { return "ina" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "unum" }
return "inir"
}
fn non_def_suffix_neut(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "it" }
if str_eq(gram_case, "genitive") { return "ins" }
if str_eq(gram_case, "dative") { return "inu" }
if str_eq(gram_case, "accusative") { return "it" }
return "it"
}
// plural
if str_eq(gram_case, "nominative") { return "in" }
if str_eq(gram_case, "accusative") { return "in" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "unum" }
return "in"
}
fn non_def_suffix_fem(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "in" }
if str_eq(gram_case, "genitive") { return "innar" }
if str_eq(gram_case, "dative") { return "inni" }
if str_eq(gram_case, "accusative") { return "ina" }
return "in"
}
// plural
if str_eq(gram_case, "nominative") { return "inar" }
if str_eq(gram_case, "accusative") { return "inar" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "innar" }
return "inar"
}
// non_noun_phrase: noun phrase builder
//
// Old Norse definiteness is expressed through a suffix enclitic, not a separate
// article. For indefinite nouns the declined form is returned as-is.
//
// noun: nominative singular Old Norse noun (e.g. "armr", "land")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false" (string comparison)
//
// Returns the complete noun phrase string.
fn non_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let base: String = non_decline(noun, gram_case, number)
if !str_eq(definite, "true") { return base }
// Append the appropriate definite suffix.
let gender: String = non_detect_gender(noun)
if str_eq(gender, "masculine") {
return base + non_def_suffix_masc(gram_case, number)
}
if str_eq(gender, "neuter") {
return base + non_def_suffix_neut(gram_case, number)
}
if str_eq(gender, "feminine") {
return base + non_def_suffix_fem(gram_case, number)
}
// Fallback: append generic -inn
return base + "inn"
}
+30
View File
@@ -0,0 +1,30 @@
// auto-generated by elc --emit-header — do not edit
extern fn non_str_ends(s: String, suf: String) -> Bool
extern fn non_drop(s: String, n: Int) -> String
extern fn non_last(s: String) -> String
extern fn non_slot(person: String, number: String) -> Int
extern fn non_vera_present(slot: Int) -> String
extern fn non_vera_past(slot: Int) -> String
extern fn non_hafa_present(slot: Int) -> String
extern fn non_hafa_past(slot: Int) -> String
extern fn non_ganga_present(slot: Int) -> String
extern fn non_ganga_past(slot: Int) -> String
extern fn non_sja_present(slot: Int) -> String
extern fn non_sja_past(slot: Int) -> String
extern fn non_segja_present(slot: Int) -> String
extern fn non_segja_past(slot: Int) -> String
extern fn non_koma_present(slot: Int) -> String
extern fn non_koma_past(slot: Int) -> String
extern fn non_map_canonical(verb: String) -> String
extern fn non_weak_present(stem: String, slot: Int) -> String
extern fn non_weak_past(stem: String, slot: Int) -> String
extern fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn non_decline_masc(noun: String, gram_case: String, number: String) -> String
extern fn non_decline_fem(noun: String, gram_case: String, number: String) -> String
extern fn non_decline_neut(noun: String, gram_case: String, number: String) -> String
extern fn non_detect_gender(noun: String) -> String
extern fn non_decline(noun: String, gram_case: String, number: String) -> String
extern fn non_def_suffix_masc(gram_case: String, number: String) -> String
extern fn non_def_suffix_neut(gram_case: String, number: String) -> String
extern fn non_def_suffix_fem(gram_case: String, number: String) -> String
extern fn non_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+348
View File
@@ -0,0 +1,348 @@
// morphology-peo.el - Old Persian morphology for the NLG engine.
//
// Implements Old Persian verb conjugation and noun declension for the ca. 600-300 BCE
// period (Achaemenid Empire). Designed as a companion to morphology.el and called
// by the engine when the language profile code is "peo".
//
// Language profile: code=peo, name=Old Persian, morph_type=fusional, word_order=SOV,
// question_strategy=particle, script=latin, family=iranian.
//
// Old Persian is attested primarily in royal cuneiform inscriptions (Behistun,
// Persepolis, Naqsh-e Rostam, etc.). The transliteration used here follows the
// standard scholarly convention with macrons for long vowels (ā, ī, ū). The corpus
// is small most productive forms are individually attested in the inscriptions.
//
// Verb conjugation covered:
// Tenses: present, past (imperfect)
// Persons: first/second/third x singular/plural (slots 0-5)
// Irregulars: ah- (to be), kar- (to do/make), xšāya- (to rule),
// tar- (to cross/overcome), dā- (to give)
// Canonical map: "be" -> "ah-"
// Pro-drop: the subject pronoun is typically omitted; the engine provides
// conjugated forms only and does not add pronouns.
//
// Noun declension covered:
// a-stem masculine (like "dahyu" country/land)
// Cases: nominative, accusative, genitive, dative x singular, plural
// (Many case distinctions collapsed in practice; 4 cases implemented)
// Articles: none Old Persian has no article system
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn peo_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn peo_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular (adam)
// 1 = 2nd singular (tuvam)
// 2 = 3rd singular (hauv)
// 3 = 1st plural (vayam)
// 4 = 2nd plural (yuvam)
// 5 = 3rd plural (taiy)
fn peo_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Regular present endings
//
// Old Persian present active indicative endings (Schmitt 1991 reconstruction):
// Sg: 1st -āmiy 2nd -ahiy 3rd -atiy
// Pl: 1st -āmahy 2nd -ātā 3rd -antiy
//
// These attach to the present stem. The present stem typically equals the
// verbal root (sometimes with a thematic vowel -a-).
fn peo_present_suffix(slot: Int) -> String {
if slot == 0 { return "āmiy" }
if slot == 1 { return "ahiy" }
if slot == 2 { return "atiy" }
if slot == 3 { return "āmahy" }
if slot == 4 { return "ātā" }
return "antiy"
}
// Regular past (imperfect) endings
//
// Old Persian imperfect (past) endings. The imperfect is formed with the augment
// a- prefixed to the stem, but since many verbs in the inscriptions appear without
// the augment or with it fossilised, the engine omits the augment prefix and
// returns only the stem + ending combination.
//
// Imperfect endings:
// Sg: 1st -am 2nd 3rd -a
// Pl: 1st -āmā 2nd -ātā 3rd
fn peo_past_suffix(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "ā" }
if slot == 2 { return "a" }
if slot == 3 { return "āmā" }
if slot == 4 { return "ātā" }
return "ā"
}
// Irregular verb tables
// ah- (to be) the Old Persian verb for being/existence
// Present: amiy, ahiy, astiy, amahy, astā, hatiy
// Past: āham, āha, āha, āhama, āhata, āhan
fn peo_ah_present(slot: Int) -> String {
if slot == 0 { return "amiy" }
if slot == 1 { return "ahiy" }
if slot == 2 { return "astiy" }
if slot == 3 { return "amahy" }
if slot == 4 { return "astā" }
return "hatiy"
}
fn peo_ah_past(slot: Int) -> String {
if slot == 0 { return "āham" }
if slot == 1 { return "āha" }
if slot == 2 { return "āha" }
if slot == 3 { return "āhama" }
if slot == 4 { return "āhata" }
return "āhan"
}
// kar- (to do/make) highly productive verb, uses present stem kun-/kunav-
// Present: kunāmiy, kunāhiy, kunautiy, kunāmahy, kunātā, kunavantiy
// Past: akunavam (1sg), akunava (3sg) limited attestation; simplified
fn peo_kar_present(slot: Int) -> String {
if slot == 0 { return "kunāmiy" }
if slot == 1 { return "kunāhiy" }
if slot == 2 { return "kunautiy" }
if slot == 3 { return "kunāmahy" }
if slot == 4 { return "kunātā" }
return "kunavantiy"
}
fn peo_kar_past(slot: Int) -> String {
if slot == 0 { return "akunavam" }
if slot == 1 { return "akunavā" }
if slot == 2 { return "akunava" }
if slot == 3 { return "akunavāmā" }
if slot == 4 { return "akunavātā" }
return "akunavan"
}
// xšāya- (to rule) verb of the royal inscriptions, thematic stem xšāya-
// Present: xšāyāmiy, xšāyāhiy, xšāyatiy, xšāyāmahy, xšāyātā, xšāyantiy
fn peo_xsaya_present(slot: Int) -> String {
if slot == 0 { return "xšāyāmiy" }
if slot == 1 { return "xšāyāhiy" }
if slot == 2 { return "xšāyatiy" }
if slot == 3 { return "xšāyāmahy" }
if slot == 4 { return "xšāyātā" }
return "xšāyantiy"
}
// tar- (to cross/overcome) limited attestation in inscriptions
// Attested: taratiy (3sg pres), tarantiy (3pl pres)
// Other slots approximated using regular present endings on stem tar-
fn peo_tar_present(slot: Int) -> String {
if slot == 2 { return "taratiy" }
if slot == 5 { return "tarantiy" }
// Other slots: apply regular endings to stem tar-
return "tar" + peo_present_suffix(slot)
}
// dā- (to give) long-vowel root, contracts in some forms
// Present: dāmiy, dāhiy, dātiy, dāmahy, dātā, dantiy
fn peo_da_present(slot: Int) -> String {
if slot == 0 { return "dāmiy" }
if slot == 1 { return "dāhiy" }
if slot == 2 { return "dātiy" }
if slot == 3 { return "dāmahy" }
if slot == 4 { return "dātā" }
return "dantiy"
}
fn peo_da_past(slot: Int) -> String {
if slot == 0 { return "adām" }
if slot == 1 { return "adāā" }
if slot == 2 { return "adā" }
if slot == 3 { return "adāmā" }
if slot == 4 { return "adātā" }
return "adān"
}
// Canonical verb mapping
//
// Maps English semantic labels to Old Persian verbal stems / citation forms.
fn peo_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "ah" }
if str_eq(verb, "do") { return "kar" }
if str_eq(verb, "make") { return "kar" }
if str_eq(verb, "rule") { return "xšāya" }
if str_eq(verb, "cross") { return "tar" }
if str_eq(verb, "give") { return "" }
return verb
}
// peo_conjugate: main conjugation entry point
//
// verb: Old Persian verbal stem or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Pro-drop language the engine produces conjugated
// verb forms only; subject pronouns are omitted unless explicitly generated by
// the semantic layer.
fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = peo_map_canonical(verb)
let slot: Int = peo_slot(person, number)
// ah- (to be)
if str_eq(v, "ah") {
if str_eq(tense, "present") { return peo_ah_present(slot) }
if str_eq(tense, "past") { return peo_ah_past(slot) }
return v
}
// kar- (to do/make)
if str_eq(v, "kar") {
if str_eq(tense, "present") { return peo_kar_present(slot) }
if str_eq(tense, "past") { return peo_kar_past(slot) }
return v
}
// xšāya- (to rule)
if str_eq(v, "xšāya") {
if str_eq(tense, "present") { return peo_xsaya_present(slot) }
// Past of xšāya-: apply imperfect endings to xšāya- stem
if str_eq(tense, "past") { return "xšāya" + peo_past_suffix(slot) }
return v
}
// tar- (to cross/overcome)
if str_eq(v, "tar") {
if str_eq(tense, "present") { return peo_tar_present(slot) }
if str_eq(tense, "past") { return "tar" + peo_past_suffix(slot) }
return v
}
// dā- (to give)
if str_eq(v, "") {
if str_eq(tense, "present") { return peo_da_present(slot) }
if str_eq(tense, "past") { return peo_da_past(slot) }
return v
}
// Regular thematic verb
//
// Apply present or past endings directly to the stem. The stem is assumed
// to be in the form supplied (Old Persian thematic stems typically end in -a-).
if str_eq(tense, "present") { return v + peo_present_suffix(slot) }
if str_eq(tense, "past") { return v + peo_past_suffix(slot) }
// Unknown tense: return the bare stem
return v
}
// a-stem masculine declension ("dahyu" country/land)
//
// The a-stem is the primary masculine nominal class in Old Persian. The paradigm
// below uses "dahyu" (country, land the most frequent noun in the inscriptions)
// as the fully specified exemplar.
//
// Singular: nom dahyāuš acc dahyum gen dahyāuš dat dahyavā
// Plural: nom dahyāva acc dahyūn gen dahyūnām dat dahyubiyā
//
// The singular nom/gen syncretism (both dahyāuš) is historically inherited from
// Old Iranian; it is not a scribal error.
fn peo_decline_astem(noun: String, gram_case: String, number: String) -> String {
if str_eq(noun, "dahyu") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "dahyāuš" }
if str_eq(gram_case, "accusative") { return "dahyum" }
if str_eq(gram_case, "genitive") { return "dahyāuš" }
if str_eq(gram_case, "dative") { return "dahyavā" }
return "dahyāuš"
}
if str_eq(gram_case, "nominative") { return "dahyāva" }
if str_eq(gram_case, "accusative") { return "dahyūn" }
if str_eq(gram_case, "genitive") { return "dahyūnām" }
if str_eq(gram_case, "dative") { return "dahyubiyā" }
return "dahyāva"
}
// Generic a-stem masculine: apply Old Persian nominal endings to base.
// Base form assumed to be the uninflected stem (without final -u/-a).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun + "āuš" }
if str_eq(gram_case, "accusative") { return noun + "am" }
if str_eq(gram_case, "genitive") { return noun + "āuš" }
if str_eq(gram_case, "dative") { return noun + "avā" }
return noun + "āuš"
}
// plural
if str_eq(gram_case, "nominative") { return noun + "āva" }
if str_eq(gram_case, "accusative") { return noun + "ūn" }
if str_eq(gram_case, "genitive") { return noun + "ūnām" }
if str_eq(gram_case, "dative") { return noun + "ubiyā" }
return noun + "āva"
}
// peo_decline: main declension entry point
//
// noun: Old Persian noun (nominative singular or uninflected stem)
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Only the a-stem masculine paradigm is currently implemented; other stem classes
// fall back to the nominative singular form. The corpus is small enough that
// most attested nouns are a-stems or i-stems whose oblique forms can be
// approximated by the a-stem pattern.
fn peo_decline(noun: String, gram_case: String, number: String) -> String {
return peo_decline_astem(noun, gram_case, number)
}
// peo_noun_phrase: noun phrase builder
//
// Old Persian has no definite or indefinite articles. Definiteness was expressed
// contextually or through word order, not morphologically. This function ignores
// the "definite" parameter and returns the declined noun form directly.
//
// noun: Old Persian noun (nominative singular or stem)
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false" (ignored no articles in Old Persian)
//
// Returns the declined noun form.
fn peo_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return peo_decline(noun, gram_case, number)
}
+19
View File
@@ -0,0 +1,19 @@
// auto-generated by elc --emit-header — do not edit
extern fn peo_drop(s: String, n: Int) -> String
extern fn peo_ends(s: String, suf: String) -> Bool
extern fn peo_slot(person: String, number: String) -> Int
extern fn peo_present_suffix(slot: Int) -> String
extern fn peo_past_suffix(slot: Int) -> String
extern fn peo_ah_present(slot: Int) -> String
extern fn peo_ah_past(slot: Int) -> String
extern fn peo_kar_present(slot: Int) -> String
extern fn peo_kar_past(slot: Int) -> String
extern fn peo_xsaya_present(slot: Int) -> String
extern fn peo_tar_present(slot: Int) -> String
extern fn peo_da_present(slot: Int) -> String
extern fn peo_da_past(slot: Int) -> String
extern fn peo_map_canonical(verb: String) -> String
extern fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn peo_decline_astem(noun: String, gram_case: String, number: String) -> String
extern fn peo_decline(noun: String, gram_case: String, number: String) -> String
extern fn peo_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+546
View File
@@ -0,0 +1,546 @@
// morphology-pi.el - Pali morphology for the NLG engine.
//
// Implements Pali verb conjugation and noun declension using standard IAST-subset
// Latin transliteration. Pali is the liturgical language of Theravada Buddhism
// (canonical form of Middle Indo-Aryan, ca. 3rd century BCE).
//
// Language profile: code=pi, name=Pali, morph_type=fusional, word_order=SOV,
// question_strategy=particle, script=latin-iast, family=indo-aryan.
//
// Verb conjugation covered:
// Tenses: present (bhū-ādi class), aorist (past), future
// Persons: first/second/third x singular/plural (slots 0-5)
// Endings: present -āmi/-asi/-ati/-āma/-atha/-anti
// aorist -iṃ/-i/-i/-imhā/-ittha/-iṃsu
// future -issāmi/-issasi/-issati/-issāma/-issatha/-issanti
// Irregulars: atthi/hoti (be), gacchati (go), passati (see),
// vadati (say), karoti (do)
// Canonical map: "be" -> "hoti"
//
// Noun declension covered:
// a-stem masculine (paradigm: "purisa" man): 8 cases, sg + pl
// ā-stem feminine (paradigm: "kaññā" girl): 8 cases, sg (pl approx.)
// Cases: nominative, accusative, instrumental, dative, ablative,
// genitive, locative, vocative
//
// Articles:
// Pali has no articles. pi_noun_phrase returns the declined noun directly.
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn pi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn pi_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn pi_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular (ahaṃ)
// 1 = 2nd singular (tvaṃ)
// 2 = 3rd singular (so/sā/taṃ)
// 3 = 1st plural (mayaṃ)
// 4 = 2nd plural (tumhe)
// 5 = 3rd plural (te/tā/tāni)
fn pi_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Present tense endings (bhū-ādi class)
//
// The standard present active endings for the most common Pali verb class:
// 1sg -āmi 2sg -asi 3sg -ati
// 1pl -āma 2pl -atha 3pl -anti
fn pi_present_ending(slot: Int) -> String {
if slot == 0 { return "āmi" }
if slot == 1 { return "asi" }
if slot == 2 { return "ati" }
if slot == 3 { return "āma" }
if slot == 4 { return "atha" }
return "anti"
}
// Aorist (past) tense endings
//
// The Pali aorist is the standard simple past. The most common set:
// 1sg -iṃ 2sg -i 3sg -i
// 1pl -imhā 2pl -ittha 3pl -iṃsu
//
// These are appended to the verb root (or a modified stem). For regular
// weak aorists, the root is taken from the infinitive by stripping -ati.
fn pi_aorist_ending(slot: Int) -> String {
if slot == 0 { return "iṃ" }
if slot == 1 { return "i" }
if slot == 2 { return "i" }
if slot == 3 { return "imhā" }
if slot == 4 { return "ittha" }
return "iṃsu"
}
// Future tense endings
//
// Future is formed with the suffix -issa- inserted between root and personal ending:
// 1sg -issāmi 2sg -issasi 3sg -issati
// 1pl -issāma 2pl -issatha 3pl -issanti
fn pi_future_ending(slot: Int) -> String {
if slot == 0 { return "issāmi" }
if slot == 1 { return "issasi" }
if slot == 2 { return "issati" }
if slot == 3 { return "issāma" }
if slot == 4 { return "issatha" }
return "issanti"
}
// Irregular verb tables
//
// atthi/hoti (to be): two parallel paradigms.
// The "hoti" paradigm is used by default (canonical "be" maps here).
// "atthi" paradigm is included for the commonly seen alternative.
fn pi_hoti_present(slot: Int) -> String {
if slot == 0 { return "homi" }
if slot == 1 { return "hosi" }
if slot == 2 { return "hoti" }
if slot == 3 { return "homa" }
if slot == 4 { return "hotha" }
return "honti"
}
fn pi_atthi_present(slot: Int) -> String {
if slot == 0 { return "amhi" }
if slot == 1 { return "asi" }
if slot == 2 { return "atthi" }
if slot == 3 { return "amha" }
if slot == 4 { return "attha" }
return "santi"
}
// Past (aorist) of hoti/atthi: āsi paradigm (a-aorist)
fn pi_hoti_aorist(slot: Int) -> String {
if slot == 0 { return "āsiṃ" }
if slot == 1 { return "āsi" }
if slot == 2 { return "āsi" }
if slot == 3 { return "āsimhā" }
if slot == 4 { return "āsittha" }
return "āsiṃsu"
}
// Future of hoti: regular -issa- on root "ho-"
fn pi_hoti_future(slot: Int) -> String {
if slot == 0 { return "hossāmi" }
if slot == 1 { return "hossasi" }
if slot == 2 { return "hossati" }
if slot == 3 { return "hossāma" }
if slot == 4 { return "hossatha" }
return "hossanti"
}
// gacchati (to go): present is suppletive; aorist uses root "gam-"/"agamā-"
fn pi_gacchati_present(slot: Int) -> String {
if slot == 0 { return "gacchāmi" }
if slot == 1 { return "gacchasi" }
if slot == 2 { return "gacchati" }
if slot == 3 { return "gacchāma" }
if slot == 4 { return "gacchatha" }
return "gacchanti"
}
fn pi_gacchati_aorist(slot: Int) -> String {
if slot == 0 { return "agamāsiṃ" }
if slot == 1 { return "agamāsi" }
if slot == 2 { return "agamāsi" }
if slot == 3 { return "agamāsimhā" }
if slot == 4 { return "agamāsittha" }
return "agamaṃsu"
}
fn pi_gacchati_future(slot: Int) -> String {
if slot == 0 { return "gamissāmi" }
if slot == 1 { return "gamissasi" }
if slot == 2 { return "gamissati" }
if slot == 3 { return "gamissāma" }
if slot == 4 { return "gamissatha" }
return "gamissanti"
}
// passati (to see): regular present; aorist root "dis-"/"addasā-"
fn pi_passati_present(slot: Int) -> String {
if slot == 0 { return "passāmi" }
if slot == 1 { return "passasi" }
if slot == 2 { return "passati" }
if slot == 3 { return "passāma" }
if slot == 4 { return "passatha" }
return "passanti"
}
fn pi_passati_aorist(slot: Int) -> String {
if slot == 0 { return "addasāsiṃ" }
if slot == 1 { return "addasāsi" }
if slot == 2 { return "addasāsi" }
if slot == 3 { return "addasāsimhā" }
if slot == 4 { return "addasāsittha" }
return "addasāsiṃsu"
}
fn pi_passati_future(slot: Int) -> String {
if slot == 0 { return "dakkhissāmi" }
if slot == 1 { return "dakkhissasi" }
if slot == 2 { return "dakkhissati" }
if slot == 3 { return "dakkhissāma" }
if slot == 4 { return "dakkhissatha" }
return "dakkhissanti"
}
// vadati (to say): regular throughout; aorist uses avadi-
fn pi_vadati_present(slot: Int) -> String {
if slot == 0 { return "vadāmi" }
if slot == 1 { return "vadasi" }
if slot == 2 { return "vadati" }
if slot == 3 { return "vadāma" }
if slot == 4 { return "vadatha" }
return "vadanti"
}
fn pi_vadati_aorist(slot: Int) -> String {
if slot == 0 { return "avadāsiṃ" }
if slot == 1 { return "avadāsi" }
if slot == 2 { return "avadāsi" }
if slot == 3 { return "avadāsimhā" }
if slot == 4 { return "avadāsittha" }
return "avadāsiṃsu"
}
fn pi_vadati_future(slot: Int) -> String {
if slot == 0 { return "vadissāmi" }
if slot == 1 { return "vadissasi" }
if slot == 2 { return "vadissati" }
if slot == 3 { return "vadissāma" }
if slot == 4 { return "vadissatha" }
return "vadissanti"
}
// karoti (to do/make): karo- present stem; kāri-/akāsi aorist
fn pi_karoti_present(slot: Int) -> String {
if slot == 0 { return "karomi" }
if slot == 1 { return "karosi" }
if slot == 2 { return "karoti" }
if slot == 3 { return "karoma" }
if slot == 4 { return "karotha" }
return "karonti"
}
fn pi_karoti_aorist(slot: Int) -> String {
if slot == 0 { return "akāsiṃ" }
if slot == 1 { return "akāsi" }
if slot == 2 { return "akāsi" }
if slot == 3 { return "akāsimhā" }
if slot == 4 { return "akāsittha" }
return "akāsiṃsu"
}
fn pi_karoti_future(slot: Int) -> String {
if slot == 0 { return "karissāmi" }
if slot == 1 { return "karissasi" }
if slot == 2 { return "karissati" }
if slot == 3 { return "karissāma" }
if slot == 4 { return "karissatha" }
return "karissanti"
}
// Canonical verb mapping
//
// Maps English semantic labels to Pali verb citation forms (3rd sg present).
fn pi_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "hoti" }
if str_eq(verb, "go") { return "gacchati" }
if str_eq(verb, "see") { return "passati" }
if str_eq(verb, "say") { return "vadati" }
if str_eq(verb, "do") { return "karoti" }
if str_eq(verb, "make") { return "karoti" }
return verb
}
// Regular verb stem derivation
//
// For bhū-ādi class verbs, the present stem is derived by stripping -ati from
// the 3sg present form (the citation form), then appending the appropriate ending.
// The same root (without -ati) is used for the aorist and future.
//
// e.g. "bhavati" -> root "bhava" -> present "bhavāmi", future "bhavissāmi"
// -> aorist root = "bhavi" -> "bhaviṃ"
fn pi_regular_root(verb: String) -> String {
// Strip -ati if present to get the thematic stem
if pi_str_ends(verb, "ati") { return pi_drop(verb, 3) }
// Some forms end in -eti (e-class verbs)
if pi_str_ends(verb, "eti") { return pi_drop(verb, 3) }
// Otherwise treat the whole form as the root
return verb
}
// pi_conjugate: main conjugation entry point
//
// verb: Pali 3sg present (citation form, e.g. "bhavati", "hoti") or English
// canonical label
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the citation form on unknown input.
fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = pi_map_canonical(verb)
let slot: Int = pi_slot(person, number)
// Irregulars
if str_eq(v, "hoti") {
if str_eq(tense, "present") { return pi_hoti_present(slot) }
if str_eq(tense, "past") { return pi_hoti_aorist(slot) }
if str_eq(tense, "future") { return pi_hoti_future(slot) }
return v
}
if str_eq(v, "atthi") {
if str_eq(tense, "present") { return pi_atthi_present(slot) }
if str_eq(tense, "past") { return pi_hoti_aorist(slot) }
if str_eq(tense, "future") { return pi_hoti_future(slot) }
return v
}
if str_eq(v, "gacchati") {
if str_eq(tense, "present") { return pi_gacchati_present(slot) }
if str_eq(tense, "past") { return pi_gacchati_aorist(slot) }
if str_eq(tense, "future") { return pi_gacchati_future(slot) }
return v
}
if str_eq(v, "passati") {
if str_eq(tense, "present") { return pi_passati_present(slot) }
if str_eq(tense, "past") { return pi_passati_aorist(slot) }
if str_eq(tense, "future") { return pi_passati_future(slot) }
return v
}
if str_eq(v, "vadati") {
if str_eq(tense, "present") { return pi_vadati_present(slot) }
if str_eq(tense, "past") { return pi_vadati_aorist(slot) }
if str_eq(tense, "future") { return pi_vadati_future(slot) }
return v
}
if str_eq(v, "karoti") {
if str_eq(tense, "present") { return pi_karoti_present(slot) }
if str_eq(tense, "past") { return pi_karoti_aorist(slot) }
if str_eq(tense, "future") { return pi_karoti_future(slot) }
return v
}
// Regular bhū-ādi class verb
let root: String = pi_regular_root(v)
if str_eq(tense, "present") {
return root + pi_present_ending(slot)
}
if str_eq(tense, "past") {
// Aorist: root + i-aorist ending
return root + pi_aorist_ending(slot)
}
if str_eq(tense, "future") {
return root + pi_future_ending(slot)
}
// Unknown tense: return citation form unchanged
return v
}
// a-stem masculine declension ("purisa" man)
//
// The a-stem masculine is the most frequent Pali declension class.
// Paradigm based on "puriso" (nominative singular).
//
// Eight cases: nominative, accusative, instrumental, dative, ablative,
// genitive, locative, vocative.
//
// Singular:
// nom puriso acc purisaṃ instr purisena dat purisāya (or purissā)
// abl purisā gen purisassa loc purisasmiṃ voc purisa
//
// Plural:
// nom purisā acc purise instr purisehi dat purisānaṃ
// abl purisānaṃ gen purisānaṃ loc purisesu voc purisā
//
// The caller passes the stem (without final vowel), e.g. "purisa".
// Detection: strip the final -o (nom sg) or use the stem directly.
fn pi_decline_a_masc_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "" }
if str_eq(gram_case, "instrumental") { return stem + "ena" }
if str_eq(gram_case, "dative") { return stem + "āya" }
if str_eq(gram_case, "ablative") { return stem + "ā" }
if str_eq(gram_case, "genitive") { return stem + "ssa" }
if str_eq(gram_case, "locative") { return stem + "smiṃ" }
if str_eq(gram_case, "vocative") { return stem }
// Default: nominative
return stem + "o"
}
fn pi_decline_a_masc_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ā" }
if str_eq(gram_case, "accusative") { return stem + "e" }
if str_eq(gram_case, "instrumental") { return stem + "ehi" }
if str_eq(gram_case, "dative") { return stem + "ānaṃ" }
if str_eq(gram_case, "ablative") { return stem + "ānaṃ" }
if str_eq(gram_case, "genitive") { return stem + "ānaṃ" }
if str_eq(gram_case, "locative") { return stem + "esu" }
if str_eq(gram_case, "vocative") { return stem + "ā" }
return stem + "ā"
}
// ā-stem feminine declension ("kaññā" girl)
//
// ā-stem feminines are the second most common class. Paradigm based on "kaññā".
//
// Singular:
// nom kaññā acc kaññaṃ instr kaññāya dat kaññāya
// abl kaññāya gen kaññāya loc kaññāyaṃ voc kaññe
//
// Plural (approximated from standard tables; many forms merge):
// nom kaññā acc kaññā instr kaññāhi dat kaññānaṃ
// abl kaññānaṃ gen kaññānaṃ loc kaññāsu voc kaññā
//
// The caller passes the stem without the final ā, e.g. "kaññ".
fn pi_decline_a_fem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ā" }
if str_eq(gram_case, "accusative") { return stem + "aṃ" }
if str_eq(gram_case, "instrumental") { return stem + "āya" }
if str_eq(gram_case, "dative") { return stem + "āya" }
if str_eq(gram_case, "ablative") { return stem + "āya" }
if str_eq(gram_case, "genitive") { return stem + "āya" }
if str_eq(gram_case, "locative") { return stem + "āyaṃ" }
if str_eq(gram_case, "vocative") { return stem + "e" }
return stem + "ā"
}
fn pi_decline_a_fem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ā" }
if str_eq(gram_case, "accusative") { return stem + "ā" }
if str_eq(gram_case, "instrumental") { return stem + "āhi" }
if str_eq(gram_case, "dative") { return stem + "ānaṃ" }
if str_eq(gram_case, "ablative") { return stem + "ānaṃ" }
if str_eq(gram_case, "genitive") { return stem + "ānaṃ" }
if str_eq(gram_case, "locative") { return stem + "āsu" }
if str_eq(gram_case, "vocative") { return stem + "ā" }
return stem + "ā"
}
// Declension class detection
//
// Detect whether a noun is an a-stem masculine or ā-stem feminine based on its
// citation form (nominative singular).
//
// ends in -o -> a-stem masculine (puriso, devo, loko)
// ends in -> ā-stem feminine (kaññā, mātu[?], nadī)
// ends in -a -> treat as a-stem masculine stem (some words cited without -o)
//
// For other stems the module falls back to a-stem masculine as the safest default.
fn pi_detect_class(noun: String) -> String {
if pi_str_ends(noun, "o") { return "a_masc" }
if pi_str_ends(noun, "ā") { return "a_fem" }
// Some a-masc nouns may be cited as stems ending in -a
if pi_str_ends(noun, "a") { return "a_masc" }
// Default to a-stem masculine
return "a_masc"
}
// pi_decline: main declension entry point
//
// noun: Pali nominative singular (citation form, e.g. "puriso", "kaññā")
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the citation form on unknown input.
fn pi_decline(noun: String, gram_case: String, number: String) -> String {
let nclass: String = pi_detect_class(noun)
if str_eq(nclass, "a_masc") {
// Derive stem: strip final -o or -a to get the consonant stem
let stem: String = noun
if pi_str_ends(noun, "o") {
let stem = pi_drop(noun, 1)
}
if pi_str_ends(noun, "a") {
let stem = pi_drop(noun, 1)
}
if str_eq(number, "singular") { return pi_decline_a_masc_sg(stem, gram_case) }
return pi_decline_a_masc_pl(stem, gram_case)
}
if str_eq(nclass, "a_fem") {
// Derive stem: strip final
let stem: String = noun
if pi_str_ends(noun, "ā") {
let stem = pi_drop(noun, 1)
}
if str_eq(number, "singular") { return pi_decline_a_fem_sg(stem, gram_case) }
return pi_decline_a_fem_pl(stem, gram_case)
}
// Unknown class: return citation form unchanged
return noun
}
// pi_noun_phrase: noun phrase builder
//
// Pali has no articles (definite or indefinite). The declined form is the
// complete noun phrase. The definite parameter is accepted for interface
// compatibility with other language modules but has no effect.
//
// noun: nominative singular Pali noun (e.g. "puriso", "kaññā")
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural"
// definite: ignored (Pali has no articles)
//
// Returns the inflected noun form.
fn pi_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return pi_decline(noun, gram_case, number)
}
+34
View File
@@ -0,0 +1,34 @@
// auto-generated by elc --emit-header — do not edit
extern fn pi_str_ends(s: String, suf: String) -> Bool
extern fn pi_drop(s: String, n: Int) -> String
extern fn pi_last_char(s: String) -> String
extern fn pi_slot(person: String, number: String) -> Int
extern fn pi_present_ending(slot: Int) -> String
extern fn pi_aorist_ending(slot: Int) -> String
extern fn pi_future_ending(slot: Int) -> String
extern fn pi_hoti_present(slot: Int) -> String
extern fn pi_atthi_present(slot: Int) -> String
extern fn pi_hoti_aorist(slot: Int) -> String
extern fn pi_hoti_future(slot: Int) -> String
extern fn pi_gacchati_present(slot: Int) -> String
extern fn pi_gacchati_aorist(slot: Int) -> String
extern fn pi_gacchati_future(slot: Int) -> String
extern fn pi_passati_present(slot: Int) -> String
extern fn pi_passati_aorist(slot: Int) -> String
extern fn pi_passati_future(slot: Int) -> String
extern fn pi_vadati_present(slot: Int) -> String
extern fn pi_vadati_aorist(slot: Int) -> String
extern fn pi_vadati_future(slot: Int) -> String
extern fn pi_karoti_present(slot: Int) -> String
extern fn pi_karoti_aorist(slot: Int) -> String
extern fn pi_karoti_future(slot: Int) -> String
extern fn pi_map_canonical(verb: String) -> String
extern fn pi_regular_root(verb: String) -> String
extern fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn pi_decline_a_masc_sg(stem: String, gram_case: String) -> String
extern fn pi_decline_a_masc_pl(stem: String, gram_case: String) -> String
extern fn pi_decline_a_fem_sg(stem: String, gram_case: String) -> String
extern fn pi_decline_a_fem_pl(stem: String, gram_case: String) -> String
extern fn pi_detect_class(noun: String) -> String
extern fn pi_decline(noun: String, gram_case: String, number: String) -> String
extern fn pi_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+921
View File
@@ -0,0 +1,921 @@
// morphology-ru.el - Russian morphology: noun declension and verb conjugation.
//
// Russian is a fusional language with:
// - 6 grammatical cases: nominative, accusative, genitive, dative,
// instrumental, prepositional
// - 3 genders: masculine (m), feminine (f), neuter (n)
// - 2 numbers: singular (sg), plural (pl)
// - Aspect: perfective / imperfective (caller feature)
// - No articles
// - Cyrillic script (El strings are Unicode-transparent)
//
// Conventions:
// gender: "m" | "f" | "n"
// case: "nom" | "acc" | "gen" | "dat" | "ins" | "pre"
// number: "sg" | "pl"
// person: "1" | "2" | "3"
// tense: "present" | "past" | "future"
//
// Stem-type classification:
// "hard" - stem ends in a hard consonant
// "soft" - stem ends in a soft consonant (+ ь) or a hushing consonant
// "special" - lexically irregular
//
// Depends on: language-profile (str_eq, str_len, str_slice, str_drop_last,
// str_ends_with)
// Gender heuristic from ending
//
// Russian gender is mostly predictable from the nominative singular ending:
// ends in consonant masculine (стол, дом)
// ends in masculine or feminine (must be lexical)
// ends in -а / feminine (женщина, земля)
// ends in -о / -е neuter (слово, море)
//
// The heuristic returns the most probable gender. Caller should override
// for known exceptions (путь, рубль are masc despite ).
import "morphology.el"
fn ru_gender(noun: String) -> String {
let n: Int = str_len(noun)
if n == 0 { return "m" }
let last: String = str_slice(noun, n - 1, n)
// Neuter: -о or -е
if str_eq(last, "о") { return "n" }
if str_eq(last, "е") { return "n" }
if str_eq(last, "ё") { return "n" }
// Feminine: -а or
if str_eq(last, "а") { return "f" }
if str_eq(last, "я") { return "f" }
// Soft sign: ambiguous default feminine (most common case)
if str_eq(last, "ь") { return "f" }
// Consonant or й: masculine
return "m"
}
// Stem-type classifier
//
// "hard" ends in a plain consonant (стол) or -а (женщина) or -о (слово)
// "soft" ends in -ь, -й, -я, -е, -ие, -ия
// "sibilant" ends in ж/ш/ч/щ (triggers spelling rules)
fn ru_stem_type(noun: String, gender: String) -> String {
let n: Int = str_len(noun)
if n == 0 { return "hard" }
let last: String = str_slice(noun, n - 1, n)
if str_eq(last, "ь") { return "soft" }
if str_eq(last, "й") { return "soft" }
if str_eq(last, "я") { return "soft" }
if str_eq(last, "е") { return "soft" }
// Sibilants (hushing consonants: ж ш ч щ) use sibilant rules
if str_eq(last, "ж") { return "sibilant" }
if str_eq(last, "ш") { return "sibilant" }
if str_eq(last, "ч") { return "sibilant" }
if str_eq(last, "щ") { return "sibilant" }
return "hard"
}
// Noun declension
//
// Main entry point. Handles irregulars by lemma, then dispatches to the
// paradigm appropriate for (gender × stem_type).
fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String {
// Lexical irregulars
// человек (person) plural suppletive: люди
if str_eq(noun, "человек") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "человек" }
if str_eq(gram_case, "acc") { return "человека" }
if str_eq(gram_case, "gen") { return "человека" }
if str_eq(gram_case, "dat") { return "человеку" }
if str_eq(gram_case, "ins") { return "человеком" }
if str_eq(gram_case, "pre") { return "человеке" }
}
// plural: люди paradigm
if str_eq(gram_case, "nom") { return "люди" }
if str_eq(gram_case, "acc") { return "людей" }
if str_eq(gram_case, "gen") { return "людей" }
if str_eq(gram_case, "dat") { return "людям" }
if str_eq(gram_case, "ins") { return "людьми" }
if str_eq(gram_case, "pre") { return "людях" }
return "люди"
}
// ребёнок (child) plural suppletive: дети
if str_eq(noun, "ребёнок") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "ребёнок" }
if str_eq(gram_case, "acc") { return "ребёнка" }
if str_eq(gram_case, "gen") { return "ребёнка" }
if str_eq(gram_case, "dat") { return "ребёнку" }
if str_eq(gram_case, "ins") { return "ребёнком" }
if str_eq(gram_case, "pre") { return "ребёнке" }
}
if str_eq(gram_case, "nom") { return "дети" }
if str_eq(gram_case, "acc") { return "детей" }
if str_eq(gram_case, "gen") { return "детей" }
if str_eq(gram_case, "dat") { return "детям" }
if str_eq(gram_case, "ins") { return "детьми" }
if str_eq(gram_case, "pre") { return "детях" }
return "дети"
}
// время (time) neuter in -мя (heteroclitic declension)
if str_eq(noun, "время") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "время" }
if str_eq(gram_case, "acc") { return "время" }
if str_eq(gram_case, "gen") { return "времени" }
if str_eq(gram_case, "dat") { return "времени" }
if str_eq(gram_case, "ins") { return "временем" }
if str_eq(gram_case, "pre") { return "времени" }
}
if str_eq(gram_case, "nom") { return "времена" }
if str_eq(gram_case, "acc") { return "времена" }
if str_eq(gram_case, "gen") { return "времён" }
if str_eq(gram_case, "dat") { return "временам" }
if str_eq(gram_case, "ins") { return "временами" }
if str_eq(gram_case, "pre") { return "временах" }
return "времена"
}
// имя (name) neuter in -мя
if str_eq(noun, "имя") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "имя" }
if str_eq(gram_case, "acc") { return "имя" }
if str_eq(gram_case, "gen") { return "имени" }
if str_eq(gram_case, "dat") { return "имени" }
if str_eq(gram_case, "ins") { return "именем" }
if str_eq(gram_case, "pre") { return "имени" }
}
if str_eq(gram_case, "nom") { return "имена" }
if str_eq(gram_case, "acc") { return "имена" }
if str_eq(gram_case, "gen") { return "имён" }
if str_eq(gram_case, "dat") { return "именам" }
if str_eq(gram_case, "ins") { return "именами" }
if str_eq(gram_case, "pre") { return "именах" }
return "имена"
}
// путь (way/path) masculine soft with unique instrumental
if str_eq(noun, "путь") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "путь" }
if str_eq(gram_case, "acc") { return "путь" }
if str_eq(gram_case, "gen") { return "пути" }
if str_eq(gram_case, "dat") { return "пути" }
if str_eq(gram_case, "ins") { return "путём" }
if str_eq(gram_case, "pre") { return "пути" }
}
if str_eq(gram_case, "nom") { return "пути" }
if str_eq(gram_case, "acc") { return "пути" }
if str_eq(gram_case, "gen") { return "путей" }
if str_eq(gram_case, "dat") { return "путям" }
if str_eq(gram_case, "ins") { return "путями" }
if str_eq(gram_case, "pre") { return "путях" }
return "пути"
}
// мать (mother) feminine with -ер- insert
if str_eq(noun, "мать") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "мать" }
if str_eq(gram_case, "acc") { return "мать" }
if str_eq(gram_case, "gen") { return "матери" }
if str_eq(gram_case, "dat") { return "матери" }
if str_eq(gram_case, "ins") { return "матерью" }
if str_eq(gram_case, "pre") { return "матери" }
}
if str_eq(gram_case, "nom") { return "матери" }
if str_eq(gram_case, "acc") { return "матерей" }
if str_eq(gram_case, "gen") { return "матерей" }
if str_eq(gram_case, "dat") { return "матерям" }
if str_eq(gram_case, "ins") { return "матерями" }
if str_eq(gram_case, "pre") { return "матерях" }
return "матери"
}
// дочь (daughter) same insert pattern
if str_eq(noun, "дочь") {
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return "дочь" }
if str_eq(gram_case, "acc") { return "дочь" }
if str_eq(gram_case, "gen") { return "дочери" }
if str_eq(gram_case, "dat") { return "дочери" }
if str_eq(gram_case, "ins") { return "дочерью" }
if str_eq(gram_case, "pre") { return "дочери" }
}
if str_eq(gram_case, "nom") { return "дочери" }
if str_eq(gram_case, "acc") { return "дочерей" }
if str_eq(gram_case, "gen") { return "дочерей" }
if str_eq(gram_case, "dat") { return "дочерям" }
if str_eq(gram_case, "ins") { return "дочерями" }
if str_eq(gram_case, "pre") { return "дочерях" }
return "дочери"
}
// Regular paradigm dispatch
let stype: String = ru_stem_type(noun, gender)
return ru_decline_regular(noun, gender, stype, gram_case, number)
}
// Regular declension paradigms
//
// Handles hard, soft, and sibilant stems across all three genders.
fn ru_decline_regular(noun: String, gender: String, stype: String, gram_case: String, number: String) -> String {
if str_eq(gender, "m") {
return ru_decline_masc(noun, stype, gram_case, number)
}
if str_eq(gender, "f") {
return ru_decline_fem(noun, stype, gram_case, number)
}
return ru_decline_neut(noun, stype, gram_case, number)
}
// Masculine declension
//
// Hard (стол): stem = noun itself (zero ending in nom sg)
// Sg: стол, стол, стола, столу, столом, столе
// Pl: столы, столы, столов, столам, столами, столах
//
// Soft ending in (трамвай трамва-):
// Sg: трамвай, трамвай, трамвая, трамваю, трамваем, трамвае
// Pl: трамваи, трамваи, трамваев, трамваям, трамваями, трамваях
//
// Soft ending in (рубль рубл-):
// Sg: рубль, рубль, рубля, рублю, рублём, рубле
// Pl: рубли, рубли, рублей, рублям, рублями, рублях
//
// Sibilant (нож нож-):
// Same as hard but ins sg = -ом only when stressed; unstressed = -ем
// Here we assume stressed stem and use -ом.
fn ru_decline_masc(noun: String, stype: String, gram_case: String, number: String) -> String {
let n: Int = str_len(noun)
if str_eq(stype, "soft") {
let last: String = str_slice(noun, n - 1, n)
// Stem in (трамвай)
if str_eq(last, "й") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun } // inanimate
if str_eq(gram_case, "gen") { return stem + "я" }
if str_eq(gram_case, "dat") { return stem + "ю" }
if str_eq(gram_case, "ins") { return stem + "ем" }
if str_eq(gram_case, "pre") { return stem + "е" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "и" }
if str_eq(gram_case, "acc") { return stem + "и" }
if str_eq(gram_case, "gen") { return stem + "ев" }
if str_eq(gram_case, "dat") { return stem + "ям" }
if str_eq(gram_case, "ins") { return stem + "ями" }
if str_eq(gram_case, "pre") { return stem + "ях" }
return stem + "и"
}
// Stem in (рубль, гость)
if str_eq(last, "ь") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun }
if str_eq(gram_case, "gen") { return stem + "я" }
if str_eq(gram_case, "dat") { return stem + "ю" }
if str_eq(gram_case, "ins") { return stem + "ём" }
if str_eq(gram_case, "pre") { return stem + "е" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "и" }
if str_eq(gram_case, "acc") { return stem + "и" }
if str_eq(gram_case, "gen") { return stem + "ей" }
if str_eq(gram_case, "dat") { return stem + "ям" }
if str_eq(gram_case, "ins") { return stem + "ями" }
if str_eq(gram_case, "pre") { return stem + "ях" }
return stem + "и"
}
}
// Hard and sibilant: stem = noun (zero ending in nom sg)
let stem: String = noun
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return stem }
if str_eq(gram_case, "acc") { return stem } // inanimate; animate = gen
if str_eq(gram_case, "gen") { return stem + "а" }
if str_eq(gram_case, "dat") { return stem + "у" }
if str_eq(gram_case, "ins") { return stem + "ом" }
if str_eq(gram_case, "pre") { return stem + "е" }
return stem
}
// Plural
if str_eq(gram_case, "nom") { return stem + "ы" }
if str_eq(gram_case, "acc") { return stem + "ы" }
if str_eq(gram_case, "gen") { return stem + "ов" }
if str_eq(gram_case, "dat") { return stem + "ам" }
if str_eq(gram_case, "ins") { return stem + "ами" }
if str_eq(gram_case, "pre") { return stem + "ах" }
return stem + "ы"
}
// Feminine declension
//
// Hard in -а (женщина):
// Sg: женщина, женщину, женщины, женщине, женщиной, женщине
// Pl: женщины, женщин, женщин, женщинам, женщинами, женщинах
//
// Soft in (земля):
// Sg: земля, землю, земли, земле, землёй, земле
// Pl: земли, земли, земель, землям, землями, землях
//
// Soft in (тетрадь, ночь):
// Sg: тетрадь, тетрадь, тетради, тетради, тетрадью, тетради
// Pl: тетради, тетради, тетрадей, тетрадям, тетрадями, тетрадях
fn ru_decline_fem(noun: String, stype: String, gram_case: String, number: String) -> String {
let n: Int = str_len(noun)
let last: String = str_slice(noun, n - 1, n)
// Soft in
if str_eq(last, "ь") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun }
if str_eq(gram_case, "gen") { return stem + "и" }
if str_eq(gram_case, "dat") { return stem + "и" }
if str_eq(gram_case, "ins") { return stem + "ью" }
if str_eq(gram_case, "pre") { return stem + "и" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "и" }
if str_eq(gram_case, "acc") { return stem + "и" }
if str_eq(gram_case, "gen") { return stem + "ей" }
if str_eq(gram_case, "dat") { return stem + "ям" }
if str_eq(gram_case, "ins") { return stem + "ями" }
if str_eq(gram_case, "pre") { return stem + "ях" }
return stem + "и"
}
// Soft in (земля, семья)
if str_eq(last, "я") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return stem + "ю" }
if str_eq(gram_case, "gen") { return stem + "и" }
if str_eq(gram_case, "dat") { return stem + "е" }
if str_eq(gram_case, "ins") { return stem + "ей" }
if str_eq(gram_case, "pre") { return stem + "е" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "и" }
if str_eq(gram_case, "acc") { return stem + "и" }
// Genitive plural: zero ending (with possible fleeting vowel) use -ей for soft
if str_eq(gram_case, "gen") { return stem + "ей" }
if str_eq(gram_case, "dat") { return stem + "ям" }
if str_eq(gram_case, "ins") { return stem + "ями" }
if str_eq(gram_case, "pre") { return stem + "ях" }
return stem + "и"
}
// Hard in -а (женщина, страна, рука)
if str_eq(last, "а") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return stem + "у" }
if str_eq(gram_case, "gen") { return stem + "ы" }
if str_eq(gram_case, "dat") { return stem + "е" }
if str_eq(gram_case, "ins") { return stem + "ой" }
if str_eq(gram_case, "pre") { return stem + "е" }
return noun
}
// Plural: gen pl = zero ending (stem only) for most -а feminines
if str_eq(gram_case, "nom") { return stem + "ы" }
if str_eq(gram_case, "acc") { return stem + "ы" }
if str_eq(gram_case, "gen") { return stem }
if str_eq(gram_case, "dat") { return stem + "ам" }
if str_eq(gram_case, "ins") { return stem + "ами" }
if str_eq(gram_case, "pre") { return stem + "ах" }
return stem + "ы"
}
// Fallback: treat as hard -а (unexpected ending)
return noun
}
// Neuter declension
//
// Hard in -о (слово):
// Sg: слово, слово, слова, слову, словом, слове
// Pl: слова, слова, слов, словам, словами, словах
//
// Soft in -е (море, поле):
// Sg: море, море, моря, морю, морем, море
// Pl: моря, моря, морей, морям, морями, морях
//
// Soft in -ие (здание):
// Sg: здание, здание, здания, зданию, зданием, здании
// Pl: здания, здания, зданий, зданиям, зданиями, зданиях
fn ru_decline_neut(noun: String, stype: String, gram_case: String, number: String) -> String {
let n: Int = str_len(noun)
let last: String = str_slice(noun, n - 1, n)
// Nouns in -ие (здание, здравоохранение)
if str_ends_with(noun, "ие") {
let stem: String = str_drop_last(noun, 2)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun }
if str_eq(gram_case, "gen") { return stem + "ия" }
if str_eq(gram_case, "dat") { return stem + "ию" }
if str_eq(gram_case, "ins") { return stem + "ием" }
if str_eq(gram_case, "pre") { return stem + "ии" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "ия" }
if str_eq(gram_case, "acc") { return stem + "ия" }
if str_eq(gram_case, "gen") { return stem + "ий" }
if str_eq(gram_case, "dat") { return stem + "иям" }
if str_eq(gram_case, "ins") { return stem + "иями" }
if str_eq(gram_case, "pre") { return stem + "иях" }
return stem + "ия"
}
// Soft in -е (море, поле)
if str_eq(last, "е") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun }
if str_eq(gram_case, "gen") { return stem + "я" }
if str_eq(gram_case, "dat") { return stem + "ю" }
if str_eq(gram_case, "ins") { return stem + "ем" }
if str_eq(gram_case, "pre") { return noun }
return noun
}
if str_eq(gram_case, "nom") { return stem + "я" }
if str_eq(gram_case, "acc") { return stem + "я" }
if str_eq(gram_case, "gen") { return stem + "ей" }
if str_eq(gram_case, "dat") { return stem + "ям" }
if str_eq(gram_case, "ins") { return stem + "ями" }
if str_eq(gram_case, "pre") { return stem + "ях" }
return stem + "я"
}
// Hard in -о (слово, окно, место)
if str_eq(last, "о") {
let stem: String = str_drop_last(noun, 1)
if str_eq(number, "sg") {
if str_eq(gram_case, "nom") { return noun }
if str_eq(gram_case, "acc") { return noun }
if str_eq(gram_case, "gen") { return stem + "а" }
if str_eq(gram_case, "dat") { return stem + "у" }
if str_eq(gram_case, "ins") { return stem + "ом" }
if str_eq(gram_case, "pre") { return stem + "е" }
return noun
}
if str_eq(gram_case, "nom") { return stem + "а" }
if str_eq(gram_case, "acc") { return stem + "а" }
// Genitive plural: zero ending (слов, окон, мест)
if str_eq(gram_case, "gen") { return stem }
if str_eq(gram_case, "dat") { return stem + "ам" }
if str_eq(gram_case, "ins") { return stem + "ами" }
if str_eq(gram_case, "pre") { return stem + "ах" }
return stem + "а"
}
// Fallback
return noun
}
// Past-tense agreement
//
// Russian past tense agrees with the subject in gender and number.
// The past stem is derived from the infinitive (drop -ть/-ти/-чь).
//
// Endings: masc (bare stem), fem -а, neut -о, pl
//
// Special: verbs in -чь (мочь мог), -ти (идти шёл) are irregular.
fn ru_past_agree(verb_stem: String, gender: String, number: String) -> String {
if str_eq(number, "pl") {
return verb_stem + "и"
}
if str_eq(gender, "f") { return verb_stem + "а" }
if str_eq(gender, "n") { return verb_stem + "о" }
return verb_stem
}
// First-conjugation present tense
//
// Model: читать (чита-) читаю, читаешь, читает, читаем, читаете, читают
// Model: писать (пиш-) stem changes in conjugation (handled by caller)
//
// This function takes an already-computed present stem (e.g. "чита") and
// applies first-conjugation endings.
//
// 1sg: stem + (after vowel) or -у (after consonant)
// 2sg: stem + -ешь
// 3sg: stem + -ет
// 1pl: stem + -ем
// 2pl: stem + -ете
// 3pl: stem + -ют (after vowel) or -ут (after consonant)
fn ru_conjugate_1st(stem: String, tense: String, person: String, number: String) -> String {
if str_eq(tense, "present") {
let n: Int = str_len(stem)
let last: String = str_slice(stem, n - 1, n)
let vowels: Bool = false
let vowels =
str_eq(last, "а") ||
str_eq(last, "е") ||
str_eq(last, "и") ||
str_eq(last, "о") ||
str_eq(last, "у") ||
str_eq(last, "ю") ||
str_eq(last, "я") ||
str_eq(last, "э") ||
str_eq(last, "ё") ||
str_eq(last, "ы")
if str_eq(number, "sg") {
if str_eq(person, "1") {
if vowels { return stem + "ю" }
return stem + "у"
}
if str_eq(person, "2") { return stem + "ешь" }
return stem + "ет"
}
if str_eq(person, "1") { return stem + "ем" }
if str_eq(person, "2") { return stem + "ете" }
if vowels { return stem + "ют" }
return stem + "ут"
}
// Past and future handled by ru_conjugate
return stem
}
// Second-conjugation present tense
//
// Model: говорить (говор-) говорю, говоришь, говорит, говорим, говорите, говорят
//
// 1sg: stem + / -у
// 2sg: stem + -ишь
// 3sg: stem + -ит
// 1pl: stem + -им
// 2pl: stem + -ите
// 3pl: stem + -ят / -ат
fn ru_conjugate_2nd(stem: String, tense: String, person: String, number: String) -> String {
if str_eq(tense, "present") {
let n: Int = str_len(stem)
let last: String = str_slice(stem, n - 1, n)
let after_vowel: Bool =
str_eq(last, "а") ||
str_eq(last, "е") ||
str_eq(last, "и") ||
str_eq(last, "о") ||
str_eq(last, "у") ||
str_eq(last, "ю") ||
str_eq(last, "я") ||
str_eq(last, "э") ||
str_eq(last, "ё") ||
str_eq(last, "ы")
if str_eq(number, "sg") {
if str_eq(person, "1") {
if after_vowel { return stem + "ю" }
return stem + "у"
}
if str_eq(person, "2") { return stem + "ишь" }
return stem + "ит"
}
if str_eq(person, "1") { return stem + "им" }
if str_eq(person, "2") { return stem + "ите" }
if after_vowel { return stem + "ят" }
return stem + "ат"
}
return stem
}
// Irregular verb forms
//
// Returns the inflected form for known irregular verbs, or "" if unknown.
// gender is used for past tense agreement.
fn ru_irregular(verb: String, tense: String, person: String, number: String) -> String {
// быть (to be) present tense: есть (invariant) / rarely conjugated
if str_eq(verb, "быть") {
if str_eq(tense, "present") {
// Modern Russian uses invariant "есть" for all persons in present
return "есть"
}
// Future: буду, будешь, будет, будем, будете, будут
if str_eq(tense, "future") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "буду" }
if str_eq(person, "2") { return "будешь" }
return "будет"
}
if str_eq(person, "1") { return "будем" }
if str_eq(person, "2") { return "будете" }
return "будут"
}
// Past: returned without gender agreement caller must call ru_past_agree
return ""
}
// идти (to go on foot) present: иду, идёшь, идёт, идём, идёте, идут
if str_eq(verb, "идти") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "иду" }
if str_eq(person, "2") { return "идёшь" }
return "идёт"
}
if str_eq(person, "1") { return "идём" }
if str_eq(person, "2") { return "идёте" }
return "идут"
}
// Past stem: шёл (m), шла (f), шло (n), шли (pl)
return ""
}
// ехать (to go by vehicle) present: еду, едешь, едет, едем, едете, едут
if str_eq(verb, "ехать") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "еду" }
if str_eq(person, "2") { return "едешь" }
return "едет"
}
if str_eq(person, "1") { return "едем" }
if str_eq(person, "2") { return "едете" }
return "едут"
}
return ""
}
// говорить (to speak) 2nd conjugation
if str_eq(verb, "говорить") {
if str_eq(tense, "present") {
return ru_conjugate_2nd("говор", "present", person, number)
}
return ""
}
// знать (to know) 1st conjugation, regular: зна-
if str_eq(verb, "знать") {
if str_eq(tense, "present") {
return ru_conjugate_1st("зна", "present", person, number)
}
return ""
}
// видеть (to see) 2nd conjugation, 1sg mutation: вижу
if str_eq(verb, "видеть") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "вижу" }
if str_eq(person, "2") { return "видишь" }
return "видит"
}
if str_eq(person, "1") { return "видим" }
if str_eq(person, "2") { return "видите" }
return "видят"
}
return ""
}
// делать (to do) 1st conjugation: дела-
if str_eq(verb, "делать") {
if str_eq(tense, "present") {
return ru_conjugate_1st("дела", "present", person, number)
}
return ""
}
// хотеть (to want) mixed conjugation (1st pl, 2nd sg/3sg)
if str_eq(verb, "хотеть") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "хочу" }
if str_eq(person, "2") { return "хочешь" }
return "хочет"
}
if str_eq(person, "1") { return "хотим" }
if str_eq(person, "2") { return "хотите" }
return "хотят"
}
return ""
}
// мочь (can/be able) present: могу, можешь, может, можем, можете, могут
if str_eq(verb, "мочь") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "могу" }
if str_eq(person, "2") { return "можешь" }
return "может"
}
if str_eq(person, "1") { return "можем" }
if str_eq(person, "2") { return "можете" }
return "могут"
}
return ""
}
// сказать (to say, pfv) present (= future perfective):
// скажу, скажешь, скажет, скажем, скажете, скажут
if str_eq(verb, "сказать") {
if str_eq(tense, "present") {
if str_eq(number, "sg") {
if str_eq(person, "1") { return "скажу" }
if str_eq(person, "2") { return "скажешь" }
return "скажет"
}
if str_eq(person, "1") { return "скажем" }
if str_eq(person, "2") { return "скажете" }
return "скажут"
}
return ""
}
return ""
}
// Past stem table
//
// Returns the masculine past stem (without agreement ending) for known verbs.
// The caller must run ru_past_agree(stem, gender, number) to get the full form.
fn ru_past_stem(verb: String) -> String {
// Regular -ать: drop -ть, add nothing (past stem = infinitive minus -ть)
if str_eq(verb, "читать") { return "чита" }
if str_eq(verb, "знать") { return "зна" }
if str_eq(verb, "делать") { return "дела" }
if str_eq(verb, "сказать") { return "сказа" }
if str_eq(verb, "думать") { return "дума" }
if str_eq(verb, "работать") { return "работа" }
if str_eq(verb, "писать") { return "писа" }
if str_eq(verb, "слушать") { return "слуша" }
if str_eq(verb, "отвечать") { return "отвеча" }
// Regular -ить/-еть
if str_eq(verb, "говорить") { return "говори" }
if str_eq(verb, "видеть") { return "виде" }
if str_eq(verb, "смотреть") { return "смотре" }
if str_eq(verb, "иметь") { return "име" }
if str_eq(verb, "хотеть") { return "хоте" }
// Irregular pasts
if str_eq(verb, "быть") { return "бы" } // был/была/было/были
if str_eq(verb, "идти") { return "шё" } // шёл caller must handle suffix
if str_eq(verb, "ехать") { return "еха" } // ехал
if str_eq(verb, "мочь") { return "мо" } // мог (m: мог, not мол)
if str_eq(verb, "нести") { return "нё" } // нёс
if str_eq(verb, "вести") { return "вё" } // вёл
// Derive from infinitive: drop -ть
let n: Int = str_len(verb)
if n > 2 {
let last2: String = str_slice(verb, n - 2, n)
if str_eq(last2, "ть") {
return str_drop_last(verb, 2)
}
}
return verb
}
// Unified Russian verb conjugation
//
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3"
// number: "sg" | "pl"
// gender: "m" | "f" | "n" used for past tense agreement only
fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String {
// Copula "byt" (transliterated быть)
// Russian present tense has zero copula "Кошка большая" not "Кошка есть большая".
// Return empty string so gram_order_constituents skips the verb slot.
if str_eq(verb, "byt") {
if str_eq(tense, "present") { return "" }
// Future: буду/будешь/будет... Past: был/была/было/были
// For now return transliterated placeholder; full Unicode tables TBD.
if str_eq(tense, "future") { return "budet" }
return "byl"
}
// Past tense
if str_eq(tense, "past") {
// Special-case verbs with non-standard past forms
if str_eq(verb, "идти") {
if str_eq(number, "pl") { return "шли" }
if str_eq(gender, "f") { return "шла" }
if str_eq(gender, "n") { return "шло" }
return "шёл"
}
if str_eq(verb, "мочь") {
if str_eq(number, "pl") { return "могли" }
if str_eq(gender, "f") { return "могла" }
if str_eq(gender, "n") { return "могло" }
return "мог"
}
if str_eq(verb, "нести") {
if str_eq(number, "pl") { return "несли" }
if str_eq(gender, "f") { return "несла" }
if str_eq(gender, "n") { return "несло" }
return "нёс"
}
if str_eq(verb, "вести") {
if str_eq(number, "pl") { return "вели" }
if str_eq(gender, "f") { return "вела" }
if str_eq(gender, "n") { return "вело" }
return "вёл"
}
let ps: String = ru_past_stem(verb)
return ru_past_agree(ps, gender, number)
}
// Future tense (imperfective)
//
// Imperfective future = быть (conjugated) + infinitive
if str_eq(tense, "future") {
let aux: String = ru_irregular("быть", "future", person, number)
return aux + " " + verb
}
// Present tense
// Try the irregular table first
let irr: String = ru_irregular(verb, tense, person, number)
if !str_eq(irr, "") {
return irr
}
// Derive conjugation class from infinitive ending
let n: Int = str_len(verb)
if n > 4 {
let last4: String = str_slice(verb, n - 4, n)
// -ить verbs 2nd conjugation (most)
if str_eq(last4, "ить ") {
// remove trailing space if any defensive
}
}
// Detect -ить suffix 2nd conjugation
if str_ends_with(verb, "ить") {
let stem: String = str_drop_last(verb, 3)
return ru_conjugate_2nd(stem, "present", person, number)
}
// Detect -еть suffix 2nd conjugation
if str_ends_with(verb, "еть") {
let stem: String = str_drop_last(verb, 3)
return ru_conjugate_2nd(stem, "present", person, number)
}
// Detect -ать suffix 1st conjugation
if str_ends_with(verb, "ать") {
let stem: String = str_drop_last(verb, 2)
return ru_conjugate_1st(stem, "present", person, number)
}
// Detect -ять suffix 1st conjugation
if str_ends_with(verb, "ять") {
let stem: String = str_drop_last(verb, 2)
return ru_conjugate_1st(stem, "present", person, number)
}
// Detect -овать suffix 1st conjugation with -у- stem
if str_ends_with(verb, "овать") {
let stem: String = str_drop_last(verb, 5) + "у"
return ru_conjugate_1st(stem, "present", person, number)
}
// Detect -нуть suffix 1st conjugation
if str_ends_with(verb, "нуть") {
let stem: String = str_drop_last(verb, 4) + "н"
return ru_conjugate_1st(stem, "present", person, number)
}
// Fallback: return the infinitive
return verb
}
+14
View File
@@ -0,0 +1,14 @@
// auto-generated by elc --emit-header — do not edit
extern fn ru_gender(noun: String) -> String
extern fn ru_stem_type(noun: String, gender: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
extern fn ru_decline_regular(noun: String, gender: String, stype: String, gram_case: String, number: String) -> String
extern fn ru_decline_masc(noun: String, stype: String, gram_case: String, number: String) -> String
extern fn ru_decline_fem(noun: String, stype: String, gram_case: String, number: String) -> String
extern fn ru_decline_neut(noun: String, stype: String, gram_case: String, number: String) -> String
extern fn ru_past_agree(verb_stem: String, gender: String, number: String) -> String
extern fn ru_conjugate_1st(stem: String, tense: String, person: String, number: String) -> String
extern fn ru_conjugate_2nd(stem: String, tense: String, person: String, number: String) -> String
extern fn ru_irregular(verb: String, tense: String, person: String, number: String) -> String
extern fn ru_past_stem(verb: String) -> String
extern fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String
+652
View File
@@ -0,0 +1,652 @@
// morphology-sa.el - Sanskrit morphology for the NLG engine.
//
// Implements Sanskrit verb conjugation and noun declension using IAST
// transliteration as the primary form. Designed as a companion to
// morphology.el and called by the engine when the language profile code
// is "sa".
//
// Language profile: code=sa, name=Sanskrit, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=devanagari,
// family=indo-aryan.
//
// Verb conjugation covered:
// Tenses: present (laṭ), past (imperfect laṅ), future (lṛṭ)
// Persons: first/second/third × singular/plural
// (dual is treated as plural throughout see sa_slot)
// Classes: Class 1 (bhū-adi, stem + a + endings) as the regular path
// Irregulars: as, bhū, gam, dṛś, vad, kṛ (the core NLG vocabulary)
// Canonical map: "be" -> "as"
//
// Noun declension covered:
// Cases: nominative, accusative, instrumental, dative, ablative,
// genitive, locative, vocative (all 8 Sanskrit cases)
// Stem types: a-stem masculine (paradigm: deva),
// ā-stem feminine (paradigm: devī)
// Numbers: singular, plural (dual collapsed to plural)
//
// Sanskrit has no articles. sa_noun_phrase returns the declined noun
// directly.
//
// Notes on IAST diacritics used throughout this file:
// ā ī ū long vowels
// vocalic r
// anusvāra (nasalisation / homorganic nasal)
// visarga (final breath)
// ś palatal / retroflex sibilants
// retroflex stops and nasal
// ñ palatal nasal
// Sandhi is intentionally suppressed forms are returned in their
// isolated (pausa) shapes so the NLG realizer can apply sandhi later.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn sa_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn sa_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Maps person × number to a 0-based index into paradigm arrays.
//
// 0 = 1st singular (uttama eka)
// 1 = 2nd singular (madhyama eka)
// 2 = 3rd singular (prathama eka)
// 3 = 1st plural (uttama bahu)
// 4 = 2nd plural (madhyama bahu)
// 5 = 3rd plural (prathama bahu)
//
// Sanskrit has a dual number but for NLG simplicity the dual is collapsed:
// "dual" inputs return the same slot as "plural". Forms in this file
// therefore carry plural endings even when the dual was requested.
fn sa_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third person
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// Semantic-layer canonical English labels are mapped to IAST dictionary
// entries before conjugation. The dictionary entry is then looked up in
// the irregular table; unknown entries fall through to the Class-1 path.
fn sa_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "as" }
if str_eq(verb, "become") { return "bhu" }
if str_eq(verb, "go") { return "gam" }
if str_eq(verb, "see") { return "drs" }
if str_eq(verb, "speak") { return "vad" }
if str_eq(verb, "say") { return "vad" }
if str_eq(verb, "do") { return "kr" }
if str_eq(verb, "make") { return "kr" }
return verb
}
// Irregular verb: as (to be)
//
// Present (laṭ) parasmaipada:
// 1sg asmi 2sg asi 3sg asti
// 1pl smaḥ 2pl stha 3pl santi
//
// Imperfect (laṅ) parasmaipada:
// 1sg āsam 2sg āsīḥ 3sg āsīt
// 1pl āsma 2pl āsta 3pl āsan
fn sa_as_present(slot: Int) -> String {
if slot == 0 { return "asmi" }
if slot == 1 { return "asi" }
if slot == 2 { return "asti" }
if slot == 3 { return "smaḥ" }
if slot == 4 { return "stha" }
return "santi"
}
fn sa_as_past(slot: Int) -> String {
if slot == 0 { return "āsam" }
if slot == 1 { return "āsīḥ" }
if slot == 2 { return "āsīt" }
if slot == 3 { return "āsma" }
if slot == 4 { return "āsta" }
return "āsan"
}
// Future (lṛṭ) of as: bhaviṣyāmi series (uses bhū as suppletive stem)
fn sa_as_future(slot: Int) -> String {
if slot == 0 { return "bhaviṣyāmi" }
if slot == 1 { return "bhaviṣyasi" }
if slot == 2 { return "bhaviṣyati" }
if slot == 3 { return "bhaviṣyāmaḥ" }
if slot == 4 { return "bhaviṣyatha" }
return "bhaviṣyanti"
}
// Irregular verb: bhū (to be, to become)
//
// Class 1; present stem bho → bhava (guṇa of u before -a-).
//
// Present (laṭ):
// 1sg bhavāmi 2sg bhavasi 3sg bhavati
// 1pl bhavāmaḥ 2pl bhavatha 3pl bhavanti
//
// Imperfect (laṅ):
// 1sg abhavam 2sg abhavaḥ 3sg abhavat
// 1pl abhavāma 2pl abhavata 3pl abhavan
//
// Future (lṛṭ): regular from bhaviṣya-
fn sa_bhu_present(slot: Int) -> String {
if slot == 0 { return "bhavāmi" }
if slot == 1 { return "bhavasi" }
if slot == 2 { return "bhavati" }
if slot == 3 { return "bhavāmaḥ" }
if slot == 4 { return "bhavatha" }
return "bhavanti"
}
fn sa_bhu_past(slot: Int) -> String {
if slot == 0 { return "abhavam" }
if slot == 1 { return "abhavaḥ" }
if slot == 2 { return "abhavat" }
if slot == 3 { return "abhavāma" }
if slot == 4 { return "abhavata" }
return "abhavan"
}
fn sa_bhu_future(slot: Int) -> String {
if slot == 0 { return "bhaviṣyāmi" }
if slot == 1 { return "bhaviṣyasi" }
if slot == 2 { return "bhaviṣyati" }
if slot == 3 { return "bhaviṣyāmaḥ" }
if slot == 4 { return "bhaviṣyatha" }
return "bhaviṣyanti"
}
// Irregular verb: gam (to go)
//
// Historically Class 1 with the present stem gaccha- (inserted -ccha-).
//
// Present (laṭ):
// gacchāmi gacchasi gacchati
// gacchāmaḥ gacchatha gacchanti
//
// Imperfect (laṅ): augmented agaccha-
//
// Future: gamiṣyati series
fn sa_gam_present(slot: Int) -> String {
if slot == 0 { return "gacchāmi" }
if slot == 1 { return "gacchasi" }
if slot == 2 { return "gacchati" }
if slot == 3 { return "gacchāmaḥ" }
if slot == 4 { return "gacchatha" }
return "gacchanti"
}
fn sa_gam_past(slot: Int) -> String {
if slot == 0 { return "agaccham" }
if slot == 1 { return "agacchaḥ" }
if slot == 2 { return "agacchat" }
if slot == 3 { return "agacchāma" }
if slot == 4 { return "agacchata" }
return "agacchan"
}
fn sa_gam_future(slot: Int) -> String {
if slot == 0 { return "gamiṣyāmi" }
if slot == 1 { return "gamiṣyasi" }
if slot == 2 { return "gamiṣyati" }
if slot == 3 { return "gamiṣyāmaḥ" }
if slot == 4 { return "gamiṣyatha" }
return "gamiṣyanti"
}
// Irregular verb: dṛś (to see)
//
// Suppletive present stem paśya- (Class 4 / ātmanepada suppletive).
// Used in the active (parasmaipada) sense throughout for NLG simplicity.
//
// Present: paśyāmi paśyasi paśyati paśyāmaḥ paśyatha paśyanti
// Imperfect: apaśyam series
// Future: drakṣyati series
fn sa_drs_present(slot: Int) -> String {
if slot == 0 { return "paśyāmi" }
if slot == 1 { return "paśyasi" }
if slot == 2 { return "paśyati" }
if slot == 3 { return "paśyāmaḥ" }
if slot == 4 { return "paśyatha" }
return "paśyanti"
}
fn sa_drs_past(slot: Int) -> String {
if slot == 0 { return "apaśyam" }
if slot == 1 { return "apaśyaḥ" }
if slot == 2 { return "apaśyat" }
if slot == 3 { return "apaśyāma" }
if slot == 4 { return "apaśyata" }
return "apaśyan"
}
fn sa_drs_future(slot: Int) -> String {
if slot == 0 { return "drakṣyāmi" }
if slot == 1 { return "drakṣyasi" }
if slot == 2 { return "drakṣyati" }
if slot == 3 { return "drakṣyāmaḥ" }
if slot == 4 { return "drakṣyatha" }
return "drakṣyanti"
}
// Irregular verb: vad (to speak, to say)
//
// Class 1; present stem vada-.
//
// Present: vadāmi vadasi vadati vadāmaḥ vadatha vadanti
// Imperfect: avadam series
// Future: vadiṣyati series
fn sa_vad_present(slot: Int) -> String {
if slot == 0 { return "vadāmi" }
if slot == 1 { return "vadasi" }
if slot == 2 { return "vadati" }
if slot == 3 { return "vadāmaḥ" }
if slot == 4 { return "vadatha" }
return "vadanti"
}
fn sa_vad_past(slot: Int) -> String {
if slot == 0 { return "avadam" }
if slot == 1 { return "avadaḥ" }
if slot == 2 { return "avadat" }
if slot == 3 { return "avadāma" }
if slot == 4 { return "avadata" }
return "avadan"
}
fn sa_vad_future(slot: Int) -> String {
if slot == 0 { return "vadiṣyāmi" }
if slot == 1 { return "vadiṣyasi" }
if slot == 2 { return "vadiṣyati" }
if slot == 3 { return "vadiṣyāmaḥ" }
if slot == 4 { return "vadiṣyatha" }
return "vadiṣyanti"
}
// Irregular verb: kṛ (to do, to make)
//
// Class 8 (tanādi); highly irregular. Present stem karo- (sg) / kuru- (pl).
//
// Present:
// 1sg karomi 2sg karoṣi 3sg karoti
// 1pl kurmaḥ 2pl kurutha 3pl kurvanti
//
// Imperfect: akaro- / akuru-
// Future: kariṣyati series
fn sa_kr_present(slot: Int) -> String {
if slot == 0 { return "karomi" }
if slot == 1 { return "karoṣi" }
if slot == 2 { return "karoti" }
if slot == 3 { return "kurmaḥ" }
if slot == 4 { return "kurutha" }
return "kurvanti"
}
fn sa_kr_past(slot: Int) -> String {
if slot == 0 { return "akaravam" }
if slot == 1 { return "akarodaḥ" }
if slot == 2 { return "akarot" }
if slot == 3 { return "akurma" }
if slot == 4 { return "akuruta" }
return "akurvan"
}
fn sa_kr_future(slot: Int) -> String {
if slot == 0 { return "kariṣyāmi" }
if slot == 1 { return "kariṣyasi" }
if slot == 2 { return "kariṣyati" }
if slot == 3 { return "kariṣyāmaḥ" }
if slot == 4 { return "kariṣyatha" }
return "kariṣyanti"
}
// Class-1 regular conjugation (bhū-adi)
//
// The thematic class: root guṇa-strengthened root + a + personal ending.
// Present endings (parasmaipada):
// 1sg -āmi 2sg -asi 3sg -ati
// 1pl -āmaḥ 2pl -atha 3pl -anti
//
// Imperfect (laṅ) = augment a- + stem + imperfect endings:
// 1sg -am 2sg -aḥ 3sg -at
// 1pl -āma 2pl -ata 3pl -an
//
// Future (lṛṭ) = stem + iṣya + present personal endings
//
// The caller supplies the present-tense verbal stem (e.g. "bodha" for
// "to know/wake"). We do not derive stems automatically from roots for
// arbitrary input only the known irregular verbs above have rootstem
// derivation. Unknown verbs are conjugated as if their input IS the stem.
fn sa_class1_present_ending(slot: Int) -> String {
if slot == 0 { return "āmi" }
if slot == 1 { return "asi" }
if slot == 2 { return "ati" }
if slot == 3 { return "āmaḥ" }
if slot == 4 { return "atha" }
return "anti"
}
fn sa_class1_past_ending(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "aḥ" }
if slot == 2 { return "at" }
if slot == 3 { return "āma" }
if slot == 4 { return "ata" }
return "an"
}
fn sa_class1_future_ending(slot: Int) -> String {
if slot == 0 { return "iṣyāmi" }
if slot == 1 { return "iṣyasi" }
if slot == 2 { return "iṣyati" }
if slot == 3 { return "iṣyāmaḥ" }
if slot == 4 { return "iṣyatha" }
return "iṣyanti"
}
fn sa_class1_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + sa_class1_present_ending(slot)
}
if str_eq(tense, "past") {
return "a" + stem + sa_class1_past_ending(slot)
}
if str_eq(tense, "future") {
return stem + sa_class1_future_ending(slot)
}
return stem
}
// sa_conjugate: main conjugation entry point
//
// verb: IAST form (e.g. "gam", "as") or English canonical ("be", "go")
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural" (or "dual" treated as plural)
//
// Returns the inflected form in IAST. Falls back to the verb stem for any
// unknown input rather than crashing.
fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = sa_map_canonical(verb)
let slot: Int = sa_slot(person, number)
// Irregular: as (to be)
if str_eq(v, "as") {
if str_eq(tense, "present") { return sa_as_present(slot) }
if str_eq(tense, "past") { return sa_as_past(slot) }
if str_eq(tense, "future") { return sa_as_future(slot) }
return v
}
// Irregular: bhū (to be/become)
if str_eq(v, "bhu") {
if str_eq(tense, "present") { return sa_bhu_present(slot) }
if str_eq(tense, "past") { return sa_bhu_past(slot) }
if str_eq(tense, "future") { return sa_bhu_future(slot) }
return v
}
// Irregular: gam (to go)
if str_eq(v, "gam") {
if str_eq(tense, "present") { return sa_gam_present(slot) }
if str_eq(tense, "past") { return sa_gam_past(slot) }
if str_eq(tense, "future") { return sa_gam_future(slot) }
return v
}
// Irregular: dṛś / drs (to see)
if str_eq(v, "drs") {
if str_eq(tense, "present") { return sa_drs_present(slot) }
if str_eq(tense, "past") { return sa_drs_past(slot) }
if str_eq(tense, "future") { return sa_drs_future(slot) }
return v
}
// Irregular: vad (to speak/say)
if str_eq(v, "vad") {
if str_eq(tense, "present") { return sa_vad_present(slot) }
if str_eq(tense, "past") { return sa_vad_past(slot) }
if str_eq(tense, "future") { return sa_vad_future(slot) }
return v
}
// Irregular: kṛ / kr (to do/make)
if str_eq(v, "kr") {
if str_eq(tense, "present") { return sa_kr_present(slot) }
if str_eq(tense, "past") { return sa_kr_past(slot) }
if str_eq(tense, "future") { return sa_kr_future(slot) }
return v
}
// Regular Class-1 fallback
// Treat the supplied string as a present-tense verbal stem and apply the
// standard thematic endings. This handles any verb the caller passes in
// the form "<stem>" without a recognised root tag.
return sa_class1_conjugate(v, tense, slot)
}
// a-stem masculine paradigm (deva)
//
// Stems of the deva- type are the most numerous Sanskrit noun class.
// All eight cases × singular and plural are encoded below.
//
// Singular:
// nom deva-ḥ "devaḥ" (visarga in citation; use bare form here)
// acc deva-m "devam"
// ins deva-na "devena" (guṇa: a+nena)
// dat deva-āya "devāya"
// abl deva-āt "devāt"
// gen deva-sya "devasya"
// loc deva-e "deve"
// voc deva "deva" (bare stem)
//
// Plural:
// nom deva-āḥ "devāḥ"
// acc deva-ān "devān"
// ins deva-aiḥ "devaiḥ"
// dat deva-bhyaḥ "devebhyaḥ" (with connecting -e-)
// abl deva-bhyaḥ "devebhyaḥ" (dat=abl in plural for a-stems)
// gen deva-ānām "devānām"
// loc deva-eṣu "deveṣu"
// voc deva-āḥ "devāḥ"
fn sa_decline_a_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "m" }
if str_eq(gram_case, "instrumental") { return stem + "ena" }
if str_eq(gram_case, "dative") { return stem + "āya" }
if str_eq(gram_case, "ablative") { return stem + "āt" }
if str_eq(gram_case, "genitive") { return stem + "sya" }
if str_eq(gram_case, "locative") { return stem + "e" }
if str_eq(gram_case, "vocative") { return stem }
return stem
}
fn sa_decline_a_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "āḥ" }
if str_eq(gram_case, "accusative") { return stem + "ān" }
if str_eq(gram_case, "instrumental") { return stem + "aiḥ" }
if str_eq(gram_case, "dative") { return stem + "ebhyaḥ" }
if str_eq(gram_case, "ablative") { return stem + "ebhyaḥ" }
if str_eq(gram_case, "genitive") { return stem + "ānām" }
if str_eq(gram_case, "locative") { return stem + "eṣu" }
if str_eq(gram_case, "vocative") { return stem + "āḥ" }
return stem + "āḥ"
}
// ā-stem feminine paradigm (devī / nārī type)
//
// ā-stems are the primary feminine class. Paradigm for nārī (woman):
//
// Singular:
// nom nārī acc nārīm ins nāryā
// dat nāryai abl nāryāḥ gen nāryāḥ
// loc nāryām voc nāri
//
// Plural:
// nom nāryaḥ acc nārīḥ ins nārībhiḥ
// dat nārībhyaḥ abl nārībhyaḥ gen nārīṇām
// loc nārīṣu voc nāryaḥ
//
// For input the caller passes the nominative singular form (e.g. "nārī").
// We strip the final ī to obtain the stem for oblique formation.
fn sa_decline_aa_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ī" }
if str_eq(gram_case, "accusative") { return stem + "īm" }
if str_eq(gram_case, "instrumental") { return stem + "" }
if str_eq(gram_case, "dative") { return stem + "yai" }
if str_eq(gram_case, "ablative") { return stem + "yāḥ" }
if str_eq(gram_case, "genitive") { return stem + "yāḥ" }
if str_eq(gram_case, "locative") { return stem + "yām" }
if str_eq(gram_case, "vocative") { return stem + "i" }
return stem + "ī"
}
fn sa_decline_aa_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "yaḥ" }
if str_eq(gram_case, "accusative") { return stem + "īḥ" }
if str_eq(gram_case, "instrumental") { return stem + "ībhiḥ" }
if str_eq(gram_case, "dative") { return stem + "ībhyaḥ" }
if str_eq(gram_case, "ablative") { return stem + "ībhyaḥ" }
if str_eq(gram_case, "genitive") { return stem + "īṇām" }
if str_eq(gram_case, "locative") { return stem + "īṣu" }
if str_eq(gram_case, "vocative") { return stem + "yaḥ" }
return stem + "yaḥ"
}
// Stem-type detection
//
// Infers the stem class from the nominative singular form supplied by the
// caller. Sanskrit stems are conventionally cited in their nom-sg form.
//
// Heuristics (sufficient for the NLG working vocabulary):
// ends in ā -> ā-stem feminine
// ends in ī -> ā-stem feminine (long ī subtype)
// ends in aḥ -> a-stem masculine (visarga ending from -as)
// ends in a -> treat as a-stem masculine (bare stem supplied)
// otherwise -> return base form as-is (unknown/consonant stem fallback)
fn sa_stem_type(noun: String) -> String {
if sa_str_ends(noun, "ā") { return "aa" }
if sa_str_ends(noun, "ī") { return "aa" }
if sa_str_ends(noun, "aḥ") { return "a" }
if sa_str_ends(noun, "a") { return "a" }
return "unknown"
}
// sa_extract_stem: strip the nominative-singular suffix to get the bare stem.
//
// a-stem "deva" -> "dev" (if ends in bare -a; strip final char)
// "devaḥ" -> "dev" (strip -aḥ = 2 Unicode code-points but
// since is multi-byte we treat "aḥ" as suffix)
// ā-stem "nārī" -> "nār" (strip )
// "nārā" -> "nār" (strip )
//
// Because IAST diacritics are multi-byte UTF-8 the raw byte lengths do not
// equal character counts. The engine's str_len / str_slice operate on bytes.
// Rather than counting UTF-8 bytes for each diacritic here we take a simpler
// path: we look for a known suffix and drop a fixed number of characters.
// For the characters used:
// ā = 2 bytes (U+0101)
// ī = 2 bytes (U+012B)
// = 3 bytes (U+1E25)
// So "aḥ" = 1 + 3 = 4 bytes; bare "a" = 1 byte; "ā" = 2 bytes; "ī" = 2 bytes.
//
// The function uses str_ends_with for detection, then str_slice to strip.
fn sa_extract_stem(noun: String, stype: String) -> String {
let n: Int = str_len(noun)
if str_eq(stype, "a") {
// Check whether it ends in "aḥ" (visarga form): 4 bytes to strip
if sa_str_ends(noun, "aḥ") {
return str_slice(noun, 0, n - 4)
}
// Otherwise bare -a: 1 byte
return str_slice(noun, 0, n - 1)
}
if str_eq(stype, "aa") {
// ī or ā: both 2 bytes
return str_slice(noun, 0, n - 2)
}
return noun
}
// sa_decline: main declension entry point
//
// noun: nominative singular IAST form (e.g. "deva", "devaḥ", "nārī")
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural" (dual plural)
//
// Returns the inflected form. Unknown stems return the noun unchanged.
fn sa_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = sa_stem_type(noun)
if str_eq(stype, "a") {
let stem: String = sa_extract_stem(noun, "a")
if str_eq(number, "singular") { return sa_decline_a_stem_sg(stem, gram_case) }
return sa_decline_a_stem_pl(stem, gram_case)
}
if str_eq(stype, "aa") {
let stem: String = sa_extract_stem(noun, "aa")
if str_eq(number, "singular") { return sa_decline_aa_stem_sg(stem, gram_case) }
return sa_decline_aa_stem_pl(stem, gram_case)
}
// Unknown stem class: return noun unchanged rather than producing garbage
return noun
}
// sa_noun_phrase: noun phrase builder
//
// Sanskrit has no articles neither definite nor indefinite. The definite
// parameter is accepted for interface compatibility with other language modules
// but has no effect on the output.
//
// Sanskrit expresses definiteness and referential status through word order,
// demonstratives (etad / tad), and discourse context none of which is the
// responsibility of the morphology module.
//
// noun: nominative singular IAST form
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural"
// definite: ignored
fn sa_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return sa_decline(noun, gram_case, number)
}
+36
View File
@@ -0,0 +1,36 @@
// auto-generated by elc --emit-header — do not edit
extern fn sa_str_ends(s: String, suf: String) -> Bool
extern fn sa_str_drop_last(s: String, n: Int) -> String
extern fn sa_slot(person: String, number: String) -> Int
extern fn sa_map_canonical(verb: String) -> String
extern fn sa_as_present(slot: Int) -> String
extern fn sa_as_past(slot: Int) -> String
extern fn sa_as_future(slot: Int) -> String
extern fn sa_bhu_present(slot: Int) -> String
extern fn sa_bhu_past(slot: Int) -> String
extern fn sa_bhu_future(slot: Int) -> String
extern fn sa_gam_present(slot: Int) -> String
extern fn sa_gam_past(slot: Int) -> String
extern fn sa_gam_future(slot: Int) -> String
extern fn sa_drs_present(slot: Int) -> String
extern fn sa_drs_past(slot: Int) -> String
extern fn sa_drs_future(slot: Int) -> String
extern fn sa_vad_present(slot: Int) -> String
extern fn sa_vad_past(slot: Int) -> String
extern fn sa_vad_future(slot: Int) -> String
extern fn sa_kr_present(slot: Int) -> String
extern fn sa_kr_past(slot: Int) -> String
extern fn sa_kr_future(slot: Int) -> String
extern fn sa_class1_present_ending(slot: Int) -> String
extern fn sa_class1_past_ending(slot: Int) -> String
extern fn sa_class1_future_ending(slot: Int) -> String
extern fn sa_class1_conjugate(stem: String, tense: String, slot: Int) -> String
extern fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sa_decline_a_stem_sg(stem: String, gram_case: String) -> String
extern fn sa_decline_a_stem_pl(stem: String, gram_case: String) -> String
extern fn sa_decline_aa_stem_sg(stem: String, gram_case: String) -> String
extern fn sa_decline_aa_stem_pl(stem: String, gram_case: String) -> String
extern fn sa_stem_type(noun: String) -> String
extern fn sa_extract_stem(noun: String, stype: String) -> String
extern fn sa_decline(noun: String, gram_case: String, number: String) -> String
extern fn sa_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+455
View File
@@ -0,0 +1,455 @@
// morphology-sga.el - Old Irish morphology for the NLG engine.
//
// Implements Old Irish verb conjugation, noun declension, and initial consonant
// lenition for the ca. 600-900 CE period. Designed as a companion to
// morphology.el and called by the engine when the language profile code is "sga".
//
// Language profile: code=sga, name=Old Irish, morph_type=fusional, word_order=VSO,
// question_strategy=particle, script=latin, family=celtic.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third x singular/plural (slots 0-5)
// Forms: absolute (verb-initial position only; conjunct not implemented)
// Irregulars: bith (to be, substantive), téit (to go), gaibid (to take/hold),
// ad· (to see), as·beir (to say)
// Copula: "is" invariant in 3sg; used as the default copular form
// Canonical map: "be" -> copula "is" (3sg) / "bith" (substantive be)
//
// Noun declension covered:
// o-stem masculine (like "fer" man)
// ā-stem feminine (like "ben" woman)
// Cases: nominative, vocative, accusative, genitive, dative
// Numbers: singular, plural
// Definite article: simplified "in" prefix (article + noun)
//
// Lenition (initial mutation):
// b->bh, c->ch, d->dh, f->fh, g->gh, m->mh, p->ph, s->sh, t->th
// Other initial consonants and vowels are returned unchanged.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn sga_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn sga_first(s: String) -> String {
if str_len(s) == 0 { return "" }
return str_slice(s, 0, 1)
}
fn sga_rest(s: String) -> String {
let n: Int = str_len(s)
if n <= 1 { return "" }
return str_slice(s, 1, n)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular ()
// 1 = 2nd singular ()
// 2 = 3rd singular (é/sí/ed)
// 3 = 1st plural (sní)
// 4 = 2nd plural ()
// 5 = 3rd plural (é/sí/ed)
fn sga_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Lenition
//
// Initial consonant lenition (séimhiú) the most common mutation in Old Irish.
// Applies after certain prepositions, the genitive of feminine nouns, and many
// other grammatical environments. Simplified: only the first consonant is mutated.
//
// Lenition table (b c d f g m p s t):
// b -> bh c -> ch d -> dh f -> fh g -> gh
// m -> mh p -> ph s -> sh t -> th
// Vowels and other consonants: unchanged.
fn sga_lenite(word: String) -> String {
let init: String = sga_first(word)
let tail: String = sga_rest(word)
if str_eq(init, "b") { return "bh" + tail }
if str_eq(init, "c") { return "ch" + tail }
if str_eq(init, "d") { return "dh" + tail }
if str_eq(init, "f") { return "fh" + tail }
if str_eq(init, "g") { return "gh" + tail }
if str_eq(init, "m") { return "mh" + tail }
if str_eq(init, "p") { return "ph" + tail }
if str_eq(init, "s") { return "sh" + tail }
if str_eq(init, "t") { return "th" + tail }
// Vowels and unlenitable consonants (l, n, r, ): unchanged
return word
}
// Copula paradigm
//
// The Old Irish copula "is" is largely invariant in the present tense in its
// most common (3rd singular assertive) use. The full paradigm has variants
// (am, at, is in sg; ammi, adib, it in pl) but for NLG purposes we return "is"
// for 3sg and the simplified forms elsewhere.
fn sga_copula_present(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "at" }
if slot == 2 { return "is" }
if slot == 3 { return "am" }
if slot == 4 { return "adib" }
return "it"
}
// Substantive "bith" (to be)
//
// "Bith" is the substantive/existential verb for being. It differs from the
// copula "is" in that it can carry tense inflection and express existence rather
// than predication.
//
// Present: am, at, is, am, adib, at
// Past: ba, ba, ba, bámmar, bádaid, batar
fn sga_bith_present(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "at" }
if slot == 2 { return "is" }
if slot == 3 { return "am" }
if slot == 4 { return "adib" }
return "at"
}
fn sga_bith_past(slot: Int) -> String {
if slot == 0 { return "ba" }
if slot == 1 { return "ba" }
if slot == 2 { return "ba" }
if slot == 3 { return "bámmar" }
if slot == 4 { return "bádaid" }
return "batar"
}
// Irregular verb tables
// téit (to go) strong verb, highly suppletive in the past
// Present: tíagu, téit, téit, tíagmai, tíagid, tíagat
// Past: lod, lod, luid, lodmar, lodaid, lotar
fn sga_teit_present(slot: Int) -> String {
if slot == 0 { return "tíagu" }
if slot == 1 { return "téit" }
if slot == 2 { return "téit" }
if slot == 3 { return "tíagmai" }
if slot == 4 { return "tíagid" }
return "tíagat"
}
fn sga_teit_past(slot: Int) -> String {
if slot == 0 { return "lod" }
if slot == 1 { return "lod" }
if slot == 2 { return "luid" }
if slot == 3 { return "lodmar" }
if slot == 4 { return "lodaid" }
return "lotar"
}
// gaibid (to take/hold) AI class strong verb
// Present: gaibim, gaibi, gaibid, gaibmi, gaibthe, gaibid
fn sga_gaibid_present(slot: Int) -> String {
if slot == 0 { return "gaibim" }
if slot == 1 { return "gaibi" }
if slot == 2 { return "gaibid" }
if slot == 3 { return "gaibmi" }
if slot == 4 { return "gaibthe" }
return "gaibid"
}
// ad· (to see) compound verb with deuterotonic stress
// Present: ad·ciu, ad·cí, ad·cí, ad·cími, ad·cíthe, ad·ciat
fn sga_adci_present(slot: Int) -> String {
if slot == 0 { return "ad·ciu" }
if slot == 1 { return "ad·cí" }
if slot == 2 { return "ad·cí" }
if slot == 3 { return "ad·cími" }
if slot == 4 { return "ad·cíthe" }
return "ad·ciat"
}
// as·beir (to say) compound verb
// Present: as·biur, as·beir, as·beir, as·beram, as·berid, as·berat
fn sga_asbeir_present(slot: Int) -> String {
if slot == 0 { return "as·biur" }
if slot == 1 { return "as·beir" }
if slot == 2 { return "as·beir" }
if slot == 3 { return "as·beram" }
if slot == 4 { return "as·berid" }
return "as·berat"
}
// Canonical verb mapping
//
// Maps English semantic labels to Old Irish infinitives / citation forms.
// Old Irish verbs are cited by 3sg present absolute (the most stable form).
fn sga_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "is" }
if str_eq(verb, "go") { return "téit" }
if str_eq(verb, "take") { return "gaibid" }
if str_eq(verb, "hold") { return "gaibid" }
if str_eq(verb, "see") { return "ad·cí" }
if str_eq(verb, "say") { return "as·beir" }
return verb
}
// Regular AI-class verb conjugation
//
// The AI (first) conjugation is the most productive class in Old Irish.
// Stems ending in a broad consonant take broad endings; slender take slender
// endings. For simplicity this module uses a single generic set.
//
// Present absolute (verb-initial position):
// Sg: 1st -aim, 2nd -ai, 3rd -aid
// Pl: 1st -am, 2nd -aid, 3rd -at
//
// There is no regular past tense for AI verbs in the simple sense the engine
// falls back to the citation form when past is requested for unknown verbs.
fn sga_ai_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "aim" }
if slot == 1 { return stem + "ai" }
if slot == 2 { return stem + "aid" }
if slot == 3 { return stem + "am" }
if slot == 4 { return stem + "aid" }
return stem + "at"
}
// sga_conjugate: main conjugation entry point
//
// verb: Old Irish citation form or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected absolute form. Unknown tenses fall back to the citation
// form. Conjunct forms (after particles) are not implemented; only absolute
// forms are produced.
fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = sga_map_canonical(verb)
let slot: Int = sga_slot(person, number)
// Copula "is"
if str_eq(v, "is") {
if str_eq(tense, "present") { return sga_copula_present(slot) }
// Past copula: "ba" is the standard past (invariant simplification)
return "ba"
}
// Substantive bith
if str_eq(v, "bith") {
if str_eq(tense, "present") { return sga_bith_present(slot) }
if str_eq(tense, "past") { return sga_bith_past(slot) }
return v
}
// téit (to go)
if str_eq(v, "téit") {
if str_eq(tense, "present") { return sga_teit_present(slot) }
if str_eq(tense, "past") { return sga_teit_past(slot) }
return v
}
// gaibid (to take/hold)
if str_eq(v, "gaibid") {
if str_eq(tense, "present") { return sga_gaibid_present(slot) }
// Past gaibid: gab- preterite (simplified to slot 2 form for all)
return "gab"
}
// ad· (to see)
if str_eq(v, "ad·cí") {
if str_eq(tense, "present") { return sga_adci_present(slot) }
// Past: ro·acc forms exist but are complex; return citation form
return v
}
// as·beir (to say)
if str_eq(v, "as·beir") {
if str_eq(tense, "present") { return sga_asbeir_present(slot) }
return v
}
// Regular AI-class verb
//
// Citation form for AI verbs ends in -id (3sg pres abs). Strip -id to get
// the stem, then apply AI endings. If the verb doesn't end in -id, use the
// whole form as the stem (best-effort).
if str_ends_with(v, "id") {
let stem: String = sga_drop(v, 2)
if str_eq(tense, "present") { return sga_ai_present(stem, slot) }
// No regular past implemented: return citation form
return v
}
// Unknown verb: return citation form unchanged
return v
}
// o-stem masculine declension ("fer" man)
//
// The o-stem is the most common masculine declension class.
//
// Singular: nom fer voc a fhir acc fer gen fir dat fiur
// Plural: nom fir voc a firu acc firu gen fer dat feraib
fn sga_decline_ostem(noun: String, gram_case: String, number: String) -> String {
if str_eq(noun, "fer") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "fer" }
if str_eq(gram_case, "vocative") { return "fhir" }
if str_eq(gram_case, "accusative") { return "fer" }
if str_eq(gram_case, "genitive") { return "fir" }
if str_eq(gram_case, "dative") { return "fiur" }
return "fer"
}
if str_eq(gram_case, "nominative") { return "fir" }
if str_eq(gram_case, "vocative") { return "firu" }
if str_eq(gram_case, "accusative") { return "firu" }
if str_eq(gram_case, "genitive") { return "fer" }
if str_eq(gram_case, "dative") { return "feraib" }
return "fir"
}
// Generic o-stem masculine: append case endings to noun base.
// Nom/Acc sg take no ending; Gen sg syncopates (approximated by +a removed);
// the engine uses safe suffix-only approximations here.
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "vocative") { return sga_lenite(noun) }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "u" }
return noun
}
if str_eq(gram_case, "nominative") { return noun + "i" }
if str_eq(gram_case, "vocative") { return noun + "u" }
if str_eq(gram_case, "accusative") { return noun + "u" }
if str_eq(gram_case, "genitive") { return noun }
if str_eq(gram_case, "dative") { return noun + "aib" }
return noun + "i"
}
// ā-stem feminine declension ("ben" woman)
//
// The ā-stem is the most common feminine declension class. It shows heavy
// syncope and internal change (ben -> mná in oblique forms).
//
// Singular: nom ben voc a ben acc bein gen mná dat mnáib
// Plural: nom mná voc a mná acc mná gen ban dat mnáib
fn sga_decline_astem(noun: String, gram_case: String, number: String) -> String {
if str_eq(noun, "ben") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "ben" }
if str_eq(gram_case, "vocative") { return "ben" }
if str_eq(gram_case, "accusative") { return "bein" }
if str_eq(gram_case, "genitive") { return "mná" }
if str_eq(gram_case, "dative") { return "mnáib" }
return "ben"
}
if str_eq(gram_case, "nominative") { return "mná" }
if str_eq(gram_case, "vocative") { return "mná" }
if str_eq(gram_case, "accusative") { return "mná" }
if str_eq(gram_case, "genitive") { return "ban" }
if str_eq(gram_case, "dative") { return "mnáib" }
return "mná"
}
// Generic ā-stem feminine: suffix-based approximation.
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "vocative") { return noun }
if str_eq(gram_case, "accusative") { return noun + "i" }
if str_eq(gram_case, "genitive") { return noun + "e" }
if str_eq(gram_case, "dative") { return noun + "aib" }
return noun
}
if str_eq(gram_case, "nominative") { return noun + "a" }
if str_eq(gram_case, "vocative") { return noun + "a" }
if str_eq(gram_case, "accusative") { return noun + "a" }
if str_eq(gram_case, "genitive") { return noun }
if str_eq(gram_case, "dative") { return noun + "aib" }
return noun + "a"
}
// Gender detection heuristic
//
// Ideally gender is supplied by the lexicon. Known exemplars are routed
// explicitly; everything else defaults to o-stem masculine.
fn sga_detect_gender(noun: String) -> String {
if str_eq(noun, "ben") { return "feminine" }
if str_eq(noun, "mná") { return "feminine" }
// Default: masculine o-stem
return "masculine"
}
// sga_decline: main declension entry point
//
// noun: nominative singular Old Irish noun (e.g. "fer", "ben")
// gram_case: "nominative" | "vocative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to nominative singular on unknown input.
fn sga_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = sga_detect_gender(noun)
if str_eq(gender, "masculine") { return sga_decline_ostem(noun, gram_case, number) }
if str_eq(gender, "feminine") { return sga_decline_astem(noun, gram_case, number) }
// Fallback
return noun
}
// sga_noun_phrase: noun phrase builder
//
// Old Irish has a definite article that agrees in case, number, and gender.
// The article has many allomorphs (int, in, ind, na, ). For NLG purposes this
// module uses the simplified invariant form "in" for all definite contexts,
// placed before the declined noun.
//
// Indefinite nouns carry no article (like Latin).
//
// noun: nominative singular Old Irish noun
// gram_case: "nominative" | "vocative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false"
//
// Returns the complete noun phrase string.
fn sga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let base: String = sga_decline(noun, gram_case, number)
if !str_eq(definite, "true") { return base }
// Definite: prepend simplified article "in"
return "in " + base
}
+22
View File
@@ -0,0 +1,22 @@
// auto-generated by elc --emit-header — do not edit
extern fn sga_drop(s: String, n: Int) -> String
extern fn sga_first(s: String) -> String
extern fn sga_rest(s: String) -> String
extern fn sga_slot(person: String, number: String) -> Int
extern fn sga_lenite(word: String) -> String
extern fn sga_copula_present(slot: Int) -> String
extern fn sga_bith_present(slot: Int) -> String
extern fn sga_bith_past(slot: Int) -> String
extern fn sga_teit_present(slot: Int) -> String
extern fn sga_teit_past(slot: Int) -> String
extern fn sga_gaibid_present(slot: Int) -> String
extern fn sga_adci_present(slot: Int) -> String
extern fn sga_asbeir_present(slot: Int) -> String
extern fn sga_map_canonical(verb: String) -> String
extern fn sga_ai_present(stem: String, slot: Int) -> String
extern fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sga_decline_ostem(noun: String, gram_case: String, number: String) -> String
extern fn sga_decline_astem(noun: String, gram_case: String, number: String) -> String
extern fn sga_detect_gender(noun: String) -> String
extern fn sga_decline(noun: String, gram_case: String, number: String) -> String
extern fn sga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+640
View File
@@ -0,0 +1,640 @@
// morphology-sux.el Sumerian morphology for the NLG engine.
//
// Implements Sumerian noun declension, verb conjugation, and sentence
// construction. Designed as a companion to morphology.el and called by
// the engine when the language profile code is "sux".
//
// Language profile: code=sux, name=Sumerian, morph_type=agglutinative,
// word_order=SOV, question_strategy=particle, script=latin_transliteration,
// family=isolate (no known relatives).
//
// Typological overview:
// Sumerian is the world's oldest attested written language, first recorded
// in Uruk ca. 3500 BCE; active spoken use through ca. 2000 BCE; learned
// scribal language to ca. 100 CE. It is a language isolate unrelated to
// any other known language family.
//
// ERGATIVITY: Sumerian is ergative-absolutive. The subject of a transitive
// verb (the AGENT) takes the ergative case (-e). The subject of an
// intransitive verb and the direct object of a transitive verb share the
// same form: the ABSOLUTIVE (unmarked, no suffix). This is structurally
// the opposite of nominative-accusative languages (like Latin or Greek)
// where the subject of both transitive and intransitive verbs is marked the
// same way. Example: lugal-e é mu-un-dù = "the king built the house",
// where lugal-e = king-ERG (agent of transitive "build") and é = house-ABS
// (patient, unmarked).
//
// NOUN CLASSES: animate (humans, gods) and inanimate (everything else).
// Animate plurals use -ene; inanimate plurals use -a (collective).
//
// VERB CHAIN: Sumerian finite verbs are polysynthetic chains encoding:
// (modal)(negation)(dimensional prefixes)(stem)(voice/causative)(suffixes)
// The dimensional prefix chain includes pronominal agreement markers for
// agent and patient. This module implements a simplified but linguistically
// honest subset: perfective mu-/imperfective i- prefix + personal suffixes.
//
// KEY VERBS: Several high-frequency verbs have frozen citation forms.
// The copula is often omitted in equational sentences; when present it is
// usually the clitic -am or the verb me "to be".
//
// TRANSLITERATION: Standard Assyriological transliteration uses subscript
// numbers for sign disambiguation (dug, tum, am) written here with the
// corresponding Unicode characters (dug4, tum2, am3 using ASCII where the
// subscript forms are unavailable in string literals, or as etc. where
// needed). Special characters: š (sh), ŋ (velar nasal), ĝ (= ŋ alternate),
// (voiceless velar/uvular fricative).
//
// Cases implemented:
// absolutive (), ergative (-e), genitive (-ak), dative (-ra),
// locative (-a), ablative (-ta), comitative (-da), equative (-gin)
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq,
// native_list_empty, native_list_append, native_list_get, native_list_len)
// String helpers
import "morphology.el"
fn sux_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn sux_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn sux_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn sux_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index used in paradigm tables.
// 0 = 1st singular (ĝe / ĝa "I")
// 1 = 2nd singular (za-e "you")
// 2 = 3rd singular (animate: a-ne "he/she"; inanimate implied)
// 3 = 1st plural (me "we")
// 4 = 2nd plural (me-zen "you (pl)")
// 5 = 3rd plural (animate: a-ne-ne "they")
//
// Dual is not grammaticalised distinctly from plural in Sumerian verbal
// morphology; dual inputs fall through to plural.
fn sux_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third person
if str_eq(number, "singular") { return 2 }
return 5
}
// Ergative personal suffixes
//
// These encode the AGENT of a transitive verb when it appears as a standalone
// noun suffix (postposition on the nominal element in the ergative chain).
// In practice the -e ergative postposition on nouns is more common; these
// pronominal suffixes appear in the verbal chain. Provided here for the
// pronominal agent agreement slot.
//
// Ergative suffixes on nominal phrases (the postposition form, not verbal):
// 1sg: -(e)n 2sg: -(e)n 3sg animate: -e
// 1pl: -enden 2pl: -enzen 3pl animate: -eš
fn sux_ergative_suffix(person: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "-en" }
return "-enden"
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return "-en" }
return "-enzen"
}
// third
if str_eq(number, "singular") { return "-e" }
return "-eš"
}
// Absolutive personal suffixes
//
// The absolutive is the UNMARKED case in Sumerian ergative grammar. It is
// used for:
// (a) the single argument of an intransitive verb (= subject)
// (b) the direct object of a transitive verb (= patient)
//
// There is no overt suffix on the noun in the absolutive. However, the VERB
// agrees with the absolutive argument via a suffix in the verbal chain.
// These are the verbal agreement suffixes for the absolutive participant:
// 1sg: -en 2sg: -en 3sg: (null) 1pl: -enden 2pl: -enzen 3pl:
fn sux_absolutive_suffix(person: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "-en" }
return "-enden"
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return "-en" }
return "-enzen"
}
// third person: null agreement suffix for 3sg and 3pl
return ""
}
// Canonical verb mapping
//
// The semantic layer may pass English canonical labels. Map these to the
// Sumerian citation stem (the base form as used in transliteration).
fn sux_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "me" }
if str_eq(verb, "say") { return "dug4" }
if str_eq(verb, "go") { return "du" }
if str_eq(verb, "see") { return "igi-bar" }
if str_eq(verb, "do") { return "ak" }
if str_eq(verb, "make") { return "ak" }
if str_eq(verb, "bring") { return "tum2" }
if str_eq(verb, "build") { return "" }
if str_eq(verb, "give") { return "šum2" }
if str_eq(verb, "know") { return "zu" }
if str_eq(verb, "hear") { return "ĝeštug2 ĝar" }
if str_eq(verb, "love") { return "ki-aĝ2" }
if str_eq(verb, "sit") { return "tuš" }
if str_eq(verb, "stand") { return "gub" }
if str_eq(verb, "come") { return "ĝen" }
if str_eq(verb, "eat") { return "gu7" }
if str_eq(verb, "drink") { return "naĝ" }
if str_eq(verb, "write") { return "sar" }
return verb
}
// Verbal personal suffixes (simplified finite chain)
//
// In the simplified model the personal suffix encodes the absolutive argument
// (the subject of intransitive verbs, or the patient of transitive verbs).
// Full Sumerian verbal morphology is polysynthetic and beyond NLG scope; this
// models the most productive layer: stem + absolutive agreement.
//
// Suffix paradigm:
// slot 0 (1sg): -en slot 3 (1pl): -enden
// slot 1 (2sg): -en slot 4 (2pl): -enzen
// slot 2 (3sg): slot 5 (3pl): -eš
fn sux_personal_suffix(slot: Int) -> String {
if slot == 0 { return "en" }
if slot == 1 { return "en" }
if slot == 2 { return "" }
if slot == 3 { return "enden" }
if slot == 4 { return "enzen" }
return ""
}
// Special: me (to be)
//
// The Sumerian copula "me" (to be) has several uses:
// - As a full verb with personal endings in present: me-en (I am), me-en
// (you are), / -am3 (he/she/it is copula often omitted or realised as
// the clitic -am3 attached to the predicate), me-en-dè (we are)
// - As a predicative clitic: noun/adj + -am3
// - In past: ha-ma-an-me (he/she was rare in NLG contexts)
//
// For NLG purposes:
// present: use me + personal suffix; 3sg = omit entirely or "-am3" clitic
// past: ba-me + personal suffix (archaic; approximate)
fn sux_me_present(slot: Int) -> String {
if slot == 0 { return "me-en" }
if slot == 1 { return "me-en" }
if slot == 2 { return "" }
if slot == 3 { return "me-en-dè" }
if slot == 4 { return "me-en-zè-en" }
return "me-eš"
}
fn sux_me_past(slot: Int) -> String {
if slot == 0 { return "ba-me-en" }
if slot == 1 { return "ba-me-en" }
if slot == 2 { return "ba-me" }
if slot == 3 { return "ba-me-en-dè" }
if slot == 4 { return "ba-me-en-zè-en" }
return "ba-me-eš"
}
// Special: dug4/e (to say)
//
// "dug4" is the perfective stem; "e" is the imperfective/present stem.
// Common finite forms:
// present (imperfective): e + personal suffix; 3sg = e
// past (perfective): mu-un-dug4 (3sg agent; simplified)
fn sux_dug4_present(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "e" }
return "e-" + suf
}
fn sux_dug4_past(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "mu-un-dug4" }
return "mu-un-dug4-" + suf
}
// Special: du (to go)
//
// Intransitive motion verb. The agent is in the absolutive.
// present (imperfective): i-du + personal suffix
// past (perfective): mu-un-du + personal suffix (mu- on intransitive
// when the agent is 3rd person; convention varies)
fn sux_du_present(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "i-du" }
return "i-du-" + suf
}
fn sux_du_past(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "mu-un-du" }
return "mu-un-du-" + suf
}
// Special: igi bar (to see "to open the eye")
//
// A compound verb: igi (eye) + bar (to open/spread). The noun igi is the
// incorporated object; bar is the finite verb element.
// present: igi i-bar + personal suffix
// past: igi mu-un-bar + personal suffix
fn sux_igibar_present(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "igi i-bar" }
return "igi i-bar-" + suf
}
fn sux_igibar_past(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "igi mu-un-bar" }
return "igi mu-un-bar-" + suf
}
// Special: ak (to do / make)
//
// High-frequency transitive verb; forms the basis of many compound verbs.
// present: i-ak + personal suffix
// past: mu-un-ak + personal suffix
fn sux_ak_present(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "i-ak" }
return "i-ak-" + suf
}
fn sux_ak_past(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "mu-un-ak" }
return "mu-un-ak-" + suf
}
// Special: tum2/de6 (to bring)
//
// Motion verb with incorporated directionality.
// present: i-tum2 + personal suffix
// past: mu-un-tum2 + personal suffix
fn sux_tum2_present(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "i-tum2" }
return "i-tum2-" + suf
}
fn sux_tum2_past(slot: Int) -> String {
let suf: String = sux_personal_suffix(slot)
if str_eq(suf, "") { return "mu-un-tum2" }
return "mu-un-tum2-" + suf
}
// sux_conjugate: main conjugation entry point
//
// verb: Sumerian verb stem or English canonical label
// tense: "present" | "past"
// (Sumerian distinguishes imperfective/perfective aspect rather than
// tense proper; "present" maps to imperfective i-, "past" to
// perfective mu-)
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected finite verb form in standard transliteration.
// Falls back to mu- + stem for unknown verbs (productive perfective prefix).
//
// Note on ergativity in the verb chain: the prefix mu- encodes that the
// agent (ergative subject) is 3rd person in the prototypical case. Full
// pronominal infixing in the dimensional prefix chain is not modelled here;
// the returned forms represent the 3rd-person-agent baseline with personal
// suffixes for the absolutive participant.
fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = sux_map_canonical(verb)
let slot: Int = sux_slot(person, number)
// "me" copula / to be
if str_eq(v, "me") {
if str_eq(tense, "present") { return sux_me_present(slot) }
if str_eq(tense, "past") { return sux_me_past(slot) }
return sux_me_present(slot)
}
// "dug4" to say
if str_eq(v, "dug4") {
if str_eq(tense, "present") { return sux_dug4_present(slot) }
if str_eq(tense, "past") { return sux_dug4_past(slot) }
return sux_dug4_past(slot)
}
// "du" to go
if str_eq(v, "du") {
if str_eq(tense, "present") { return sux_du_present(slot) }
if str_eq(tense, "past") { return sux_du_past(slot) }
return sux_du_past(slot)
}
// "igi-bar" to see
if str_eq(v, "igi-bar") {
if str_eq(tense, "present") { return sux_igibar_present(slot) }
if str_eq(tense, "past") { return sux_igibar_past(slot) }
return sux_igibar_past(slot)
}
// "ak" to do / make
if str_eq(v, "ak") {
if str_eq(tense, "present") { return sux_ak_present(slot) }
if str_eq(tense, "past") { return sux_ak_past(slot) }
return sux_ak_past(slot)
}
// "tum2" to bring
if str_eq(v, "tum2") {
if str_eq(tense, "present") { return sux_tum2_present(slot) }
if str_eq(tense, "past") { return sux_tum2_past(slot) }
return sux_tum2_past(slot)
}
// Regular fallback: prefix + stem + personal suffix
//
// For verbs not listed above, apply the productive rule:
// imperfective (present): i- + stem + suffix
// perfective (past): mu- + stem + suffix
//
// The "un" infix after mu- is a 3rd-person animate patient marker that
// appears with transitive verbs; we omit it in the generic fallback as we
// cannot know transitivity without a lexicon.
let suf: String = sux_personal_suffix(slot)
if str_eq(tense, "present") {
if str_eq(suf, "") { return "i-" + v }
return "i-" + v + "-" + suf
}
// past / perfective
if str_eq(suf, "") { return "mu-" + v }
return "mu-" + v + "-" + suf
}
// Animacy detection
//
// Sumerian grammatical animacy (ANIMATE vs INANIMATE) is a binary noun class
// distinction affecting plural formation and pronominal reference. Animate
// nouns denote humans and deities; everything else is inanimate.
//
// Heuristic: check for known divine/human markers in the noun:
// - dingir/diĝir prefix (divine determinative, written with sign AN: 𒀭)
// - Ends in -ra, -en (lordly epithets)
// - Known human occupational terms as exact matches
// - Otherwise: inanimate
//
// This is necessarily approximate without a full lexicon. The function errs
// toward inanimate (safer default for open-class nouns).
fn sux_is_animate(noun: String) -> Bool {
// Divine determinative in transliteration
if sux_str_ends(noun, "diĝir") { return true }
if sux_str_ends(noun, "dingir") { return true }
// Common human roles
if str_eq(noun, "lugal") { return true } // king
if str_eq(noun, "nin") { return true } // lady/queen
if str_eq(noun, "en") { return true } // lord
if str_eq(noun, "ensi2") { return true } // ruler/governor
if str_eq(noun, "dumu") { return true } // son/child
if str_eq(noun, "dam") { return true } // spouse/wife
if str_eq(noun, "ama") { return true } // mother
if str_eq(noun, "ad") { return true } // father
if str_eq(noun, "a2-dam") { return true } // wife (alternate)
if str_eq(noun, "lu2") { return true } // man/person
if str_eq(noun, "munus") { return true } // woman
if str_eq(noun, "ur") { return true } // man (archaic)
if str_eq(noun, "saĝ") { return true } // head person (as noun)
if str_eq(noun, "gudu4") { return true } // priest
if str_eq(noun, "sanga") { return true } // temple administrator
if str_eq(noun, "ugula") { return true } // overseer
if str_eq(noun, "dub-sar") { return true } // scribe
if str_eq(noun, "nar") { return true } // singer/musician
if str_eq(noun, "sukkal") { return true } // minister/vizier
// Check for divine name prefix d (determinative before proper names)
if sux_str_ends(noun, "d-") { return true }
// Otherwise: inanimate
return false
}
// Case suffixes
//
// Sumerian is ergative-absolutive. The cases below are postpositional clitics
// attached to the last element of the noun phrase.
//
// absolutive: (unmarked subject of intransitive, object of transitive)
// ergative: -e (subject of transitive verb the AGENT)
// genitive: -ak (possession, association; often written -a(k) before consonant)
// dative: -ra (indirect object, beneficiary, "for/to")
// locative: -a (location "in/at/on"; -e before certain consonants)
// ablative: -ta (source, separation "from")
// comitative: -da (accompaniment "with")
// equative: -gin (comparison "like/as")
//
// Note: in connected speech the genitive -ak loses its final -k before a
// consonant; this simplification uses the full form throughout.
fn sux_case_suffix(gram_case: String) -> String {
if str_eq(gram_case, "absolutive") { return "" }
if str_eq(gram_case, "ergative") { return "-e" }
if str_eq(gram_case, "genitive") { return "-ak" }
if str_eq(gram_case, "dative") { return "-ra" }
if str_eq(gram_case, "locative") { return "-a" }
if str_eq(gram_case, "ablative") { return "-ta" }
if str_eq(gram_case, "comitative") { return "-da" }
if str_eq(gram_case, "equative") { return "-gin" }
// Terminative (-še3: "to/toward") also attested
if str_eq(gram_case, "terminative") { return "-še" }
return ""
}
// sux_decline: noun declension
//
// noun: base form of the noun (absolutive singular = citation form)
// gram_case: one of the cases listed above
// number: "singular" | "plural"
//
// Plural formation:
// Animate nouns: base + -ene (e.g. lugal-ene "kings")
// Inanimate nouns: base + -a (collective/inanimate plural; less common,
// many inanimate nouns are not pluralised at all in Sumerian)
//
// The case suffix attaches AFTER the plural marker.
//
// Absolutive singular = the bare stem (no suffix added).
fn sux_decline(noun: String, gram_case: String, number: String) -> String {
let csuf: String = sux_case_suffix(gram_case)
if str_eq(number, "singular") {
// Absolutive singular: bare stem with no suffix at all
if str_eq(gram_case, "absolutive") { return noun }
// Strip the leading dash from the suffix for direct concatenation
let suf_len: Int = str_len(csuf)
let bare_suf: String = str_slice(csuf, 1, suf_len)
return noun + bare_suf
}
// Plural
let animate: Bool = sux_is_animate(noun)
let plural_stem: String = ""
if animate {
let plural_stem = noun + "ene"
}
if !animate {
let plural_stem = noun + "a"
}
if str_eq(gram_case, "absolutive") { return plural_stem }
let suf_len2: Int = str_len(csuf)
let bare_suf2: String = str_slice(csuf, 1, suf_len2)
return plural_stem + bare_suf2
}
// sux_noun_phrase: noun phrase builder
//
// Sumerian has NO articles (definite or indefinite). Determinateness is
// expressed through context, position, or the genitive construction.
// The "definite" parameter is accepted for interface compatibility but is
// ignored the returned form is always the declined noun alone.
//
// noun: base (absolutive) form
// gram_case: grammatical case string
// number: "singular" | "plural"
// definite: ignored (Sumerian has no articles)
fn sux_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return sux_decline(noun, gram_case, number)
}
// sux_verb_chain: build the finite verbal complex
//
// Constructs the full Sumerian SOV clause for a transitive or intransitive
// predication. Sumerian word order is strict SOV.
//
// For TRANSITIVE sentences (ergative construction):
// agent (in ergative case: noun + -e) + patient (absolutive) + verb
// Example: lugal-e é mu-un-dù
// king-ERG house-ABS built
// "The king built the house."
//
// For INTRANSITIVE sentences:
// agent (in absolutive case: bare noun) + verb
// Example: lugal du
// king-ABS went
// "The king went."
//
// agent: the subject noun (base/absolutive form); ergative suffix applied here
// verb: Sumerian verb stem or canonical English label
// patient: patient noun (absolutive; pass "" for intransitive)
// tense: "present" | "past"
//
// Note: this function handles third-person singular agreement throughout, which
// is the most common narrative form in Sumerian texts. The conjugated verb
// form uses sux_conjugate with "third"/"singular" for the absolutive agreement.
fn sux_verb_chain(agent: String, verb: String, patient: String, tense: String) -> String {
let conjugated: String = sux_conjugate(verb, tense, "third", "singular")
// Intransitive: agent in absolutive + verb
if str_eq(patient, "") {
return agent + " " + conjugated
}
// Transitive: agent in ergative + patient in absolutive + verb
// Apply ergative suffix directly (no leading dash needed it is -e)
let agent_erg: String = agent + "e"
return agent_erg + " " + patient + " " + conjugated
}
// sux_realize_sentence: top-level sentence realizer
//
// Assembles a complete Sumerian sentence from semantic components.
//
// intent: "assert" | "question" | "describe"
// agent: subject noun (base form)
// predicate: verb stem / canonical label (for assert/question) OR
// adjective/noun predicate (for describe / nominal sentence)
// patient: object noun (base form); pass "" for intransitive or nominal sentence
// tense: "present" | "past"
//
// assert (declarative transitive/intransitive)
// Builds the SOV verb chain using sux_verb_chain.
// Ergative marking is applied to the agent when a patient is present.
// Example: lugal-e é mu-un-dù "The king built the house."
//
// question
// Sumerian polar questions are formed by appending the enclitic -a to the
// verb (written -am in some analyses; here "-a" on the verb complex).
// Word order is unchanged (SOV retained).
// Example: lugal-e é mu-un-dù-a? "Did the king build the house?"
//
// describe (nominal/equational sentence)
// Sumerian equational sentences (X is Y) typically omit the copula entirely
// or append -am to the predicate nominal/adjective.
// Form: agent + predicate + "-am3"
// Example: lugal-am3 "He is a king." / lugal kalag-ga-am3 "The king is mighty."
//
// When a patient is provided in describe mode, it is treated as the predicate
// complement: agent + patient-am3.
fn sux_realize_sentence(intent: String, agent: String, predicate: String, patient: String, tense: String) -> String {
if str_eq(intent, "assert") {
return sux_verb_chain(agent, predicate, patient, tense)
}
if str_eq(intent, "question") {
// Build the assertion first, then append the question enclitic -a
let assertion: String = sux_verb_chain(agent, predicate, patient, tense)
return assertion + "-a"
}
if str_eq(intent, "describe") {
// Nominal/equational sentence: agent predicate/patient + copula clitic -am3
// When patient is given it serves as the predicate nominal
if str_eq(patient, "") {
return agent + " " + predicate + "-am3"
}
return agent + " " + patient + "-am3"
}
// Fallback: plain assertion
return sux_verb_chain(agent, predicate, patient, tense)
}
+29
View File
@@ -0,0 +1,29 @@
// auto-generated by elc --emit-header — do not edit
extern fn sux_str_ends(s: String, suf: String) -> Bool
extern fn sux_str_drop_last(s: String, n: Int) -> String
extern fn sux_str_last_char(s: String) -> String
extern fn sux_str_last2(s: String) -> String
extern fn sux_slot(person: String, number: String) -> Int
extern fn sux_ergative_suffix(person: String, number: String) -> String
extern fn sux_absolutive_suffix(person: String, number: String) -> String
extern fn sux_map_canonical(verb: String) -> String
extern fn sux_personal_suffix(slot: Int) -> String
extern fn sux_me_present(slot: Int) -> String
extern fn sux_me_past(slot: Int) -> String
extern fn sux_dug4_present(slot: Int) -> String
extern fn sux_dug4_past(slot: Int) -> String
extern fn sux_du_present(slot: Int) -> String
extern fn sux_du_past(slot: Int) -> String
extern fn sux_igibar_present(slot: Int) -> String
extern fn sux_igibar_past(slot: Int) -> String
extern fn sux_ak_present(slot: Int) -> String
extern fn sux_ak_past(slot: Int) -> String
extern fn sux_tum2_present(slot: Int) -> String
extern fn sux_tum2_past(slot: Int) -> String
extern fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sux_is_animate(noun: String) -> Bool
extern fn sux_case_suffix(gram_case: String) -> String
extern fn sux_decline(noun: String, gram_case: String, number: String) -> String
extern fn sux_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn sux_verb_chain(agent: String, verb: String, patient: String, tense: String) -> String
extern fn sux_realize_sentence(intent: String, agent: String, predicate: String, patient: String, tense: String) -> String
+935
View File
@@ -0,0 +1,935 @@
// morphology-sw.el - Swahili morphology for the NLG engine.
//
// Implements Swahili noun class detection, subject/object agreement prefixes,
// tense markers, verb conjugation, negation, noun pluralization, and adjective
// agreement.
//
// Swahili is an agglutinative SVO language that uses a noun class system
// (15+ classes) instead of grammatical gender. Noun classes determine the
// agreement prefixes on verbs, adjectives, and other nominals.
//
// Key facts:
// - No case endings word order and prepositions handle relations
// - Verb morphology: SUBJ-TENSE-OBJ-STEM(-final vowel)
// - Latin script; tonal but tone unmarked in standard orthography
//
// Conventions used throughout:
// class: "1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"|"11"|"15"
// person: "1sg"|"2sg"|"3sg"|"1pl"|"2pl"|"3pl" | "1"|"2"|"3" + number "sg"/"pl"
// tense: "present"|"progressive"|"past"|"future"|"perfect"
// number: "sg" | "pl"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last)
// String helpers
import "morphology.el"
fn sw_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn sw_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn sw_str_first_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, 0, 1)
}
fn sw_str_first2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, 0, 2)
}
fn sw_str_first3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, 0, 3)
}
fn sw_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Noun class detection
//
// Returns the singular noun class number as a string.
// The noun class is inferred from the noun's prefix according to the standard
// Bantu noun class system used in Swahili.
//
// Class pairs (singular/plural):
// Class 1/2 (M-WA): m-/mw- wa- people, animates
// Class 3/4 (M-MI): m-/mw- mi- trees, plants, objects
// Class 5/6 (JI-MA): ji-/j- ma- fruits, augmentatives
// Class 7/8 (KI-VI): ki-/ch- vi-/vy- things, languages
// Class 9/10 (N): n-/m-/ same animals, loanwords
// Class 11 (U): u-/w- (10) abstract nouns
// Class 15 (KU): ku- infinitives as nouns
//
// Disambiguation between class 1 and class 3 (both m-/mw- prefix):
// Class 1 nouns refer to people/animates; class 3 to inanimate objects/plants.
// A lexical lookup for common class 1 nouns handles the ambiguous cases.
//
// Note: Class 9/10 nouns often have no visible prefix (zero-prefix or nasal
// prefix that assimilates to the following consonant).
fn sw_is_class1_noun(noun: String) -> Bool {
// Known class 1 (M-WA: human/animate) nouns
if str_eq(noun, "mtu") { return true } // person
if str_eq(noun, "mwanafunzi") { return true } // student
if str_eq(noun, "mwalimu") { return true } // teacher
if str_eq(noun, "mke") { return true } // wife/woman
if str_eq(noun, "mume") { return true } // husband/man
if str_eq(noun, "mtoto") { return true } // child
if str_eq(noun, "mgeni") { return true } // guest/stranger
if str_eq(noun, "mwana") { return true } // child/son
if str_eq(noun, "mkubwa") { return true } // elder/adult
if str_eq(noun, "mdogo") { return true } // young one
if str_eq(noun, "mgonjwa") { return true } // sick person/patient
if str_eq(noun, "mfanyakazi") { return true } // worker
if str_eq(noun, "mkulima") { return true } // farmer
if str_eq(noun, "mwimbaji") { return true } // singer
if str_eq(noun, "msomaji") { return true } // reader
if str_eq(noun, "mwandishi") { return true } // writer
if str_eq(noun, "mpiganaji") { return true } // fighter/warrior
if str_eq(noun, "msaidizi") { return true } // helper
if str_eq(noun, "mpishi") { return true } // cook
if str_eq(noun, "mwanasheria") { return true } // lawyer
if str_eq(noun, "daktari") { return true } // doctor (cl.9, but animate)
if str_eq(noun, "rafiki") { return true } // friend (cl.9)
if str_eq(noun, "ndugu") { return true } // sibling/relative (cl.9)
return false
}
fn sw_noun_class(noun: String) -> String {
// Class 15: ku- prefix infinitives used as nouns
if sw_str_ends(noun, "ku") {
if str_eq(sw_str_first2(noun), "ku") { return "15" }
}
if sw_str_first2(noun) == "ku" { return "15" }
// Explicit class 15 check
let p2: String = sw_str_first2(noun)
if str_eq(p2, "ku") { return "15" }
// Class 7/8 (KI-VI): ki- or ch-
let p3: String = sw_str_first3(noun)
if str_eq(p3, "ki-") { return "7" }
if str_eq(p2, "ki") { return "7" }
if str_eq(p2, "ch") { return "7" } // e.g. chakula (food), choo (toilet)
// Class 11 (U): u- or w- prefix abstract nouns
// Must check before class 1/3 m- prefix checks
let p1: String = sw_str_first_char(noun)
if str_eq(p1, "u") { return "11" }
if str_eq(p1, "w") { return "11" } // e.g. wimbo (song) class 11
// Class 5/6 (JI-MA): ji- prefix or zero-prefix fruits/augmentatives
if str_eq(p2, "ji") { return "5" }
// Common class 5 nouns with zero prefix (j- before vowel)
if str_eq(noun, "jicho") { return "5" } // eye
if str_eq(noun, "jino") { return "5" } // tooth
if str_eq(noun, "bega") { return "5" } // shoulder
if str_eq(noun, "tunda") { return "5" } // fruit
if str_eq(noun, "embe") { return "5" } // mango
if str_eq(noun, "gari") { return "5" } // car/vehicle
if str_eq(noun, "bei") { return "5" } // price
if str_eq(noun, "sauti") { return "5" } // voice/sound
if str_eq(noun, "thamani") { return "5" } // value
// Class 1/3 (M- prefix): disambiguate by lexical lookup
if str_eq(p1, "m") {
if sw_is_class1_noun(noun) { return "1" }
return "3" // class 3: trees, plants, inanimate m- nouns
}
if str_eq(p2, "mw") {
if sw_is_class1_noun(noun) { return "1" }
return "3"
}
// Class 9/10 (N): nasal prefix or zero prefix catch-all for most
// loanwords and animal names
// Many class 9 nouns start with: n, ny, ng, mb, nd, nj, nz, or bare vowel
if str_eq(p2, "ny") { return "9" }
if str_eq(p2, "ng") { return "9" }
if str_eq(p2, "mb") { return "9" }
if str_eq(p2, "nd") { return "9" }
if str_eq(p2, "nj") { return "9" }
if str_eq(p2, "nz") { return "9" }
if str_eq(p1, "n") { return "9" }
// Common lexical class 9 nouns (animals, loanwords, etc.)
if str_eq(noun, "paka") { return "9" } // cat
if str_eq(noun, "mbwa") { return "9" } // dog
if str_eq(noun, "simba") { return "9" } // lion
if str_eq(noun, "tembo") { return "9" } // elephant
if str_eq(noun, "nyoka") { return "9" } // snake
if str_eq(noun, "samaki") { return "9" } // fish
if str_eq(noun, "rafiki") { return "9" } // friend
if str_eq(noun, "daktari") { return "9" } // doctor
if str_eq(noun, "serikali") { return "9" } // government
if str_eq(noun, "hospitali") { return "9" } // hospital
if str_eq(noun, "shule") { return "9" } // school
if str_eq(noun, "kanisa") { return "9" } // church
if str_eq(noun, "ofisi") { return "9" } // office
if str_eq(noun, "picha") { return "9" } // picture/photo
if str_eq(noun, "sehemu") { return "9" } // part/section
if str_eq(noun, "habari") { return "9" } // news
if str_eq(noun, "nchi") { return "9" } // country/land
if str_eq(noun, "bahari") { return "9" } // sea/ocean
if str_eq(noun, "dunia") { return "9" } // world
if str_eq(noun, "ardhi") { return "9" } // ground/earth
// Default to class 9 for unknown nouns (most common catch-all)
return "9"
}
// Subject agreement prefixes
//
// Returns the subject prefix for verb conjugation.
//
// For personal pronouns: person "1","2","3" + number "sg","pl"
// For noun class agreement: person "3" + the noun's class
//
// Personal:
// 1sg (mimi) ni-
// 2sg (wewe) u-
// 3sg class 1 a-
// 1pl (sisi) tu-
// 2pl (nyinyi) m-
// 3pl class 2 wa-
//
// Noun class 3sg:
// cl.1 a- cl.2 wa-
// cl.3 u- cl.4 i-
// cl.5 li- cl.6 ya-
// cl.7 ki- cl.8 vi-
// cl.9 i- cl.10 zi-
// cl.11 u- cl.15 ku-
fn sw_subj_prefix(person: String, number: String, noun_class: String) -> String {
// First and second person always use personal prefixes regardless of noun class
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "u" }
return "m"
}
// Third person: agree with noun class
// Plural classes
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" } // class 2 (wa-)
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "i" } // class 4 (i-)
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "ya" } // class 6 (ya-)
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "vi" } // class 8 (vi-)
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "zi" } // class 10 (zi-)
if str_eq(noun_class, "10") { return "zi" }
if str_eq(noun_class, "11") { return "zi" } // class 11 pl uses cl.10
return "zi"
}
// Singular classes
if str_eq(noun_class, "1") { return "a" }
if str_eq(noun_class, "3") { return "u" }
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "li" }
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "i" }
if str_eq(noun_class, "10") { return "zi" }
if str_eq(noun_class, "11") { return "u" }
if str_eq(noun_class, "15") { return "ku" }
// Default: third person animate sg
return "a"
}
// Object agreement prefixes
//
// Object infixes are inserted between tense marker and verb stem.
// They mirror the subject prefixes with minor differences.
//
// 1sg ni- 2sg ku- 3sg cl.1 m-/mw-
// 1pl tu- 2pl wa- 3pl cl.2 wa-
// cl.3 sg u- cl.4 pl i-
// cl.5 sg li- cl.6 pl ya-
// cl.7 sg ki- cl.8 pl vi-
// cl.9 sg i- cl.10 pl zi-
// cl.11 u- cl.15 ku-
fn sw_obj_prefix(person: String, number: String, noun_class: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "ku" }
return "wa"
}
// Third person object
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" }
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "i" }
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "ya" }
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "vi" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "zi" }
if str_eq(noun_class, "10") { return "zi" }
return "wa"
}
// Singular
if str_eq(noun_class, "1") { return "m" }
if str_eq(noun_class, "3") { return "u" }
if str_eq(noun_class, "5") { return "li" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "9") { return "i" }
if str_eq(noun_class, "11") { return "u" }
if str_eq(noun_class, "15") { return "ku" }
return "m"
}
// Tense markers
//
// Tense markers are inserted between the subject prefix and the verb stem.
//
// "present" -a- (habitual/simple present; final vowel -a)
// "progressive" -na- (present progressive)
// "past" -li- (simple past)
// "future" -ta- (future)
// "perfect" -me- (present perfect)
// "remote_past" -li- (same marker; distinction via context or -ku-)
// "subjunctive" (no tense marker; final vowel changes to -e)
fn sw_tense_marker(tense: String) -> String {
if str_eq(tense, "present") { return "a" }
if str_eq(tense, "progressive") { return "na" }
if str_eq(tense, "past") { return "li" }
if str_eq(tense, "future") { return "ta" }
if str_eq(tense, "perfect") { return "me" }
if str_eq(tense, "subjunctive") { return "" }
if str_eq(tense, "remote_past") { return "li" }
// Default to progressive marker
return "na"
}
// Final vowel (verb ending)
//
// The final vowel of the verb stem changes based on tense and negation.
//
// Positive forms:
// present habitual / progressive / past / future / perfect: -a
// subjunctive: -e
//
// Negative forms:
// present: -i (the negative present uses -i instead of -a)
// past: -a (negative past: ha- prefix + -ku- tense + stem + -a)
// others: -e (negative subjunctive)
//
// Note: For most tenses the final vowel is simply -a. Only the negative present
// and subjunctive change it.
fn sw_verb_final(tense: String, negative: Bool) -> String {
if negative {
if str_eq(tense, "present") { return "i" }
if str_eq(tense, "progressive") { return "i" }
if str_eq(tense, "subjunctive") { return "e" }
return "a"
}
if str_eq(tense, "subjunctive") { return "e" }
return "a"
}
// Negative subject prefix
//
// Negation in Swahili is expressed by replacing the subject prefix with a
// negative counterpart (ha- for most; si- for 1sg).
//
// Positive Negative subject prefixes:
// ni- si- (1sg)
// u- hu- (2sg)
// a- ha- (3sg cl.1)
// tu- hatu- (1pl)
// m- ham- (2pl)
// wa- hawa- (3pl cl.2)
// other class 3sg prefixes: ha + prefix (e.g. ha+u = hau, ha+li = hali)
fn sw_neg_subj_prefix(person: String, number: String, noun_class: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "si" }
return "hatu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "hu" }
return "ham"
}
// Third person negative = ha + positive subject prefix
let pos: String = sw_subj_prefix(person, number, noun_class)
return "ha" + pos
}
// Stem extraction
//
// Swahili infinitives begin with ku-. The verb stem is the infinitive minus ku-.
// For verbs where the stem itself starts with a vowel, the ku- is retained in
// some contexts (kuomba, kusoma etc.) but stripped for conjugation.
//
// Special cases:
// kula (to eat) stem: l- (kula la in conjugation: anakula)
// kuwa (to be) stem: wa (but equatorial forms are suppletive: ni/u/ni)
// kwenda (to go) stem: end (kw + end enda)
// kuja (to come) stem: ja
fn sw_verb_stem(infinitive: String) -> String {
if str_eq(infinitive, "kula") { return "l" } // eat irregular: anakula
if str_eq(infinitive, "kuwa") { return "wa" } // be
if str_eq(infinitive, "kwenda") { return "enda" }
if str_eq(infinitive, "kuja") { return "ja" }
if str_eq(infinitive, "kusoma") { return "soma" }
if str_eq(infinitive, "kusema") { return "sema" }
if str_eq(infinitive, "kuona") { return "ona" }
if str_eq(infinitive, "kufanya") { return "fanya" }
if str_eq(infinitive, "kutaka") { return "taka" }
if str_eq(infinitive, "kujua") { return "jua" }
if str_eq(infinitive, "kupata") { return "pata" }
if str_eq(infinitive, "kuambia") { return "ambia" }
if str_eq(infinitive, "kuleta") { return "leta" }
if str_eq(infinitive, "kuweka") { return "weka" }
if str_eq(infinitive, "kuingia") { return "ingia" }
if str_eq(infinitive, "kutoka") { return "toka" }
if str_eq(infinitive, "kupiga") { return "piga" }
if str_eq(infinitive, "kuimba") { return "imba" }
if str_eq(infinitive, "kucheza") { return "cheza" }
if str_eq(infinitive, "kulala") { return "lala" }
if str_eq(infinitive, "kuandika") { return "andika" }
if str_eq(infinitive, "kununua") { return "nunua" }
if str_eq(infinitive, "kuuza") { return "uza" }
if str_eq(infinitive, "kupenda") { return "penda" }
if str_eq(infinitive, "kuchukua") { return "chukua" }
if str_eq(infinitive, "kulipa") { return "lipa" }
if str_eq(infinitive, "kusikia") { return "sikia" }
if str_eq(infinitive, "kuamka") { return "amka" }
if str_eq(infinitive, "kukaa") { return "kaa" }
if str_eq(infinitive, "kurudi") { return "rudi" }
if str_eq(infinitive, "kushinda") { return "shinda" }
if str_eq(infinitive, "kusaidia") { return "saidia" }
if str_eq(infinitive, "kuzungumza") { return "zungumza" }
if str_eq(infinitive, "kupumzika") { return "pumzika" }
if str_eq(infinitive, "kufika") { return "fika" }
if str_eq(infinitive, "kuomba") { return "omba" }
if str_eq(infinitive, "kushukuru") { return "shukuru" }
// Generic: strip leading ku (2 chars) or kw (for kwenda etc.)
if sw_str_first2(infinitive) == "ku" {
return str_slice(infinitive, 2, str_len(infinitive))
}
if sw_str_first2(infinitive) == "kw" {
return str_slice(infinitive, 2, str_len(infinitive))
}
return infinitive
}
// Positive verb conjugation
//
// Structure: SUBJ_PREFIX + TENSE_MARKER + STEM + FINAL_VOWEL
//
// Examples:
// anasoma = a (3sg cl.1) + na (prog) + soma (read) + [stem ends -a]
// nilikula = ni (1sg) + li (past) + ku (special) + la (kula stem)
// tutasema = tu (1pl) + ta (future) + sema (say) + [stem ends -a]
// amesoma = a (3sg) + me (perfect) + soma + [final -a]
//
// Special case: kula the stem is bare "l", and the full conjugation inserts
// ku before the stem in most tenses: a-na-ku-la, a-li-ku-la, a-ta-ku-la
// but a-me-l-a is not standard standard is amekula.
fn sw_conjugate(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
let subj: String = sw_subj_prefix(person, number, noun_class)
let tm: String = sw_tense_marker(tense)
let fv: String = sw_verb_final(tense, false)
// kula (eat) stem is "l" but conjugates as "kula" after prefix+tense
if str_eq(verb_stem, "l") {
if str_eq(tm, "") {
return subj + "kula"
}
return subj + tm + "kula"
}
// kuwa (be) present equational forms are suppletive
if str_eq(verb_stem, "wa") {
if str_eq(tense, "present") {
// Equational: ni/u/ni for 1sg/2sg/3sg; tu/m/ni for pl
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu ni"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "u" }
return "m ni"
}
// 3sg/3pl: use class-based copula
if str_eq(number, "sg") {
// Locative/existential forms: niko, uko, yuko etc. (class-based)
if str_eq(noun_class, "1") { return "yuko" }
if str_eq(noun_class, "3") { return "upo" }
if str_eq(noun_class, "5") { return "lipo" }
if str_eq(noun_class, "7") { return "kipo" }
if str_eq(noun_class, "9") { return "ipo" }
if str_eq(noun_class, "11") { return "upo" }
return "yuko"
}
// plural
if str_eq(noun_class, "1") { return "wako" }
if str_eq(noun_class, "3") { return "ipo" }
if str_eq(noun_class, "5") { return "yapo" }
if str_eq(noun_class, "7") { return "vipo" }
if str_eq(noun_class, "9") { return "zipo" }
return "wako"
}
// Progressive kuwa: niko/uko/yuko (existence/location)
if str_eq(tense, "progressive") {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "niko" }
return "tuko"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "uko" }
return "mko"
}
if str_eq(number, "sg") {
if str_eq(noun_class, "1") { return "yuko" }
return subj + "ko"
}
if str_eq(noun_class, "1") { return "wako" }
return subj + "ko"
}
// Past/future/perfect kuwa: regular
}
// Regular conjugation
let stem_final: String = sw_str_last_char(verb_stem)
// If stem already ends in the correct final vowel, don't double it
if str_eq(fv, "a") {
if str_eq(stem_final, "a") {
// Stem ends in -a: result is subj+tm+stem (final -a already present)
if str_eq(tm, "") {
return subj + verb_stem
}
return subj + tm + verb_stem
}
}
if str_eq(tm, "") {
return subj + verb_stem + fv
}
return subj + tm + verb_stem + fv
}
// Negative verb conjugation
//
// Negative structure: NEG_SUBJ_PREFIX + (tense) + STEM + FINAL_VOWEL_I
//
// Negative present:
// ha- prefix replaces subject prefix; final vowel -i
// e.g. hasomi (he/she doesn't read), sisomi (I don't read)
//
// Negative past:
// ha- prefix + -ku- tense infix + stem + -a
// e.g. hakusoma (he/she didn't read), sikusoma (I didn't read)
//
// Negative future:
// ha- prefix + -ta- + stem + -i (or sometimes -a; -i is more standard)
// e.g. hatasoma (he/she won't read)
//
// Negative perfect:
// ha- prefix + -ja- (not yet) + stem + -a
// e.g. hajaenda (he/she hasn't gone yet)
fn sw_negative(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
let neg_subj: String = sw_neg_subj_prefix(person, number, noun_class)
// kula special gram_case
if str_eq(verb_stem, "l") {
if str_eq(tense, "past") { return neg_subj + "kukula" }
if str_eq(tense, "perfect") { return neg_subj + "jakula" }
return neg_subj + "kuli"
}
if str_eq(tense, "present") {
let fv: String = sw_verb_final("present", true) // -i
let stem_no_a: String = verb_stem
// If stem ends in -a, drop it before adding -i (some grammars)
// Standard: ha + stem-minus-final-a + i
let slen: Int = str_len(verb_stem)
if slen > 0 {
let last: String = sw_str_last_char(verb_stem)
if str_eq(last, "a") {
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
}
}
return neg_subj + verb_stem + fv
}
if str_eq(tense, "past") {
// Negative past: ha + ku + stem + a
return neg_subj + "ku" + verb_stem + "a"
}
if str_eq(tense, "future") {
// Negative future: ha + ta + stem + i (standard)
let fv: String = sw_verb_final("present", true) // -i
return neg_subj + "ta" + verb_stem + fv
}
if str_eq(tense, "perfect") {
// Negative perfect "not yet": ha + ja + stem + a
return neg_subj + "ja" + verb_stem + "a"
}
if str_eq(tense, "progressive") {
// Negative progressive: same as negative present
let fv: String = sw_verb_final("present", true)
let slen: Int = str_len(verb_stem)
if slen > 0 {
let last: String = sw_str_last_char(verb_stem)
if str_eq(last, "a") {
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
}
}
return neg_subj + verb_stem + fv
}
// Default negative: ha + stem + i
return neg_subj + verb_stem + "i"
}
// Noun plural formation
//
// Swahili plurals are formed by changing the noun class prefix.
//
// Class 1 (m-/mw-) Class 2 (wa-): mtu watu, mwalimu walimu
// Class 3 (m-/mw-) Class 4 (mi-): mti miti, mwamba miamba
// Class 5 (ji-/) Class 6 (ma-): jicho macho, tunda matunda
// Class 7 (ki-/ch-) Class 8 (vi-/vy-): kitu vitu, chakula vyakula
// Class 9 (n-/) Class 10 (n-/): same prefix no change in prefix;
// some class 9 nouns add ma- (ma- class) in plural
// Class 11 (u-) Class 10 (n-/): ubao mbao
//
// Known lexical exceptions are handled first.
fn sw_noun_plural(noun: String) -> String {
// Lexical irregulars
if str_eq(noun, "mtu") { return "watu" }
if str_eq(noun, "mtoto") { return "watoto" }
if str_eq(noun, "mke") { return "wake" }
if str_eq(noun, "mume") { return "waume" }
if str_eq(noun, "mwana") { return "wana" }
if str_eq(noun, "mwalimu") { return "walimu" }
if str_eq(noun, "mgeni") { return "wageni" }
if str_eq(noun, "mwanafunzi") { return "wanafunzi" }
if str_eq(noun, "mfanyakazi") { return "wafanyakazi" }
if str_eq(noun, "mkulima") { return "wakulima" }
if str_eq(noun, "mgonjwa") { return "wagonjwa" }
if str_eq(noun, "jicho") { return "macho" }
if str_eq(noun, "jino") { return "meno" }
if str_eq(noun, "bega") { return "mabega" }
if str_eq(noun, "tunda") { return "matunda" }
if str_eq(noun, "gari") { return "magari" }
if str_eq(noun, "embe") { return "maembe" }
if str_eq(noun, "wimbo") { return "nyimbo" } // class 11 10
if str_eq(noun, "ubao") { return "mbao" }
if str_eq(noun, "ugonjwa") { return "magonjwa" }
if str_eq(noun, "uso") { return "nyuso" }
if str_eq(noun, "ukuta") { return "kuta" }
if str_eq(noun, "ulimi") { return "ndimi" }
if str_eq(noun, "upande") { return "pande" }
if str_eq(noun, "uwezo") { return "nguvu" } // approximate
// Class 9/10 nouns: same form sg/pl (zero-change class)
if str_eq(noun, "paka") { return "paka" }
if str_eq(noun, "samaki") { return "samaki" }
if str_eq(noun, "rafiki") { return "rafiki" }
if str_eq(noun, "daktari") { return "madaktari" } // some use ma- pl
if str_eq(noun, "habari") { return "habari" }
if str_eq(noun, "nchi") { return "nchi" }
if str_eq(noun, "bahari") { return "bahari" }
if str_eq(noun, "shule") { return "shule" }
if str_eq(noun, "hospitali") { return "hospitali" }
if str_eq(noun, "ofisi") { return "ofisi" }
if str_eq(noun, "serikali") { return "serikali" }
// Productive rules
// Class 1 (m- people): m- wa-
if sw_is_class1_noun(noun) {
if str_eq(sw_str_first2(noun), "mw") {
return "wa" + str_slice(noun, 2, str_len(noun))
}
if str_eq(sw_str_first_char(noun), "m") {
return "wa" + str_slice(noun, 1, str_len(noun))
}
}
// Class 7 (ki-/ch-) Class 8 (vi-/vy-)
let p2: String = sw_str_first2(noun)
if str_eq(p2, "ki") {
return "vi" + str_slice(noun, 2, str_len(noun))
}
if str_eq(p2, "ch") {
return "vy" + str_slice(noun, 2, str_len(noun))
}
// Class 5 (ji-) Class 6 (ma-)
if str_eq(p2, "ji") {
return "ma" + str_slice(noun, 2, str_len(noun))
}
// Class 11 (u-/w-) Class 10 (n-/): drop u, may add n
let p1: String = sw_str_first_char(noun)
if str_eq(p1, "u") {
// Most u- class nouns: drop u bare stem (class 10 zero prefix)
return str_slice(noun, 1, str_len(noun))
}
// Class 3 (m-/mw- inanimate) Class 4 (mi-)
if str_eq(p1, "m") {
if str_eq(p2, "mw") {
return "mi" + str_slice(noun, 2, str_len(noun))
}
return "mi" + str_slice(noun, 1, str_len(noun))
}
// Class 9/10: no prefix change (same form)
return noun
}
// Adjective agreement
//
// Adjectives in Swahili take a class agreement prefix that matches the noun
// they modify. The prefix is placed before the adjective stem.
//
// Agreement prefixes (singular):
// cl.1 m-/mw- cl.2 wa-
// cl.3 m-/mw- cl.4 mi-
// cl.5 (j-)l- cl.6 ma-
// cl.7 ki-/ch- cl.8 vi-/vy-
// cl.9 (n-) cl.10 (n-)
// cl.11 mw- cl.15 ku-
//
// The adjective prefix for class 1/3 singular before a vowel-initial stem
// uses mw- instead of m-.
//
// number: "sg" | "pl"
// noun_class: singular class of the noun (the function applies the correct
// plural class internally when number == "pl")
fn sw_adj_prefix(noun_class: String, number: String) -> String {
// Plural classes
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" } // cl.2
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "mi" } // cl.4
if str_eq(noun_class, "4") { return "mi" }
if str_eq(noun_class, "5") { return "ma" } // cl.6
if str_eq(noun_class, "6") { return "ma" }
if str_eq(noun_class, "7") { return "vi" } // cl.8
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "n" } // cl.10 (nasal)
if str_eq(noun_class, "10") { return "n" }
if str_eq(noun_class, "11") { return "n" } // cl.10 in plural
return "wa"
}
// Singular classes
if str_eq(noun_class, "1") { return "m" }
if str_eq(noun_class, "3") { return "m" }
if str_eq(noun_class, "4") { return "mi" }
if str_eq(noun_class, "5") { return "j" } // l- before consonant, j- before vowel
if str_eq(noun_class, "6") { return "ma" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "n" }
if str_eq(noun_class, "10") { return "n" }
if str_eq(noun_class, "11") { return "mw" }
if str_eq(noun_class, "15") { return "ku" }
return ""
}
// sw_agree_adj: apply the agreement prefix to an adjective stem.
//
// adj_stem: the adjective without any class prefix (e.g. "zuri", "kubwa", "dogo")
// noun_class: class of the noun being modified (singular class number as string)
// number: "sg" | "pl"
//
// Returns the fully prefixed adjective form.
//
// Note: many common adjectives in Swahili are Arabic/Bantu borrowings and do
// not take class prefixes (e.g. "nzuri" beautiful, "kubwa" big, "dogo" small).
// Prefix agreement applies primarily to native Bantu adjective stems and
// demonstratives. This function applies the prefix mechanically; for adjectives
// that are invariant loanwords the caller should use the bare form.
fn sw_agree_adj(adj_stem: String, noun_class: String, number: String) -> String {
// Invariant adjectives (no class prefix required in standard usage)
if str_eq(adj_stem, "nzuri") { return "nzuri" } // good/beautiful
if str_eq(adj_stem, "baya") { return "baya" } // bad
if str_eq(adj_stem, "safi") { return "safi" } // clean
if str_eq(adj_stem, "chafu") { return "chafu" } // dirty
if str_eq(adj_stem, "ghali") { return "ghali" } // expensive
if str_eq(adj_stem, "rahisi") { return "rahisi" } // cheap/easy
if str_eq(adj_stem, "mzuri") { return sw_adj_prefix(noun_class, number) + "zuri" }
// Apply class prefix to Bantu adjective stems
let prefix: String = sw_adj_prefix(noun_class, number)
if str_eq(prefix, "") {
return adj_stem
}
// cl.1/3 prefix m- before consonant, mw- before vowel
if str_eq(prefix, "m") {
let first: String = sw_str_first_char(adj_stem)
if str_eq(first, "a") { return "mw" + adj_stem }
if str_eq(first, "e") { return "mw" + adj_stem }
if str_eq(first, "i") { return "mw" + adj_stem }
if str_eq(first, "o") { return "mw" + adj_stem }
if str_eq(first, "u") { return "mw" + adj_stem }
return "m" + adj_stem
}
// cl.5 prefix j- before vowel, l- before consonant
if str_eq(prefix, "j") {
let first: String = sw_str_first_char(adj_stem)
if str_eq(first, "a") { return "j" + adj_stem }
if str_eq(first, "e") { return "j" + adj_stem }
if str_eq(first, "i") { return "j" + adj_stem }
if str_eq(first, "o") { return "j" + adj_stem }
if str_eq(first, "u") { return "j" + adj_stem }
return "l" + adj_stem
}
return prefix + adj_stem
}
// Demonstrative agreement
//
// Swahili demonstratives agree with the noun class.
// "this" (proximal): huyu (cl.1/2), huu (cl.3/11), hili (cl.5), hiki (cl.7),
// hii (cl.9), huu (cl.11)
// "that" (distal): yule (cl.1/2), ule (cl.3/11), lile (cl.5), kile (cl.7),
// ile (cl.9)
//
// proximity: "near" | "far"
fn sw_demonstrative(noun_class: String, number: String, proximity: String) -> String {
if str_eq(proximity, "near") {
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "hawa" }
if str_eq(noun_class, "3") { return "hii" }
if str_eq(noun_class, "5") { return "haya" }
if str_eq(noun_class, "7") { return "hivi" }
if str_eq(noun_class, "9") { return "hizi" }
return "hawa"
}
if str_eq(noun_class, "1") { return "huyu" }
if str_eq(noun_class, "3") { return "huu" }
if str_eq(noun_class, "5") { return "hili" }
if str_eq(noun_class, "7") { return "hiki" }
if str_eq(noun_class, "9") { return "hii" }
if str_eq(noun_class, "11") { return "huu" }
if str_eq(noun_class, "15") { return "huku" }
return "hii"
}
// distal
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wale" }
if str_eq(noun_class, "3") { return "ile" }
if str_eq(noun_class, "5") { return "yale" }
if str_eq(noun_class, "7") { return "vile" }
if str_eq(noun_class, "9") { return "zile" }
return "wale"
}
if str_eq(noun_class, "1") { return "yule" }
if str_eq(noun_class, "3") { return "ule" }
if str_eq(noun_class, "5") { return "lile" }
if str_eq(noun_class, "7") { return "kile" }
if str_eq(noun_class, "9") { return "ile" }
if str_eq(noun_class, "11") { return "ule" }
if str_eq(noun_class, "15") { return "kule" }
return "ile"
}
// Swahili copula (kuwa to be)
//
// The copula in Swahili is complex:
//
// Equational ("X is Y", noun predicate):
// ni (is/am/are) invariant for present equational
// si negative present equational
//
// Locative/existential ("X is [somewhere]", with -ko/-po/-mo):
// niko/uko/yuko/tuko/mko/wako with -ko (definite location)
// nipo/upo/yupo/tupo/mpo/wapo with -po (near location)
// nimo/umo/yumo/tumo/mmo/wamo with -mo (inside)
//
// sw_copula_present: returns the present copula form.
// use_case: "equative" | "locative"
fn sw_copula_present(person: String, number: String, use_case: String) -> String {
if str_eq(use_case, "equative") {
if str_eq(person, "1") { return "ni" }
if str_eq(person, "2") { return "ni" }
return "ni"
}
// locative -ko (most common)
if str_eq(person, "1") {
if str_eq(number, "sg") { return "niko" }
return "tuko"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "uko" }
return "mko"
}
if str_eq(number, "sg") { return "yuko" }
return "wako"
}
// sw_copula_neg_present: negative present copula.
// equative: si (invariant for all persons in negative equational)
fn sw_copula_neg_present(person: String, number: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "si" }
return "si"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "si" }
return "si"
}
return "si"
}
+23
View File
@@ -0,0 +1,23 @@
// auto-generated by elc --emit-header — do not edit
extern fn sw_str_ends(s: String, suf: String) -> Bool
extern fn sw_str_drop_last(s: String, n: Int) -> String
extern fn sw_str_first_char(s: String) -> String
extern fn sw_str_first2(s: String) -> String
extern fn sw_str_first3(s: String) -> String
extern fn sw_str_last_char(s: String) -> String
extern fn sw_is_class1_noun(noun: String) -> Bool
extern fn sw_noun_class(noun: String) -> String
extern fn sw_subj_prefix(person: String, number: String, noun_class: String) -> String
extern fn sw_obj_prefix(person: String, number: String, noun_class: String) -> String
extern fn sw_tense_marker(tense: String) -> String
extern fn sw_verb_final(tense: String, negative: Bool) -> String
extern fn sw_neg_subj_prefix(person: String, number: String, noun_class: String) -> String
extern fn sw_verb_stem(infinitive: String) -> String
extern fn sw_conjugate(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String
extern fn sw_negative(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String
extern fn sw_noun_plural(noun: String) -> String
extern fn sw_adj_prefix(noun_class: String, number: String) -> String
extern fn sw_agree_adj(adj_stem: String, noun_class: String, number: String) -> String
extern fn sw_demonstrative(noun_class: String, number: String, proximity: String) -> String
extern fn sw_copula_present(person: String, number: String, use_case: String) -> String
extern fn sw_copula_neg_present(person: String, number: String) -> String
+309
View File
@@ -0,0 +1,309 @@
// morphology-txb.el - Tocharian B morphology for the NLG engine.
//
// Implements Tocharian B verb conjugation and noun declension for the ca. 500-1000 CE
// period. Designed as a companion to morphology.el and called by the engine when
// the language profile code is "txb".
//
// Language profile: code=txb, name=Tocharian B, morph_type=fusional, word_order=SOV,
// question_strategy=particle, script=latin, family=tocharian.
//
// Tocharian B is an extinct Indo-European language attested in the Tarim Basin
// (modern Xinjiang, China). Most surviving texts are Buddhist in content.
// The transliteration used here follows standard scholarly convention (Sieg, Siegling,
// Winter); subscript dots are omitted in identifiers but retained in string literals.
//
// Verb conjugation covered:
// Tenses: present (class I endings), imperfect not implemented
// Persons: first/second/third x singular/plural (slots 0-5)
// Irregulars: käm- (to come), yä- (to go), wes-/ste (to be),
// lyut- (to see), wak- (to speak)
// Canonical map: "be" -> "ste" (3sg) / "wes" (other)
//
// Noun declension covered:
// Masculine o-stem: nom/acc/gen/dat x sg/pl
// Feminine ā-stem: nom/acc/gen/dat x sg/pl
// (The full 8-case paradigm is simplified to 4 cases for the engine.)
// Number: singular, plural
// Articles: none Tocharian B has no article system
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn txb_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn txb_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
// Person/number slot
//
// Maps person x number to a 0-based paradigm slot.
// 0 = 1st singular
// 1 = 2nd singular
// 2 = 3rd singular (most frequently attested)
// 3 = 1st plural
// 4 = 2nd plural
// 5 = 3rd plural
fn txb_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Present class I endings
//
// Tocharian B present class I (most regular verbs). The full paradigm is
// complex with active/medio-passive distinction; only the active series is
// implemented here.
//
// Active present class I endings applied to stem:
// Sg: 1st -au 2nd -ät 3rd -em
// Pl: 1st -emane 2nd -em 3rd -em
//
// Note: the plural endings show extensive syncretism in the attested corpus.
fn txb_pres1_suffix(slot: Int) -> String {
if slot == 0 { return "au" }
if slot == 1 { return "ät" }
if slot == 2 { return "em" }
if slot == 3 { return "emane" }
if slot == 4 { return "em" }
return "em"
}
// Irregular verb tables
// käm- (to come) suppletive present paradigm
// kam, käm, käm, kamnäṃ, kamnäṃ, kamnäṃ
fn txb_kam_present(slot: Int) -> String {
if slot == 0 { return "kam" }
if slot == 1 { return "käm" }
if slot == 2 { return "käm" }
if slot == 3 { return "kamnäṃ" }
if slot == 4 { return "kamnäṃ" }
return "kamnäṃ"
}
// yä- (to go) class IV verb; short stem
// yau, yät, yäm, ymäṃ, ymäṃ, yänmäṃ
fn txb_ya_present(slot: Int) -> String {
if slot == 0 { return "yau" }
if slot == 1 { return "yät" }
if slot == 2 { return "yäm" }
if slot == 3 { return "ymäṃ" }
if slot == 4 { return "ymäṃ" }
return "yänmäṃ"
}
// wes- / ste (to be) the copula/existential "be" verb
// 3sg form "ste" is by far the most attested; elsewhere "wes" is used.
// Simplified paradigm: "ste" for 3sg, "wes" for all other slots.
fn txb_wes_present(slot: Int) -> String {
if slot == 2 { return "ste" }
return "wes"
}
// lyut- (to see) class I regular stem, fully attested
// lyutau, lyutät, lyutem, lyutemane, lyutem, lyutem
fn txb_lyut_present(slot: Int) -> String {
if slot == 0 { return "lyutau" }
if slot == 1 { return "lyutät" }
if slot == 2 { return "lyutem" }
if slot == 3 { return "lyutemane" }
if slot == 4 { return "lyutem" }
return "lyutem"
}
// wak- (to speak) class I regular stem
// wakau, wakät, wakem, wakemane, wakem, wakem
fn txb_wak_present(slot: Int) -> String {
if slot == 0 { return "wakau" }
if slot == 1 { return "wakät" }
if slot == 2 { return "wakem" }
if slot == 3 { return "wakemane" }
if slot == 4 { return "wakem" }
return "wakem"
}
// Canonical verb mapping
//
// Maps English semantic labels to Tocharian B citation forms (verbal stems).
fn txb_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wes" }
if str_eq(verb, "come") { return "käm" }
if str_eq(verb, "go") { return "" }
if str_eq(verb, "see") { return "lyut" }
if str_eq(verb, "speak") { return "wak" }
if str_eq(verb, "say") { return "wak" }
return verb
}
// txb_conjugate: main conjugation entry point
//
// verb: Tocharian B stem or English canonical label
// tense: "present" (only present is implemented; others return base form)
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. For unknown verbs the stem is returned as-is
// the corpus is small enough that most productive verbs are known to the engine.
fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = txb_map_canonical(verb)
let slot: Int = txb_slot(person, number)
// wes-/ste (to be)
if str_eq(v, "wes") {
if str_eq(tense, "present") { return txb_wes_present(slot) }
return v
}
// käm- (to come)
if str_eq(v, "käm") {
if str_eq(tense, "present") { return txb_kam_present(slot) }
return v
}
// yä- (to go)
if str_eq(v, "") {
if str_eq(tense, "present") { return txb_ya_present(slot) }
return v
}
// lyut- (to see)
if str_eq(v, "lyut") {
if str_eq(tense, "present") { return txb_lyut_present(slot) }
return v
}
// wak- (to speak)
if str_eq(v, "wak") {
if str_eq(tense, "present") { return txb_wak_present(slot) }
return v
}
// Regular class I verb
//
// Apply present class I endings directly to the stem.
if str_eq(tense, "present") { return v + txb_pres1_suffix(slot) }
// Unknown tense: return base form
return v
}
// Masculine o-stem declension
//
// Tocharian B masculine o-stem endings (simplified 4-case system):
// Singular: nom -e, acc -e, gen -entse, dat -ene
// Plural: nom -i, acc -i, gen -entwetse, dat -ene
//
// The o-stem masculine is the most common nominal class. The nom/acc syncretism
// in the singular is a characteristic Tocharian B feature.
fn txb_decline_masc(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun + "e" }
if str_eq(gram_case, "accusative") { return noun + "e" }
if str_eq(gram_case, "genitive") { return noun + "entse" }
if str_eq(gram_case, "dative") { return noun + "ene" }
return noun + "e"
}
// plural
if str_eq(gram_case, "nominative") { return noun + "i" }
if str_eq(gram_case, "accusative") { return noun + "i" }
if str_eq(gram_case, "genitive") { return noun + "entwetse" }
if str_eq(gram_case, "dative") { return noun + "ene" }
return noun + "i"
}
// Feminine ā-stem declension
//
// Tocharian B feminine ā-stem endings (simplified 4-case system):
// Singular: nom -a, acc -a, gen -antse, dat -ane
// Plural: nom -ä, acc -ä, gen -antse, dat -ane
//
// The ā-stem feminine shows the same nom/acc syncretism as the masculine.
fn txb_decline_fem(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun + "a" }
if str_eq(gram_case, "accusative") { return noun + "a" }
if str_eq(gram_case, "genitive") { return noun + "antse" }
if str_eq(gram_case, "dative") { return noun + "ane" }
return noun + "a"
}
// plural
if str_eq(gram_case, "nominative") { return noun + "ä" }
if str_eq(gram_case, "accusative") { return noun + "ä" }
if str_eq(gram_case, "genitive") { return noun + "antse" }
if str_eq(gram_case, "dative") { return noun + "ane" }
return noun + "ä"
}
// Gender detection heuristic
//
// In Tocharian B the gender of a noun must ideally be supplied by the lexicon.
// Gender was originally inherited from Proto-Indo-European but many neuters
// merged into masculine. This heuristic defaults to masculine; known feminine
// items should be supplied via the vocabulary layer.
fn txb_detect_gender(noun: String) -> String {
// No reliable phonological gender markers in Tocharian B stems.
// Default: masculine.
return "masculine"
}
// txb_decline: main declension entry point
//
// noun: Tocharian B noun stem (uninflected base form)
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the bare stem on unknown input.
fn txb_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = txb_detect_gender(noun)
if str_eq(gender, "feminine") { return txb_decline_fem(noun, gram_case, number) }
// masculine (default)
return txb_decline_masc(noun, gram_case, number)
}
// txb_noun_phrase: noun phrase builder
//
// Tocharian B has no article system. Definiteness was not grammatically marked
// by a separate morpheme context and word order served that function.
// This function therefore ignores the "definite" parameter and returns the
// declined noun form directly.
//
// noun: Tocharian B noun stem
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false" (ignored no articles in Tocharian B)
//
// Returns the declined noun form.
fn txb_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return txb_decline(noun, gram_case, number)
}
+17
View File
@@ -0,0 +1,17 @@
// auto-generated by elc --emit-header — do not edit
extern fn txb_drop(s: String, n: Int) -> String
extern fn txb_ends(s: String, suf: String) -> Bool
extern fn txb_slot(person: String, number: String) -> Int
extern fn txb_pres1_suffix(slot: Int) -> String
extern fn txb_kam_present(slot: Int) -> String
extern fn txb_ya_present(slot: Int) -> String
extern fn txb_wes_present(slot: Int) -> String
extern fn txb_lyut_present(slot: Int) -> String
extern fn txb_wak_present(slot: Int) -> String
extern fn txb_map_canonical(verb: String) -> String
extern fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn txb_decline_masc(noun: String, gram_case: String, number: String) -> String
extern fn txb_decline_fem(noun: String, gram_case: String, number: String) -> String
extern fn txb_detect_gender(noun: String) -> String
extern fn txb_decline(noun: String, gram_case: String, number: String) -> String
extern fn txb_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
+421
View File
@@ -0,0 +1,421 @@
// morphology-uga.el - Ugaritic morphology for the NLG engine.
// 𐎀𐎎𐎗𐎚 Ugaritic, the language of the city-state of Ugarit.
//
// Implements Ugaritic verb conjugation (G-stem, suffix and prefix conjugations),
// noun declension (three cases, gender, number, dual), and noun-phrase construction.
//
// Ugaritic is a Northwest Semitic language (ca. 14001200 BCE), written in a
// 30-character alphabetic cuneiform. It is closely related to Biblical Hebrew
// and Phoenician. We work in standard Latin transliteration:
// ʼ = aleph (glottal stop) ʻ = ʿayin = het ġ = ghayin
// = thet = emphatic z š = shin = tsade
//
// Language profile:
// code=uga, name=Ugaritic, morph_type=semitic, word_order=VSO,
// script=alphabetic cuneiform (transliterated), family=semitic/northwest-semitic
//
// Key grammatical facts:
// - Semitic trilateral root system (nearly identical to Hebrew/Arabic)
// - Grammatical gender: masculine / feminine
// - Cases: nominative (-u), accusative (-a), genitive (-i)
// (mirrors Akkadian but with shorter endings no mimation in Ugaritic)
// - Number: singular / plural / dual
// - Dual: nom -āma, gen/acc -ēma
// - Verb tenses:
// qtl (perfect, suffix conjugation) completed action
// yqtl (imperfect, prefix conjugation) ongoing / future action
// - Verb stems: G, D, Š, N (this file: G-stem throughout)
// - No separate definite article (unlike later Hebrew ha-)
// - Copula: root kn (to be)
//
// Verb conjugation conventions:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "perfect" | "imperfect"
//
// Noun declension conventions:
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural" | "dual"
//
// Verbs covered (G-stem root, transliterated):
// "kn" to be (copula)
// "hlk" to go
// "rʼy" to see
// "ʼmr" to say
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn uga_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn uga_str_len(s: String) -> Int {
return str_len(s)
}
fn uga_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Slot index
//
// Maps person × number to a 0-based slot index.
// Gender is used separately for third-person disambiguation.
//
// Slot layout (6 primary cells, matching classic NW-Semitic paradigm):
// 0 = 1sg (I)
// 1 = 2sg (you sg masc and fem conflate in many forms)
// 2 = 3sg m (he)
// 3 = 3sg f (she)
// 4 = 1pl (we)
// 5 = 3pl (they masc default)
fn uga_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "plural") { return 4 }
return 0
}
if str_eq(person, "second") {
return 1
}
// third
if str_eq(number, "plural") { return 5 }
return 2
}
// uga_slot_g: gender-sensitive slot for third-person singular.
fn uga_slot_g(person: String, gender: String, number: String) -> Int {
let base: Int = uga_slot(person, number)
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
}
}
return base
}
// Copula: kn to be
//
// qtl (perfect): kāna (3sg m), kānat (3sg f), kānu (3pl)
// yqtl (imperfect): yakūnu (3sg m), takūnu (3sg f / 2sg), ʼakūnu (1sg)
//
// The root kn is a weak verb (hollow: middle w/y). The perfect stem is kāna-;
// the imperfect stem is -kūn-.
fn uga_kn_perfect(slot: Int) -> String {
if slot == 0 { return "kāntu" } // 1sg (kān- + suffix -tu)
if slot == 1 { return "kānta" } // 2sg (-ta suffix)
if slot == 2 { return "kāna" } // 3sg m (base)
if slot == 3 { return "kānat" } // 3sg f (-at suffix)
if slot == 4 { return "kānnu" } // 1pl (-nu suffix)
return "kānu" // 3pl (-u suffix)
}
fn uga_kn_imperfect(slot: Int) -> String {
if slot == 0 { return "ʼakūnu" } // 1sg (ʼa- prefix)
if slot == 1 { return "takūnu" } // 2sg (ta- prefix)
if slot == 2 { return "yakūnu" } // 3sg m (ya- prefix)
if slot == 3 { return "takūnu" } // 3sg f (ta- prefix, same as 2sg)
if slot == 4 { return "nakūnu" } // 1pl (na- prefix)
return "yakūnuna" // 3pl (ya- + -ūna energic suffix)
}
fn uga_is_copula(verb: String) -> Bool {
if str_eq(verb, "kn") { return true }
if str_eq(verb, "kāna") { return true }
if str_eq(verb, "be") { return true }
return false
}
fn uga_conjugate_copula(tense: String, slot: Int) -> String {
if str_eq(tense, "perfect") { return uga_kn_perfect(slot) }
return uga_kn_imperfect(slot)
}
// hlk to go
//
// Regular strong verb. Perfect stem: halak-; imperfect stem: -hluk-.
fn uga_hlk_perfect(slot: Int) -> String {
if slot == 0 { return "halaktu" } // 1sg
if slot == 1 { return "halakta" } // 2sg
if slot == 2 { return "halaka" } // 3sg m
if slot == 3 { return "halakat" } // 3sg f
if slot == 4 { return "halaknu" } // 1pl
return "halaku" // 3pl
}
fn uga_hlk_imperfect(slot: Int) -> String {
if slot == 0 { return "ʼahluku" } // 1sg
if slot == 1 { return "tahluku" } // 2sg
if slot == 2 { return "yahluku" } // 3sg m
if slot == 3 { return "tahluku" } // 3sg f
if slot == 4 { return "nahluku" } // 1pl
return "yahlukuna" // 3pl
}
// rʼy to see
//
// Third-weak verb (final y). Perfect: raʼaya (3sg m); imperfect: yarʼā (3sg m).
fn uga_ray_perfect(slot: Int) -> String {
if slot == 0 { return "raʼaytu" } // 1sg
if slot == 1 { return "raʼayta" } // 2sg
if slot == 2 { return "raʼaya" } // 3sg m
if slot == 3 { return "raʼayat" } // 3sg f
if slot == 4 { return "raʼaynu" } // 1pl
return "raʼayu" // 3pl
}
fn uga_ray_imperfect(slot: Int) -> String {
if slot == 0 { return "ʼarʼā" } // 1sg
if slot == 1 { return "tarʼā" } // 2sg
if slot == 2 { return "yarʼā" } // 3sg m
if slot == 3 { return "tarʼā" } // 3sg f
if slot == 4 { return "narʼā" } // 1pl
return "yarʼayna" // 3pl (fem plural ending for final-weak)
}
// ʼmr to say
//
// Strong verb. Perfect: ʼamara (3sg m); imperfect: yaʼmuru (3sg m).
fn uga_amr_perfect(slot: Int) -> String {
if slot == 0 { return "ʼamartu" } // 1sg
if slot == 1 { return "ʼamarta" } // 2sg
if slot == 2 { return "ʼamara" } // 3sg m
if slot == 3 { return "ʼamarat" } // 3sg f
if slot == 4 { return "ʼamarnu" } // 1pl
return "ʼamaru" // 3pl
}
fn uga_amr_imperfect(slot: Int) -> String {
if slot == 0 { return "ʼaʼmuru" } // 1sg
if slot == 1 { return "taʼmuru" } // 2sg
if slot == 2 { return "yaʼmuru" } // 3sg m
if slot == 3 { return "taʼmuru" } // 3sg f
if slot == 4 { return "naʼmuru" } // 1pl
return "yaʼmuruna" // 3pl
}
// Generic G-stem paradigm (regular strong verbs)
//
// For verbs not in the lookup table, apply the regular pattern.
// Caller supplies the 3sg perfect form (qtl) and 3sg imperfect (yqtl) form.
// We derive person forms by suffix/prefix substitution.
fn uga_generic_perfect(base3sg: String, slot: Int) -> String {
// Perfect suffixes: 1sg -tu, 2sg -ta, 3sg m , 3sg f -at, 1pl -nu, 3pl -u
if slot == 0 { return base3sg + "tu" }
if slot == 1 { return base3sg + "ta" }
if slot == 2 { return base3sg }
if slot == 3 { return base3sg + "at" }
if slot == 4 { return base3sg + "nu" }
return base3sg + "u"
}
fn uga_generic_imperfect(base3sg: String, slot: Int) -> String {
// Strip leading ya- to get the core, re-prefix per person.
// This is a heuristic for display purposes when the stem is unknown.
if slot == 0 { return "ʼa" + base3sg }
if slot == 1 { return "ta" + base3sg }
if slot == 2 { return "ya" + base3sg }
if slot == 3 { return "ta" + base3sg }
if slot == 4 { return "na" + base3sg }
return "ya" + base3sg + "una"
}
// Known-verb dispatcher
fn uga_known_verb(verb: String, tense: String, slot: Int) -> String {
// kn to be
if str_eq(verb, "kn") {
return uga_conjugate_copula(tense, slot)
}
if str_eq(verb, "kāna") {
return uga_conjugate_copula(tense, slot)
}
// hlk to go
if str_eq(verb, "hlk") {
if str_eq(tense, "perfect") { return uga_hlk_perfect(slot) }
return uga_hlk_imperfect(slot)
}
if str_eq(verb, "halaka") {
if str_eq(tense, "perfect") { return uga_hlk_perfect(slot) }
return uga_hlk_imperfect(slot)
}
// rʼy to see
if str_eq(verb, "rʼy") {
if str_eq(tense, "perfect") { return uga_ray_perfect(slot) }
return uga_ray_imperfect(slot)
}
if str_eq(verb, "raʼaya") {
if str_eq(tense, "perfect") { return uga_ray_perfect(slot) }
return uga_ray_imperfect(slot)
}
// ʼmr to say
if str_eq(verb, "ʼmr") {
if str_eq(tense, "perfect") { return uga_amr_perfect(slot) }
return uga_amr_imperfect(slot)
}
if str_eq(verb, "ʼamara") {
if str_eq(tense, "perfect") { return uga_amr_perfect(slot) }
return uga_amr_imperfect(slot)
}
return ""
}
// Main conjugation entry point
//
// uga_conjugate: conjugate a Ugaritic verb (G-stem).
//
// verb: root (23 consonants, e.g. "kn", "hlk") or 3sg perfect citation form
// tense: "perfect" | "imperfect"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns:
// - Inflected form for known verbs
// - verb unchanged as safe fallback for unknown verbs
fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = uga_slot(person, number)
if uga_is_copula(verb) {
return uga_conjugate_copula(tense, slot)
}
let known: String = uga_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
return verb
}
// Noun declension
//
// uga_decline: decline a Ugaritic noun for gram_case and number.
//
// Ugaritic case endings (no mimation unlike Akkadian):
// Masculine singular:
// Nominative: -u (e.g. malku "king")
// Accusative: -a (e.g. malka)
// Genitive: -i (e.g. malki)
// Masculine plural:
// Nominative: -ūma (e.g. malkūma)
// Genitive/Accusative: -īma (e.g. malkīma)
// Feminine singular (typically -atu base):
// Nominative: -atu (e.g. malaktu)
// Accusative: -ata (e.g. malakta)
// Genitive: -ati (e.g. malakti)
// Feminine plural:
// Nominative: -ātu (e.g. malakātu)
// Genitive/Accusative: -āti
// Dual:
// Nominative: -āma (e.g. malkāma)
// Genitive/Accusative: -ēma
//
// Strategy: strip the known nominative ending to get the bare stem,
// then apply the requested ending.
fn uga_strip_nom(noun: String) -> String {
// Strip masc sg -u
if uga_str_ends(noun, "u") {
let len: Int = uga_str_len(noun)
if len > 1 {
return uga_str_drop_last(noun, 1)
}
}
// Strip fem sg -atu
if uga_str_ends(noun, "atu") {
return uga_str_drop_last(noun, 3)
}
return noun
}
fn uga_is_fem(noun: String) -> Bool {
if uga_str_ends(noun, "atu") { return true }
if uga_str_ends(noun, "ata") { return true }
if uga_str_ends(noun, "ati") { return true }
if uga_str_ends(noun, "ātu") { return true }
if uga_str_ends(noun, "āti") { return true }
return false
}
fn uga_decline(noun: String, gram_case: String, number: String) -> String {
let fem: Bool = uga_is_fem(noun)
let stem: String = uga_strip_nom(noun)
if str_eq(number, "dual") {
if str_eq(gram_case, "nom") { return stem + "āma" }
return stem + "ēma"
}
if str_eq(number, "plural") {
if fem {
if str_eq(gram_case, "nom") { return stem + "ātu" }
return stem + "āti"
}
// Masculine plural
if str_eq(gram_case, "nom") { return stem + "ūma" }
return stem + "īma"
}
// Singular
if fem {
if str_eq(gram_case, "nom") { return stem + "atu" }
if str_eq(gram_case, "acc") { return stem + "ata" }
if str_eq(gram_case, "gen") { return stem + "ati" }
return stem + "atu"
}
// Masculine singular
if str_eq(gram_case, "nom") { return stem + "u" }
if str_eq(gram_case, "acc") { return stem + "a" }
if str_eq(gram_case, "gen") { return stem + "i" }
return stem + "u"
}
// Noun phrase
//
// uga_noun_phrase: produce the surface noun phrase.
//
// Ugaritic has no separate definite article (unlike later Hebrew ha-).
// Determination is expressed through word order, the construct chain (genitive),
// and pronominal suffixes. The definite parameter is accepted for interface
// uniformity but has no surface effect.
//
// noun: base noun (nominative singular form, e.g. "malku")
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural" | "dual"
// definite: "true" | "false" (no surface effect in Ugaritic)
fn uga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return uga_decline(noun, gram_case, number)
}
// Canonical verb mapping
//
// uga_map_canonical: map cross-lingual English canonical verb labels to
// Ugaritic G-stem roots or citation forms.
fn uga_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "kn" }
if str_eq(verb, "go") { return "hlk" }
if str_eq(verb, "see") { return "rʼy" }
if str_eq(verb, "say") { return "ʼmr" }
if str_eq(verb, "speak") { return "ʼmr" }
return verb
}
+25
View File
@@ -0,0 +1,25 @@
// auto-generated by elc --emit-header — do not edit
extern fn uga_str_ends(s: String, suf: String) -> Bool
extern fn uga_str_len(s: String) -> Int
extern fn uga_str_drop_last(s: String, n: Int) -> String
extern fn uga_slot(person: String, number: String) -> Int
extern fn uga_slot_g(person: String, gender: String, number: String) -> Int
extern fn uga_kn_perfect(slot: Int) -> String
extern fn uga_kn_imperfect(slot: Int) -> String
extern fn uga_is_copula(verb: String) -> Bool
extern fn uga_conjugate_copula(tense: String, slot: Int) -> String
extern fn uga_hlk_perfect(slot: Int) -> String
extern fn uga_hlk_imperfect(slot: Int) -> String
extern fn uga_ray_perfect(slot: Int) -> String
extern fn uga_ray_imperfect(slot: Int) -> String
extern fn uga_amr_perfect(slot: Int) -> String
extern fn uga_amr_imperfect(slot: Int) -> String
extern fn uga_generic_perfect(base3sg: String, slot: Int) -> String
extern fn uga_generic_imperfect(base3sg: String, slot: Int) -> String
extern fn uga_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn uga_strip_nom(noun: String) -> String
extern fn uga_is_fem(noun: String) -> Bool
extern fn uga_decline(noun: String, gram_case: String, number: String) -> String
extern fn uga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn uga_map_canonical(verb: String) -> String

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