Compare commits

...

181 Commits

Author SHA1 Message Date
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 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
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 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 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 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 1ed2dc3c11 add gitflow CI for dev/stage/prod environments 2026-05-04 08:55:21 -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
445 changed files with 190284 additions and 2585 deletions
+231 -34
View File
@@ -1,4 +1,4 @@
name: El SDK 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,79 +22,209 @@ 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
- 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
# 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
@@ -99,7 +232,15 @@ jobs:
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.c \
--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
@@ -107,9 +248,65 @@ jobs:
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.h \
--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
+203 -32
View File
@@ -1,4 +1,4 @@
name: El SDK CI stage
name: El SDK CI - stage
on:
push:
@@ -11,12 +11,15 @@ 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)
- name: Enforce source branch (stage <- dev only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
@@ -24,84 +27,205 @@ jobs:
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} stage"
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
- 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
# 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
@@ -109,7 +233,7 @@ jobs:
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.c \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
@@ -117,9 +241,56 @@ jobs:
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.h \
--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
+261 -146
View File
@@ -4,16 +4,22 @@ 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)
- name: Enforce source branch (main <- stage only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
@@ -21,74 +27,211 @@ jobs:
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} main"
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" \
@@ -101,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" \
@@ -121,7 +262,6 @@ jobs:
echo "Created release id=${RELEASE_ID}"
# Upload assets
upload_asset() {
local filepath="$1"
local name="$2"
@@ -132,32 +272,36 @@ 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"
# Publish artifacts to GCP 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
@@ -165,7 +309,15 @@ jobs:
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.c \
--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
@@ -173,118 +325,81 @@ jobs:
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/el_runtime.h \
--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
# Dispatch el-sdk-updated event to downstream repos
- 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"
- name: Dispatch to neuron-technologies/el-ui
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/el-ui/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/el-ui"
- name: Dispatch to neuron-technologies/elp
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/elp/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/elp"
- name: Dispatch to neuron-technologies/elql
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/elql/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/elql"
- name: Dispatch to neuron-technologies/el-ide
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/el-ide/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/el-ide"
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 "".
}
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: Any) -> String
extern fn generate_frame_lang(frame: Any, lang_code: String) -> String
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> Any
extern fn generate(semantic_form_json: String) -> String
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
+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: Any, key: String) -> String
extern fn slots_set(slots: Any, key: String, val: String) -> Any
extern fn make_slots(k0: String, v0: String) -> Any
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> Any
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> Any
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> Any
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> Any
extern fn rule_id(rule: Any) -> String
extern fn rule_lhs(rule: Any) -> String
extern fn rule_rhs_len(rule: Any) -> Int
extern fn rule_rhs(rule: Any, idx: Int) -> String
extern fn make_rule(id: String, lhs: String, r0: String) -> Any
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> Any
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> Any
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> Any
extern fn build_rules() -> Any
extern fn get_rules() -> Any
extern fn find_rule(rule_id_str: String) -> Any
extern fn make_leaf(label: String, word: String) -> String
extern fn make_node1(label: String, child0: String) -> String
extern fn make_node2(label: String, child0: String, child1: String) -> String
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) -> Any
extern fn render_tree(tree: String) -> String
extern fn gram_word_order(profile: Any) -> String
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: Any) -> String
extern fn gram_build_vp(verb: String, aux: String, profile: Any) -> String
extern fn gram_question_strategy(profile: Any) -> String
extern fn is_pronoun(word: String) -> Bool
extern fn build_np(referent: String, slots: Any) -> String
extern fn build_pp(loc: String) -> String
extern fn build_vp_body(slots: Any) -> String
extern fn build_vp_from_slots(slots: Any) -> String
extern fn generate_tree(rule_id_str: String, slots: Any) -> String
+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
+710
View File
@@ -0,0 +1,710 @@
// morphology.el - Morphology engine: inflection driven by language profile.
//
// Strategy dispatch is based on the morph_type field of the language profile:
//
// "isolating" (zh, vi) - no inflection; base form is final form.
// "agglutinative" (ja, fi, sw) - suffix chains; each feature adds a suffix.
// "fusional" (en, de, ru) - endings encode multiple features at once;
// table-driven per language.
// "polysynthetic" (various) - complex verb complexes; returns base form,
// full support deferred.
//
// Language-specific dispatch (added in 0.3.0):
// morph_conjugate and morph_pluralize first check the profile language code
// and delegate to the corresponding language module when one is available:
// es -> morphology-es.el (Spanish)
// fr -> morphology-fr.el (French)
// de -> morphology-de.el (German)
// ru -> morphology-ru.el (Russian)
// ja -> morphology-ja.el (Japanese)
// fi -> morphology-fi.el (Finnish)
// ar -> morphology-ar.el (Arabic)
// hi -> morphology-hi.el (Hindi)
// sw -> morphology-sw.el (Swahili)
// Unknown codes fall through to the morph_type strategy dispatch below.
//
// For English (fusional, code="en") the existing rules are preserved and
// routed through the engine interface rather than exposed as the sole path.
//
// Depends on: language-profile
// Language modules (loaded after this file, depend on this file):
// morphology-es, morphology-fr, morphology-de, morphology-ru, morphology-ja,
// morphology-fi, morphology-ar, morphology-hi, morphology-sw
// String helpers
import "language-profile.el"
import "morphology-es.el"
import "morphology-fr.el"
import "morphology-de.el"
import "morphology-ru.el"
import "morphology-fi.el"
import "morphology-ar.el"
import "morphology-hi.el"
import "morphology-sw.el"
import "morphology-la.el"
import "morphology-ja.el"
fn str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn str_last3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, n - 3, n)
}
fn 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 is_vowel(c: String) -> Bool {
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 }
return false
}
// Suffix application
//
// Apply a suffix to a base form with basic phonological adjustments.
// Used by the agglutinative path and the fusional helper functions.
//
// Rules applied (in order):
// base ends in "-e" and suffix starts with a vowel -> drop the trailing "e"
// base ends in CVC and suffix starts with a vowel -> double final consonant
// default: concatenate
fn morph_apply_suffix(base: String, suffix: String) -> String {
if str_eq(suffix, "") {
return base
}
let suf_start: String = str_slice(suffix, 0, 1)
let suf_starts_vowel: Bool = is_vowel(suf_start)
// Drop trailing silent -e before a vowel-initial suffix
if suf_starts_vowel {
if str_ends(base, "e") {
if !str_ends(base, "ee") {
return str_drop_last(base, 1) + suffix
}
}
}
// CVC doubling before a vowel-initial suffix
if suf_starts_vowel {
let n: Int = str_len(base)
if n >= 3 {
let c3: String = str_slice(base, n - 3, n - 2)
let c2: String = str_slice(base, n - 2, n - 1)
let c1: String = str_slice(base, n - 1, n)
if !is_vowel(c3) {
if is_vowel(c2) {
if !is_vowel(c1) {
if !str_eq(c1, "w") {
if !str_eq(c1, "x") {
if !str_eq(c1, "y") {
return base + c1 + suffix
}
}
}
}
}
}
}
}
return base + suffix
}
// English irregular tables
fn en_irregular_plural(word: String) -> String {
if str_eq(word, "child") { return "children" }
if str_eq(word, "man") { return "men" }
if str_eq(word, "woman") { return "women" }
if str_eq(word, "tooth") { return "teeth" }
if str_eq(word, "foot") { return "feet" }
if str_eq(word, "goose") { return "geese" }
if str_eq(word, "mouse") { return "mice" }
if str_eq(word, "louse") { return "lice" }
if str_eq(word, "ox") { return "oxen" }
if str_eq(word, "person") { return "people" }
if str_eq(word, "leaf") { return "leaves" }
if str_eq(word, "loaf") { return "loaves" }
if str_eq(word, "wolf") { return "wolves" }
if str_eq(word, "life") { return "lives" }
if str_eq(word, "knife") { return "knives" }
if str_eq(word, "wife") { return "wives" }
if str_eq(word, "half") { return "halves" }
if str_eq(word, "self") { return "selves" }
if str_eq(word, "elf") { return "elves" }
if str_eq(word, "shelf") { return "shelves" }
if str_eq(word, "fish") { return "fish" }
if str_eq(word, "sheep") { return "sheep" }
if str_eq(word, "deer") { return "deer" }
if str_eq(word, "moose") { return "moose" }
if str_eq(word, "series") { return "series" }
if str_eq(word, "species") { return "species" }
return ""
}
fn en_irregular_singular(word: String) -> String {
if str_eq(word, "children") { return "child" }
if str_eq(word, "men") { return "man" }
if str_eq(word, "women") { return "woman" }
if str_eq(word, "teeth") { return "tooth" }
if str_eq(word, "feet") { return "foot" }
if str_eq(word, "geese") { return "goose" }
if str_eq(word, "mice") { return "mouse" }
if str_eq(word, "lice") { return "louse" }
if str_eq(word, "oxen") { return "ox" }
if str_eq(word, "people") { return "person" }
if str_eq(word, "leaves") { return "leaf" }
if str_eq(word, "wolves") { return "wolf" }
if str_eq(word, "lives") { return "life" }
if str_eq(word, "knives") { return "knife" }
if str_eq(word, "wives") { return "wife" }
if str_eq(word, "halves") { return "half" }
if str_eq(word, "selves") { return "self" }
if str_eq(word, "elves") { return "elf" }
if str_eq(word, "shelves") { return "shelf" }
if str_eq(word, "fish") { return "fish" }
if str_eq(word, "sheep") { return "sheep" }
if str_eq(word, "deer") { return "deer" }
if str_eq(word, "moose") { return "moose" }
if str_eq(word, "series") { return "series" }
if str_eq(word, "species") { return "species" }
return ""
}
// Returns [base, 3sg-present, past, past-participle, gerund] for English
// irregular verbs, or an empty list if the verb is regular.
fn en_irregular_verb(base: String) -> [String] {
let empty: [String] = []
if str_eq(base, "be") { let r: [String] = ["be", "is", "was", "been", "being"]; return r }
if str_eq(base, "have") { let r: [String] = ["have", "has", "had", "had", "having"]; return r }
if str_eq(base, "do") { let r: [String] = ["do", "does", "did", "done", "doing"]; return r }
if str_eq(base, "go") { let r: [String] = ["go", "goes", "went", "gone", "going"]; return r }
if str_eq(base, "say") { let r: [String] = ["say", "says", "said", "said", "saying"]; return r }
if str_eq(base, "make") { let r: [String] = ["make", "makes", "made", "made", "making"]; return r }
if str_eq(base, "know") { let r: [String] = ["know", "knows", "knew", "known", "knowing"]; return r }
if str_eq(base, "take") { let r: [String] = ["take", "takes", "took", "taken", "taking"]; return r }
if str_eq(base, "see") { let r: [String] = ["see", "sees", "saw", "seen", "seeing"]; return r }
if str_eq(base, "come") { let r: [String] = ["come", "comes", "came", "come", "coming"]; return r }
if str_eq(base, "think") { let r: [String] = ["think", "thinks","thought","thought", "thinking"]; return r }
if str_eq(base, "get") { let r: [String] = ["get", "gets", "got", "gotten", "getting"]; return r }
if str_eq(base, "give") { let r: [String] = ["give", "gives", "gave", "given", "giving"]; return r }
if str_eq(base, "find") { let r: [String] = ["find", "finds", "found", "found", "finding"]; return r }
if str_eq(base, "tell") { let r: [String] = ["tell", "tells", "told", "told", "telling"]; return r }
if str_eq(base, "become") { let r: [String] = ["become", "becomes","became","become", "becoming"]; return r }
if str_eq(base, "leave") { let r: [String] = ["leave", "leaves","left", "left", "leaving"]; return r }
if str_eq(base, "feel") { let r: [String] = ["feel", "feels", "felt", "felt", "feeling"]; return r }
if str_eq(base, "put") { let r: [String] = ["put", "puts", "put", "put", "putting"]; return r }
if str_eq(base, "bring") { let r: [String] = ["bring", "brings","brought","brought", "bringing"]; return r }
if str_eq(base, "begin") { let r: [String] = ["begin", "begins","began", "begun", "beginning"];return r }
if str_eq(base, "keep") { let r: [String] = ["keep", "keeps", "kept", "kept", "keeping"]; return r }
if str_eq(base, "hold") { let r: [String] = ["hold", "holds", "held", "held", "holding"]; return r }
if str_eq(base, "write") { let r: [String] = ["write", "writes","wrote", "written", "writing"]; return r }
if str_eq(base, "stand") { let r: [String] = ["stand", "stands","stood", "stood", "standing"]; return r }
if str_eq(base, "hear") { let r: [String] = ["hear", "hears", "heard", "heard", "hearing"]; return r }
if str_eq(base, "let") { let r: [String] = ["let", "lets", "let", "let", "letting"]; return r }
if str_eq(base, "run") { let r: [String] = ["run", "runs", "ran", "run", "running"]; return r }
if str_eq(base, "meet") { let r: [String] = ["meet", "meets", "met", "met", "meeting"]; return r }
if str_eq(base, "sit") { let r: [String] = ["sit", "sits", "sat", "sat", "sitting"]; return r }
if str_eq(base, "send") { let r: [String] = ["send", "sends", "sent", "sent", "sending"]; return r }
if str_eq(base, "speak") { let r: [String] = ["speak", "speaks","spoke", "spoken", "speaking"]; return r }
if str_eq(base, "buy") { let r: [String] = ["buy", "buys", "bought", "bought", "buying"]; return r }
if str_eq(base, "pay") { let r: [String] = ["pay", "pays", "paid", "paid", "paying"]; return r }
if str_eq(base, "read") { let r: [String] = ["read", "reads", "read", "read", "reading"]; return r }
if str_eq(base, "win") { let r: [String] = ["win", "wins", "won", "won", "winning"]; return r }
if str_eq(base, "eat") { let r: [String] = ["eat", "eats", "ate", "eaten", "eating"]; return r }
if str_eq(base, "fall") { let r: [String] = ["fall", "falls", "fell", "fallen", "falling"]; return r }
if str_eq(base, "sleep") { let r: [String] = ["sleep", "sleeps","slept", "slept", "sleeping"]; return r }
if str_eq(base, "drive") { let r: [String] = ["drive", "drives","drove", "driven", "driving"]; return r }
if str_eq(base, "build") { let r: [String] = ["build", "builds","built", "built", "building"]; return r }
if str_eq(base, "cut") { let r: [String] = ["cut", "cuts", "cut", "cut", "cutting"]; return r }
if str_eq(base, "set") { let r: [String] = ["set", "sets", "set", "set", "setting"]; return r }
if str_eq(base, "hit") { let r: [String] = ["hit", "hits", "hit", "hit", "hitting"]; return r }
return empty
}
// English regular inflection helpers
fn en_verb_3sg(base: String) -> String {
if str_ends(base, "s") { return base + "es" }
if str_ends(base, "x") { return base + "es" }
if str_ends(base, "z") { return base + "es" }
if str_ends(base, "ch") { return base + "es" }
if str_ends(base, "sh") { return base + "es" }
let last: String = str_last_char(base)
if str_eq(last, "y") {
let prev: String = str_drop_last(base, 1)
let prev_last: String = str_last_char(prev)
if !is_vowel(prev_last) {
return prev + "ies"
}
}
return base + "s"
}
fn en_should_double_final(base: String) -> Bool {
let n: Int = str_len(base)
if n < 3 {
return false
}
let c3: String = str_slice(base, n - 3, n - 2)
let c2: String = str_slice(base, n - 2, n - 1)
let c1: String = str_slice(base, n - 1, n)
if !is_vowel(c3) {
if is_vowel(c2) {
if !is_vowel(c1) {
if !str_eq(c1, "w") {
if !str_eq(c1, "x") {
if !str_eq(c1, "y") {
return true
}
}
}
}
}
}
return false
}
fn en_verb_past(base: String) -> String {
if str_ends(base, "e") {
return base + "d"
}
let last: String = str_last_char(base)
if str_eq(last, "y") {
let prev: String = str_drop_last(base, 1)
let prev_last: String = str_last_char(prev)
if !is_vowel(prev_last) {
return prev + "ied"
}
}
if en_should_double_final(base) {
return base + last + "ed"
}
return base + "ed"
}
fn en_verb_gerund(base: String) -> String {
if str_ends(base, "ie") {
return str_drop_last(base, 2) + "ying"
}
if str_ends(base, "e") {
if !str_ends(base, "ee") {
return str_drop_last(base, 1) + "ing"
}
}
let last: String = str_last_char(base)
if en_should_double_final(base) {
return base + last + "ing"
}
return base + "ing"
}
// English noun pluralization (regular).
fn en_pluralize_regular(singular: String) -> String {
if str_ends(singular, "s") { return singular + "es" }
if str_ends(singular, "x") { return singular + "es" }
if str_ends(singular, "z") { return singular + "es" }
if str_ends(singular, "ch") { return singular + "es" }
if str_ends(singular, "sh") { return singular + "es" }
let last: String = str_last_char(singular)
if str_eq(last, "y") {
let prev: String = str_drop_last(singular, 1)
let prev_last: String = str_last_char(prev)
if !is_vowel(prev_last) {
return prev + "ies"
}
}
if str_ends(singular, "fe") {
return str_drop_last(singular, 2) + "ves"
}
return singular + "s"
}
// English verb conjugation
//
// tense: "present" | "past" | "future" | "perfect" | "progressive"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn en_verb_form(base: String, tense: String, person: String, number: String) -> String {
let irreg: [String] = en_irregular_verb(base)
let is_irreg: Bool = false
if native_list_len(irreg) > 0 {
let is_irreg = true
}
// "be" is fully irregular across all persons and tenses
if str_eq(base, "be") {
if str_eq(tense, "present") {
if str_eq(number, "plural") { return "are" }
if str_eq(person, "first") { return "am" }
if str_eq(person, "second") { return "are" }
return "is"
}
if str_eq(tense, "past") {
if str_eq(number, "plural") { return "were" }
if str_eq(person, "second") { return "were" }
return "was"
}
if str_eq(tense, "future") { return "will be" }
if str_eq(tense, "perfect") { return "been" }
if str_eq(tense, "progressive") { return "being" }
return "be"
}
if str_eq(tense, "present") {
if str_eq(person, "third") {
if str_eq(number, "singular") {
if is_irreg {
return native_list_get(irreg, 1)
}
return en_verb_3sg(base)
}
}
return base
}
if str_eq(tense, "past") {
if is_irreg { return native_list_get(irreg, 2) }
return en_verb_past(base)
}
if str_eq(tense, "future") {
return "will " + base
}
if str_eq(tense, "perfect") {
if is_irreg { return native_list_get(irreg, 3) }
return en_verb_past(base)
}
if str_eq(tense, "progressive") {
if is_irreg { return native_list_get(irreg, 4) }
return en_verb_gerund(base)
}
return base
}
// Determiner agreement
// Language-independent interface; only English has a/an distinction today.
fn agree_determiner(det: String, noun: String) -> String {
if str_eq(det, "a") {
let first: String = str_slice(noun, 0, 1)
let fl: String = str_to_lower(first)
if is_vowel(fl) {
return "an"
}
return "a"
}
return det
}
// Morphology engine: public interface
// morph_pluralize: pluralize a noun given a language profile.
//
// For isolating languages: return base form (no inflection).
// For agglutinative languages: look up plural suffix from Engram; fall back to
// base form when not found (Engram data drives agglutinative languages).
// For fusional English: apply English rule + irregular table.
// For other fusional languages: return base form (Engram data required).
// For polysynthetic: return base form.
fn morph_pluralize(noun: String, profile: [String]) -> String {
let mtype: String = lang_get(profile, "morph_type")
let code: String = lang_get(profile, "code")
// Language-specific dispatch (0.3.0)
// Delegate to the language module when one is available. Each module
// exposes a well-known public function; unknown codes fall through to the
// morph_type strategy below.
if str_eq(code, "es") { return es_pluralize(noun) }
if str_eq(code, "fr") { return fr_pluralize(noun) }
if str_eq(code, "de") { return de_noun_plural(noun, "unknown") }
if str_eq(code, "ru") { return ru_noun_case(noun, "m", "nom", "pl") }
if str_eq(code, "ja") { return noun } // Japanese nouns do not pluralize
if str_eq(code, "fi") { return fi_apply_case(noun, "nom", "pl") }
if str_eq(code, "ar") { return ar_sound_plural(noun, "m") }
if str_eq(code, "hi") { return hi_noun_direct(noun, hi_gender(noun), "pl") }
if str_eq(code, "sw") { return sw_noun_plural(noun) }
// morph_type fallback (isolating / agglutinative / fusional)
if str_eq(mtype, "isolating") {
// Isolating languages (zh, vi, etc.) do not inflect nouns.
// Number is expressed through context, classifiers, or separate words.
return noun
}
if str_eq(mtype, "agglutinative") {
// Agglutinative languages attach suffixes that come from vocabulary data
// in the Engram. The engine provides the mechanism; the data (suffixes)
// come from language-specific vocabulary nodes. Without loaded Engram
// data for the target language, we return the base form and let the
// caller supply the inflected form directly.
return noun
}
if str_eq(mtype, "fusional") {
if str_eq(code, "en") {
let irreg: String = en_irregular_plural(noun)
if !str_eq(irreg, "") {
return irreg
}
return en_pluralize_regular(noun)
}
// Other fusional languages: inflection tables are extensive and
// language-specific. Return base; Engram vocabulary nodes supply the
// correct plural form for the target language.
return noun
}
// polysynthetic and unknown: return base form
return noun
}
// morph_map_canonical: map cross-lingual canonical verbs to language-specific forms.
//
// Semantic layers pass English canonical labels ("be", "have", "do") as predicates.
// Language modules expect their native infinitives ("ser", "sein", "olla" ...).
// This function normalises before dispatch so each module sees its native form.
//
// Zero-copula languages (ar, ja, hi) return "" for "be" callers that receive ""
// should omit the verb from the surface form.
fn morph_map_canonical(verb: String, code: String) -> String {
if str_eq(verb, "be") {
if str_eq(code, "es") { return "ser" }
if str_eq(code, "fr") { return "etre" } // ASCII alias; fr handles "etre" -> être
if str_eq(code, "de") { return "sein" }
if str_eq(code, "fi") { return "olla" }
if str_eq(code, "ru") { return "byt" } // Latin transliteration for now
if str_eq(code, "sw") { return "kuwa" }
}
return verb
}
// morph_conjugate: conjugate a verb given tense, person, number, and profile.
//
// tense: "present" | "past" | "future" | "perfect" | "progressive"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: [String]) -> String {
let mtype: String = lang_get(profile, "morph_type")
let code: String = lang_get(profile, "code")
// Map canonical English verb labels to language-specific infinitives before dispatch.
let verb = morph_map_canonical(verb, code)
// Language-specific dispatch (0.3.0)
// Delegate to the language module when one is available. Each module
// exposes a well-known public function; unknown codes fall through to the
// morph_type strategy below.
if str_eq(code, "es") { return es_conjugate(verb, tense, person, number) }
if str_eq(code, "fr") { return fr_conjugate(verb, tense, person, number) }
if str_eq(code, "de") { return de_conjugate(verb, tense, person, number) }
if str_eq(code, "ru") { return ru_conjugate(verb, tense, person, number, "unknown") }
if str_eq(code, "ja") { return ja_conjugate(verb, "present") }
if str_eq(code, "fi") { return fi_conjugate(verb, tense, person, number) }
if str_eq(code, "ar") { return ar_conjugate(verb, tense, person, "m", number) }
if str_eq(code, "hi") { return hi_conjugate(verb, tense, person, "m", number) }
if str_eq(code, "sw") { return sw_conjugate(verb, person, number, "1", tense) }
if str_eq(code, "la") { return la_conjugate(verb, tense, person, number) }
if str_eq(code, "he") { return he_conjugate(verb, tense, person, "m", number) }
if str_eq(code, "grc") { return grc_conjugate(verb, tense, person, number) }
if str_eq(code, "ang") { return ang_conjugate(verb, tense, person, number) }
if str_eq(code, "sa") { return sa_conjugate(verb, tense, person, number) }
if str_eq(code, "got") { return got_conjugate(verb, tense, person, number) }
if str_eq(code, "non") { return non_conjugate(verb, tense, person, number) }
if str_eq(code, "enm") { return enm_conjugate(verb, tense, person, number) }
if str_eq(code, "pi") { return pi_conjugate(verb, tense, person, number) }
if str_eq(code, "fro") { return fro_conjugate(verb, tense, person, number) }
if str_eq(code, "goh") { return goh_conjugate(verb, tense, person, number) }
if str_eq(code, "sga") { return sga_conjugate(verb, tense, person, number) }
if str_eq(code, "txb") { return txb_conjugate(verb, tense, person, number) }
if str_eq(code, "peo") { return peo_conjugate(verb, tense, person, number) }
if str_eq(code, "akk") { return akk_conjugate(verb, tense, person, number) }
if str_eq(code, "uga") { return uga_conjugate(verb, tense, person, number) }
if str_eq(code, "egy") { return egy_conjugate(verb, tense, person, number) }
if str_eq(code, "sux") { return sux_conjugate(verb, tense, person, number) }
if str_eq(code, "gez") { return gez_conjugate(verb, tense, person, number) }
if str_eq(code, "cop") { return cop_conjugate(verb, tense, person, number) }
// morph_type fallback (isolating / agglutinative / fusional)
if str_eq(mtype, "isolating") {
// Isolating languages do not inflect verbs. Tense and aspect are
// expressed through particles and separate words (e.g. in Mandarin).
return verb
}
if str_eq(mtype, "agglutinative") {
// Agglutinative suffix chains. The engine builds the suffix sequence;
// the actual suffix strings come from Engram vocabulary nodes tagged with
// the language code and the grammatical feature.
// Without Engram-loaded suffix tables, return base form.
return verb
}
if str_eq(mtype, "fusional") {
if str_eq(code, "en") {
return en_verb_form(verb, tense, person, number)
}
// Other fusional languages: return base form.
// Engram vocabulary nodes carry the inflected forms for de, ru, ar, etc.
return verb
}
// polysynthetic and unknown
return verb
}
// morph_inflect: general inflection entry point.
//
// features: a semicolon-separated feature string, e.g. "plural" | "past;third;singular"
// Dispatches to the appropriate engine path based on the profile morph_type.
fn morph_inflect(word: String, features: String, profile: [String]) -> String {
// Parse the first feature token to decide what kind of inflection to apply.
let n: Int = str_len(features)
if n == 0 {
return word
}
// Scan to the first ';' to extract the leading feature token.
let i: Int = 0
let running: Bool = true
while running {
if i >= n {
let running = false
} else {
let c: String = str_slice(features, i, i + 1)
if str_eq(c, ";") {
let running = false
} else {
let i = i + 1
}
}
}
let first_feat: String = str_slice(features, 0, i)
if str_eq(first_feat, "plural") {
return morph_pluralize(word, profile)
}
// For verb features: expect "tense;person;number"
// Parse remaining tokens after the first ';'
if i < n {
let rest: String = str_slice(features, i + 1, n)
// Find second ';'
let j: Int = 0
let rn: Int = str_len(rest)
let running2: Bool = true
while running2 {
if j >= rn {
let running2 = false
} else {
let c: String = str_slice(rest, j, j + 1)
if str_eq(c, ";") {
let running2 = false
} else {
let j = j + 1
}
}
}
let person: String = str_slice(rest, 0, j)
let number: String = ""
if j < rn {
let number = str_slice(rest, j + 1, rn)
}
return morph_conjugate(word, first_feat, person, number, profile)
}
// Single token that is a tense (e.g. "past") with no person/number
return morph_conjugate(word, first_feat, "third", "singular", profile)
}
// Backward-compatible English-only entry points
//
// These preserve the original signatures for callers that were written before
// the language-profile system was introduced.
fn pluralize(singular: String) -> String {
return morph_pluralize(singular, lang_default())
}
fn singularize(plural: String) -> String {
let irreg: String = en_irregular_singular(plural)
if !str_eq(irreg, "") {
return irreg
}
if str_ends(plural, "ies") {
return str_drop_last(plural, 3) + "y"
}
if str_ends(plural, "ves") {
let stem: String = str_drop_last(plural, 3)
let last_stem: String = str_last_char(stem)
if str_eq(last_stem, "i") {
return stem + "fe"
}
return stem + "f"
}
if str_ends(plural, "ches") { return str_drop_last(plural, 2) }
if str_ends(plural, "shes") { return str_drop_last(plural, 2) }
if str_ends(plural, "xes") { return str_drop_last(plural, 2) }
if str_ends(plural, "zes") { return str_drop_last(plural, 2) }
if str_ends(plural, "ses") { return str_drop_last(plural, 2) }
if str_ends(plural, "s") {
return str_drop_last(plural, 1)
}
return plural
}
// verb_form: English verb conjugation (original signature).
fn verb_form(base: String, tense: String, person: String, number: String) -> String {
return morph_conjugate(base, tense, person, number, lang_default())
}
// irregular_plural: English irregular plural lookup (backward compat).
fn irregular_plural(word: String) -> String {
return en_irregular_plural(word)
}
// irregular_singular: English irregular singular lookup (backward compat).
fn irregular_singular(word: String) -> String {
return en_irregular_singular(word)
}

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