Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a0a2bcb44 | |||
| f78da81aa4 | |||
| 2597a092bb | |||
| 226b798407 | |||
| cfe8cb1c80 | |||
| 688b8508fb | |||
| 59cea116c5 | |||
| deb0520551 | |||
| da116b2884 | |||
| 58753a88d7 | |||
| edff25180e | |||
| 6271cb42b2 | |||
| 3da9181deb | |||
| 192241c7c1 | |||
| e7c2dc7734 | |||
| f7bd99ae45 | |||
| 93d36fddb1 | |||
| 2d751890ea | |||
| 99b113ea9d | |||
| c087b97093 | |||
| 718a2e0c06 | |||
| b6187501fd | |||
| 18e1ab6db1 | |||
| 2dec76c87a | |||
| a36a62ca14 | |||
| 28ef43264a | |||
| 35c189759c | |||
| 5c94b8680d | |||
| cebf3ded62 | |||
| b83ecf52f9 | |||
| 15ea584671 | |||
| c2afcbddf5 | |||
| dbf2c659d9 | |||
| 2b8062c55f | |||
| dfe4e83ed1 | |||
| a390ee494e | |||
| c2cd5e01e1 | |||
| 8212e12e57 | |||
| 253ee2b887 | |||
| d7540700d4 | |||
| f103e85f88 | |||
| fe84639b17 | |||
| 5fdc9fb15e | |||
| 8967fa404e | |||
| a7e6fbf2d2 | |||
| 1f4b594ae7 | |||
| cff7ce072d | |||
| f5dcca0386 | |||
| 53e0b99d5f | |||
| 5f9cad5908 | |||
| 00629b39c4 | |||
| ca1e4d57b8 | |||
| f971e96dd5 | |||
| 81a1a624f1 | |||
| 7b7f9f353b | |||
| a3732a1e9a | |||
| 2ed6b26dde | |||
| d8e9fd12f4 | |||
| 8ef3eb6bec | |||
| 027ad82db2 | |||
| 8fa9c4ba20 | |||
| 8ab8e3fd31 | |||
| 05d717744b | |||
| 9c7bde47dc | |||
| b0d0975f05 | |||
| 6f634ae432 | |||
| c0553459e1 | |||
| 908ce303f3 | |||
| edbde5ef51 | |||
| 2e529bd0fe | |||
| 5d9299a472 | |||
| e8b01583d8 | |||
| fd208583fe | |||
| b19dd5608f | |||
| 94d6eace94 | |||
| 3e29fc43ab | |||
| f1dfc394e3 | |||
| 61bf501b84 | |||
| 979a5677d5 | |||
| 2fd298df55 | |||
| 254cbe0ac2 | |||
| 17b1aa0736 | |||
| bcfb33ea83 | |||
| 60ad7f2f6b | |||
| f0c731d2db | |||
| 231cb5eddd | |||
| 54de7d3f3f | |||
| e7e0f7d3e5 | |||
| 77100649c3 | |||
| b0570656b1 | |||
| a79b421578 | |||
| 0ab9361fab | |||
| f7953eb73a | |||
| 9c350e9f2f | |||
| e93c899d1f | |||
| 0b50e61f98 | |||
| f2741e4bdb | |||
| 7fd01b8a8d | |||
| d2940f5d1d | |||
| dca741f915 | |||
| 9862f4d6e1 | |||
| 8b074d2e39 | |||
| ec9c322cc7 | |||
| 702093e043 | |||
| 95b6fac094 | |||
| af66cebfd3 | |||
| 1b9bc049de | |||
| 19a7430ff8 | |||
| 6b0a77a1dd | |||
| 17e204ff2b | |||
| fc6d496937 | |||
| 2f713d64d4 | |||
| 0a2a084d65 | |||
| 7116610ffd | |||
| 810d8107da | |||
| 607d5661d8 | |||
| c4a569f29b | |||
| ec889e1e53 | |||
| 6ced0f8009 | |||
| bd7303447b | |||
| e8f6765750 | |||
| 3726f69435 | |||
| ee86736eab | |||
| eb52be4ade | |||
| e587bedf30 | |||
| 1eef9928f4 | |||
| 1e67544c88 | |||
| 2ac11a67b1 | |||
| 7f295bffe9 | |||
| 962c8cbe57 | |||
| a54797ebfe | |||
| db157106ee | |||
| 135744b4fe | |||
| 90ddbdbfc3 | |||
| 7c00922cbd | |||
| faee6fdb25 | |||
| a6d093536a | |||
| b580a63540 | |||
| bdd7b56703 | |||
| 592f8f482a | |||
| 8524479f89 | |||
| b2775b9228 | |||
| 52d0dd4225 | |||
| d7d7852f2e | |||
| 27f99f4053 | |||
| f1c0604ed8 | |||
| 4395d551c4 | |||
| 4a3f53cae6 | |||
| f51daa6ba0 | |||
| 1ae68962cf | |||
| ce68f91a38 | |||
| 3e7d316c65 | |||
| 847f556ee3 | |||
| 92f393afd8 | |||
| 3fbfe76f14 | |||
| 9013e241c3 | |||
| f9406afc83 | |||
| e16f18b409 | |||
| 507b220518 | |||
| 4e79edbe81 | |||
| deb7faba7f | |||
| e8b22e16a2 | |||
| 26e327ac62 | |||
| c704e53102 | |||
| 3086a56b5c | |||
| 2eebd13221 | |||
| 38d1905f1d | |||
| 7c644b8d89 | |||
| 9e8d23bcd9 | |||
| 0791fda43e | |||
| 53f2df500d | |||
| b0d0f18524 | |||
| 24ccef820c | |||
| eb282d8c0c | |||
| cd164debb8 | |||
| cab8509608 | |||
| dbff2dad7a | |||
| 245eb2898e | |||
| 7bfe30b767 | |||
| 6ede9e4379 | |||
| 4ae42ee7db | |||
| 3e5130e98d | |||
| beb4e436e1 | |||
| 71a1e41f93 | |||
| 0676725cb7 | |||
| d98a968f89 | |||
| 0c9154551f | |||
| 9aa0c49d0c | |||
| 59b96e3324 | |||
| 287b39f8e6 | |||
| c45744d8ca | |||
| 2e778ca664 | |||
| 641227a7d3 | |||
| 805318e2d0 | |||
| cefff5b891 | |||
| ce9a2caff4 | |||
| d1af4b0f8b | |||
| 282df712a8 | |||
| cfcedff7f4 | |||
| eab483ed4f | |||
| cfa4301026 | |||
| f271f9d9d8 | |||
| 49a8a1c24b | |||
| 252ad04c96 | |||
| 3cc9b1cc3d | |||
| 5678745381 | |||
| e853f4a25c | |||
| 5b6915ec9e | |||
| b0e38a245a | |||
| 84b5355fce | |||
| b547d1daf8 | |||
| cc6522c69d | |||
| 5807de835e | |||
| 33af4ed09e | |||
| f2c63f95fd | |||
| 01849c2033 | |||
| 56724325ed | |||
| fea830cca2 | |||
| 8642ad3978 | |||
| 6f560de02a | |||
| 995f29eb42 | |||
| 34725a3988 | |||
| a7bbd2f792 | |||
| 30a86c78d2 | |||
| cbb27b8d87 | |||
| 1432c56cf7 | |||
| 020308a29a | |||
| 6e1a338eb6 | |||
| 834065cf45 | |||
| 3b76f0f8e0 | |||
| be013d2b42 | |||
| 2ae5dd430f | |||
| 2b45fc2f0f | |||
| c16b6ed602 | |||
| 28bc05f29f | |||
| 0b480cfb6b | |||
| dc4a9ee95f | |||
| 12e537d6ab | |||
| 909c1577f1 | |||
| ea0de16562 | |||
| 361c958618 | |||
| a1159eec65 | |||
| 69d1085d2d | |||
| 3bf3c02854 | |||
| 602cd1586a | |||
| 61a4632163 | |||
| 192528543f | |||
| 69410a6908 | |||
| 6601761cd9 | |||
| 2454c83e82 | |||
| 1a609502c8 |
+248
-37
@@ -1,4 +1,4 @@
|
||||
name: El CI — dev
|
||||
name: El SDK CI - dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,6 +11,9 @@ on:
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -19,83 +22,291 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
> /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites — all must pass
|
||||
- name: Run tests — text
|
||||
# Build elb (needed for Artifact Registry publish and downstream CI)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
- name: Run tests - calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
- name: Run tests - time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
- name: Run tests - html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (dev)
|
||||
- name: Publish elc to Artifact Registry (dev)
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
# Also tag as latest-dev
|
||||
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (dev)
|
||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:dev (or fall back to :latest on first run)
|
||||
BASE_TAG="dev"
|
||||
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:dev" \
|
||||
-t "${CI_BASE}:dev-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:dev"
|
||||
docker push "${CI_BASE}:dev-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
+228
-32
@@ -1,4 +1,4 @@
|
||||
name: El CI — stage
|
||||
name: El SDK CI - stage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,90 +11,286 @@ on:
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (stage <- dev only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "dev" ]; then
|
||||
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> stage"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites — all must pass
|
||||
- name: Run tests — text
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
- name: Run tests - calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
- name: Run tests - time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
- name: Run tests - html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (stage)
|
||||
- name: Publish elc to Artifact Registry (stage)
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build elb (needed for epm and el-install builds below)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (stage)
|
||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:stage (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:stage" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:stage" \
|
||||
-t "${CI_BASE}:stage-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:stage"
|
||||
docker push "${CI_BASE}:stage-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -4,81 +4,234 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (main <- stage only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "stage" ]; then
|
||||
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> main"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites with gen3 elc
|
||||
- name: Run tests — text
|
||||
# Build elb binary
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
- name: Run tests - calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
- name: Run tests - time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
- name: Run tests - html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Publish / update the `latest` release with the three SDK assets
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
|
||||
- name: Bundle SDK tarball
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
mkdir -p dist/sdk/bin dist/sdk/runtime
|
||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||
cp lang/el-compiler/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/el-compiler/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/*.el dist/sdk/runtime/
|
||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||
ls -lh dist/el-sdk-latest.tar.gz
|
||||
|
||||
# Publish / update the `latest` release with all SDK assets
|
||||
- name: Publish latest release
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
REPO: neuron-technologies/el
|
||||
run: |
|
||||
# Delete existing `latest` release if it exists
|
||||
EXISTING_ID=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/releases/tags/latest" \
|
||||
@@ -91,12 +244,10 @@ jobs:
|
||||
"${GITEA_API}/repos/${REPO}/releases/${EXISTING_ID}"
|
||||
fi
|
||||
|
||||
# Delete and re-create the `latest` tag so it points at HEAD
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/tags/latest" || true
|
||||
|
||||
# Create the release
|
||||
RELEASE_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -111,7 +262,6 @@ jobs:
|
||||
|
||||
echo "Created release id=${RELEASE_ID}"
|
||||
|
||||
# Upload assets
|
||||
upload_asset() {
|
||||
local filepath="$1"
|
||||
local name="$2"
|
||||
@@ -122,70 +272,134 @@ jobs:
|
||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
||||
}
|
||||
|
||||
upload_asset dist/platform/elc elc
|
||||
upload_asset el-compiler/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset el-compiler/runtime/el_runtime.h el_runtime.h
|
||||
# Per-file assets (downstream CI needs these individually)
|
||||
upload_asset lang/dist/platform/elc elc
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.h el_runtime.h
|
||||
|
||||
# SDK bundle and installer binary
|
||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||
upload_asset lang/dist/bin/el-install el-install
|
||||
|
||||
echo "Release published successfully"
|
||||
|
||||
# Dispatch el-sdk-updated event to downstream repos
|
||||
# Publish artifact to GCP Artifact Registry (prod)
|
||||
- name: Publish elc to Artifact Registry (prod)
|
||||
- name: Publish El SDK to Artifact Registry (prod)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK
|
||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:latest" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:latest" \
|
||||
-t "${CI_BASE}:${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:latest"
|
||||
docker push "${CI_BASE}:${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
- name: Dispatch to foundation/engram
|
||||
- name: Dispatch el-sdk-updated to downstream repos
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/engram/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to foundation/engram"
|
||||
|
||||
- name: Dispatch to neuron-technologies/forge
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/forge/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
|
||||
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/${repo}/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {\"el_version\": \"latest\", \"commit\": \"${GITHUB_SHA}\"}
|
||||
}" && echo "Dispatched to ${repo}" || echo "Warning: dispatch to ${repo} failed"
|
||||
done
|
||||
|
||||
Executable
+50
@@ -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
|
||||
@@ -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
@@ -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/"
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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 a→b→c 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")
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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, "&", "&")
|
||||
let r2: String = str_replace(r1, "<", "<")
|
||||
let r3: String = str_replace(r2, ">", ">")
|
||||
let r4: String = str_replace(r3, "\"", """)
|
||||
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, "&")
|
||||
check_contains("escapes <", svg2, "<")
|
||||
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")
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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 "".
|
||||
}
|
||||
@@ -1,747 +0,0 @@
|
||||
// lexer.el — el self-hosting lexer
|
||||
//
|
||||
// Tokenises an el source string into a list of token maps.
|
||||
// Each token is a Map<String, Any> with keys:
|
||||
// "kind" -> String (e.g. "Int", "Ident", "Plus")
|
||||
// "value" -> String (the raw text of the token)
|
||||
//
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get — avoids O(N²) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
if ch == "1" { return true }
|
||||
if ch == "2" { return true }
|
||||
if ch == "3" { return true }
|
||||
if ch == "4" { return true }
|
||||
if ch == "5" { return true }
|
||||
if ch == "6" { return true }
|
||||
if ch == "7" { return true }
|
||||
if ch == "8" { return true }
|
||||
if ch == "9" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_alpha(ch: String) -> Bool {
|
||||
if ch == "a" { return true }
|
||||
if ch == "b" { return true }
|
||||
if ch == "c" { return true }
|
||||
if ch == "d" { return true }
|
||||
if ch == "e" { return true }
|
||||
if ch == "f" { return true }
|
||||
if ch == "g" { return true }
|
||||
if ch == "h" { return true }
|
||||
if ch == "i" { return true }
|
||||
if ch == "j" { return true }
|
||||
if ch == "k" { return true }
|
||||
if ch == "l" { return true }
|
||||
if ch == "m" { return true }
|
||||
if ch == "n" { return true }
|
||||
if ch == "o" { return true }
|
||||
if ch == "p" { return true }
|
||||
if ch == "q" { return true }
|
||||
if ch == "r" { return true }
|
||||
if ch == "s" { return true }
|
||||
if ch == "t" { return true }
|
||||
if ch == "u" { return true }
|
||||
if ch == "v" { return true }
|
||||
if ch == "w" { return true }
|
||||
if ch == "x" { return true }
|
||||
if ch == "y" { return true }
|
||||
if ch == "z" { return true }
|
||||
if ch == "A" { return true }
|
||||
if ch == "B" { return true }
|
||||
if ch == "C" { return true }
|
||||
if ch == "D" { return true }
|
||||
if ch == "E" { return true }
|
||||
if ch == "F" { return true }
|
||||
if ch == "G" { return true }
|
||||
if ch == "H" { return true }
|
||||
if ch == "I" { return true }
|
||||
if ch == "J" { return true }
|
||||
if ch == "K" { return true }
|
||||
if ch == "L" { return true }
|
||||
if ch == "M" { return true }
|
||||
if ch == "N" { return true }
|
||||
if ch == "O" { return true }
|
||||
if ch == "P" { return true }
|
||||
if ch == "Q" { return true }
|
||||
if ch == "R" { return true }
|
||||
if ch == "S" { return true }
|
||||
if ch == "T" { return true }
|
||||
if ch == "U" { return true }
|
||||
if ch == "V" { return true }
|
||||
if ch == "W" { return true }
|
||||
if ch == "X" { return true }
|
||||
if ch == "Y" { return true }
|
||||
if ch == "Z" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_alnum_or_underscore(ch: String) -> Bool {
|
||||
if lex_is_digit(ch) { return true }
|
||||
if lex_is_alpha(ch) { return true }
|
||||
if ch == "_" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_whitespace(ch: String) -> Bool {
|
||||
if ch == " " { return true }
|
||||
if ch == "\t" { return true }
|
||||
if ch == "\n" { return true }
|
||||
if ch == "\r" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
{ "kind": kind, "value": value }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
if word == "fn" { return "Fn" }
|
||||
if word == "type" { return "Type" }
|
||||
if word == "enum" { return "Enum" }
|
||||
if word == "match" { return "Match" }
|
||||
if word == "return" { return "Return" }
|
||||
if word == "if" { return "If" }
|
||||
if word == "else" { return "Else" }
|
||||
if word == "for" { return "For" }
|
||||
if word == "in" { return "In" }
|
||||
if word == "while" { return "While" }
|
||||
if word == "import" { return "Import" }
|
||||
if word == "from" { return "From" }
|
||||
if word == "as" { return "As" }
|
||||
if word == "with" { return "With" }
|
||||
if word == "sealed" { return "Sealed" }
|
||||
if word == "activate" { return "Activate" }
|
||||
if word == "where" { return "Where" }
|
||||
if word == "test" { return "Test" }
|
||||
if word == "seed" { return "Seed" }
|
||||
if word == "assert" { return "Assert" }
|
||||
if word == "protocol" { return "Protocol" }
|
||||
if word == "impl" { return "Impl" }
|
||||
if word == "retry" { return "Retry" }
|
||||
if word == "times" { return "Times" }
|
||||
if word == "fallback" { return "Fallback" }
|
||||
if word == "reason" { return "Reason" }
|
||||
if word == "parallel" { return "Parallel" }
|
||||
if word == "trace" { return "Trace" }
|
||||
if word == "requires" { return "Requires" }
|
||||
if word == "deploy" { return "Deploy" }
|
||||
if word == "to" { return "To" }
|
||||
if word == "via" { return "Via" }
|
||||
if word == "target" { return "Target" }
|
||||
if word == "true" { return "Bool" }
|
||||
if word == "false" { return "Bool" }
|
||||
if word == "cgi" { return "Cgi" }
|
||||
if word == "service" { return "Service" }
|
||||
if word == "manager" { return "Manager" }
|
||||
if word == "engine" { return "Engine" }
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if lex_is_digit(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if is_alnum_or_underscore(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Code-bearing string detection + comment strip ────────────────────────────
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
|
||||
// or stylesheet payloads inside string literals) carry their own line and
|
||||
// block comments. Those comments leak into the served HTML and reveal build
|
||||
// notes the visitor should never see. We strip them at the lexer so every
|
||||
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
|
||||
//
|
||||
// looks_like_code — heuristic gate so we only strip strings that actually
|
||||
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
|
||||
|
||||
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||
let nchars: [String] = native_string_chars(needle)
|
||||
let nlen: Int = native_list_len(nchars)
|
||||
if start + nlen > total { return false }
|
||||
let i = 0
|
||||
let matched = true
|
||||
while i < nlen {
|
||||
let a: String = native_list_get(chars, start + i)
|
||||
let b: String = native_list_get(nchars, i)
|
||||
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
|
||||
}
|
||||
matched
|
||||
}
|
||||
|
||||
fn str_has(s: String, needle: String) -> Bool {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let i = 0
|
||||
let found = false
|
||||
while i < total {
|
||||
if substr_at(chars, i, total, needle) {
|
||||
let found = true
|
||||
let i = total
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
fn looks_like_code(s: String) -> Bool {
|
||||
if str_has(s, "<script") { return true }
|
||||
if str_has(s, "<style") { return true }
|
||||
if str_has(s, "function") {
|
||||
if str_has(s, ";") { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments — character-by-character walk. Tracks JS string state
|
||||
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||
// preserved by checking the previous char before treating // as a line
|
||||
// comment opener: if the char immediately before '/' is ':', emit the '/'
|
||||
// literally and advance one position.
|
||||
fn strip_code_comments(s: String) -> String {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let out_parts: [String] = native_list_empty()
|
||||
let i = 0
|
||||
let in_squote = false
|
||||
let in_dquote = false
|
||||
let in_btick = false
|
||||
let prev = ""
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
let in_js_string = false
|
||||
if in_squote { let in_js_string = true }
|
||||
if in_dquote { let in_js_string = true }
|
||||
if in_btick { let in_js_string = true }
|
||||
|
||||
if in_js_string {
|
||||
// Backslash escape: consume next char verbatim regardless of which.
|
||||
if ch == "\\" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let nc: String = native_list_get(chars, next_i)
|
||||
let out_parts = native_list_append(out_parts, nc)
|
||||
let prev = nc
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let prev = ch
|
||||
let i = next_i
|
||||
}
|
||||
} else {
|
||||
if in_squote {
|
||||
if ch == "'" { let in_squote = false }
|
||||
} else {
|
||||
if in_dquote {
|
||||
if ch == "\"" { let in_dquote = false }
|
||||
} else {
|
||||
if in_btick {
|
||||
if ch == "`" { let in_btick = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not in a JS string. Check for comment openers.
|
||||
let next_i = i + 1
|
||||
let next_ch = ""
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
}
|
||||
|
||||
if ch == "/" {
|
||||
if next_ch == "/" {
|
||||
// URL guard: prev char ':' means this is "://", not a comment.
|
||||
if prev == ":" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Skip until newline (newline itself is preserved so
|
||||
// surrounding line counts/structure stay sane).
|
||||
let i = i + 2
|
||||
let scanning = true
|
||||
while scanning {
|
||||
if i >= total {
|
||||
let scanning = false
|
||||
} else {
|
||||
let lc: String = native_list_get(chars, i)
|
||||
if lc == "\n" {
|
||||
let scanning = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
}
|
||||
} else {
|
||||
if next_ch == "*" {
|
||||
// Skip until matching "*/".
|
||||
let i = i + 2
|
||||
let scanning2 = true
|
||||
while scanning2 {
|
||||
if i >= total {
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let bc: String = native_list_get(chars, i)
|
||||
if bc == "*" {
|
||||
let after = i + 1
|
||||
if after < total {
|
||||
let nc2: String = native_list_get(chars, after)
|
||||
if nc2 == "/" {
|
||||
let i = after + 1
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Open a JS string?
|
||||
if ch == "'" {
|
||||
let in_squote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let in_dquote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "`" {
|
||||
let in_btick = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "\\" {
|
||||
// escape: peek next char
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "\"" {
|
||||
let parts = native_list_append(parts, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let parts = native_list_append(parts, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let parts = native_list_append(parts, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let parts = native_list_append(parts, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let parts = native_list_append(parts, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let parts = native_list_append(parts, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
let i: Int = 0
|
||||
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
// Skip whitespace
|
||||
if lex_is_whitespace(ch) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Line comments: //
|
||||
if ch == "/" {
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "/" {
|
||||
// skip to end of line
|
||||
let i = i + 2
|
||||
let running2 = true
|
||||
while running2 {
|
||||
if i >= total {
|
||||
let running2 = false
|
||||
} else {
|
||||
let lch: String = native_list_get(chars, i)
|
||||
if lch == "\n" {
|
||||
let running2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// String literal
|
||||
if ch == "\"" {
|
||||
let result = scan_string(chars, i + 1, total)
|
||||
let str_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// Compile-time scrub: strings that embed JS or CSS get
|
||||
// their // line comments and /* block comments stripped
|
||||
// before the token reaches the parser. Plain prose passes
|
||||
// through untouched.
|
||||
let clean_text = str_text
|
||||
if looks_like_code(str_text) {
|
||||
let clean_text = strip_code_comments(str_text)
|
||||
}
|
||||
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Number literal
|
||||
if lex_is_digit(ch) {
|
||||
let result = scan_digits(chars, i, total)
|
||||
let num_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// check for float (dot followed by digit)
|
||||
if new_pos < total {
|
||||
let dot_ch: String = native_list_get(chars, new_pos)
|
||||
if dot_ch == "." {
|
||||
let after_dot = new_pos + 1
|
||||
if after_dot < total {
|
||||
let after_dot_ch: String = native_list_get(chars, after_dot)
|
||||
if lex_is_digit(after_dot_ch) {
|
||||
let frac_result = scan_digits(chars, after_dot, total)
|
||||
let frac_text: String = frac_result["text"]
|
||||
let frac_pos: Int = frac_result["pos"]
|
||||
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
|
||||
let i = frac_pos
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
// Identifier or keyword
|
||||
if lex_is_alpha(ch) || ch == "_" {
|
||||
let result = scan_ident(chars, i, total)
|
||||
let word: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
let kw = keyword_kind(word)
|
||||
if kw == "" {
|
||||
let tokens = native_list_append(tokens, make_tok("Ident", word))
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok(kw, word))
|
||||
}
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Multi-char and single-char operators/delimiters
|
||||
let peek_i = i + 1
|
||||
let peek_ch = ""
|
||||
if peek_i < total {
|
||||
let peek_ch: String = native_list_get(chars, peek_i)
|
||||
}
|
||||
|
||||
if ch == "=" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Eq", "="))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "!" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Not", "!"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "<" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ">" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "&" {
|
||||
if peek_ch == "&" {
|
||||
let tokens = native_list_append(tokens, make_tok("And", "&&"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "|" {
|
||||
if peek_ch == "|" {
|
||||
let tokens = native_list_append(tokens, make_tok("Or", "||"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "-" {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ":" {
|
||||
if peek_ch == ":" {
|
||||
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "+" {
|
||||
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "*" {
|
||||
let tokens = native_list_append(tokens, make_tok("Star", "*"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "%" {
|
||||
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "(" {
|
||||
let tokens = native_list_append(tokens, make_tok("LParen", "("))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == ")" {
|
||||
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "{" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "[" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "]" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "," {
|
||||
let tokens = native_list_append(tokens, make_tok("Comma", ","))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == ";" {
|
||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "@" {
|
||||
let tokens = native_list_append(tokens, make_tok("At", "@"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "?" {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tokens = native_list_append(tokens, make_tok("Eof", ""))
|
||||
tokens
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import "language-profile.el"
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a_nodedup.el"
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -0,0 +1 @@
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -0,0 +1,6 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a.el"
|
||||
|
||||
fn main_fn(x: String) -> String {
|
||||
return x
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a.el"
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a_notail.el"
|
||||
+159
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_get(json: String, key: String) -> String
|
||||
extern fn generate_frame(frame: [String]) -> String
|
||||
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
|
||||
extern fn generate(semantic_form_json: String) -> String
|
||||
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -0,0 +1,2 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_b(x: String) -> String
|
||||
@@ -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, "?")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn slots_get(slots: [String], key: String) -> String
|
||||
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
|
||||
extern fn make_slots(k0: String, v0: String) -> [String]
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
|
||||
extern fn rule_id(rule: [String]) -> String
|
||||
extern fn rule_lhs(rule: [String]) -> String
|
||||
extern fn rule_rhs_len(rule: [String]) -> Int
|
||||
extern fn rule_rhs(rule: [String], idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
|
||||
extern fn build_rules() -> [[String]]
|
||||
extern fn get_rules() -> [[String]]
|
||||
extern fn find_rule(rule_id_str: String) -> [String]
|
||||
extern fn make_leaf(label: String, word: String) -> String
|
||||
extern fn make_node1(label: String, child0: String) -> String
|
||||
extern fn make_node2(label: String, child0: String, child1: String) -> String
|
||||
extern fn make_node3(label: String, child0: String, child1: String, child2: String) -> String
|
||||
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
|
||||
extern fn nlg_is_ws(c: String) -> Bool
|
||||
extern fn skip_ws(s: String, pos: Int) -> Int
|
||||
extern fn scan_token(s: String, start: Int) -> [String]
|
||||
extern fn render_tree(tree: String) -> String
|
||||
extern fn gram_word_order(profile: [String]) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
|
||||
extern fn gram_question_strategy(profile: [String]) -> String
|
||||
extern fn is_pronoun(word: String) -> Bool
|
||||
extern fn build_np(referent: String, slots: [String]) -> String
|
||||
extern fn build_pp(loc: String) -> String
|
||||
extern fn build_vp_body(slots: [String]) -> String
|
||||
extern fn build_vp_from_slots(slots: [String]) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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. 1100–1500): 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. 1000–1300 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. 750–1050 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. 600–900 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. 500–1000 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. 525–330 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. 1900–1600 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. 1400–1200 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. 2100–1300 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. 3000–2000 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. 4th–7th 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. 3rd–11th 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")
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fn morph_tiny(x: String) -> String {
|
||||
return x
|
||||
}
|
||||
@@ -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. 2800–100 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 (wē)
|
||||
// 4 = 2nd plural (gē)
|
||||
// 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ā gǣst gǣþ gāð gāð gāð
|
||||
// Past: ēode ēodest ēode ēodon ēodon ēodon
|
||||
|
||||
fn ang_gan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "gā" }
|
||||
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ō dēst dēþ dōð dōð dōð
|
||||
// Past: dyde dydest dyde dydon dydon dydon
|
||||
|
||||
fn ang_don_present(slot: Int) -> String {
|
||||
if slot == 0 { return "dō" }
|
||||
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 "eþ" }
|
||||
if slot == 3 { return "aþ" }
|
||||
if slot == 4 { return "aþ" }
|
||||
return "aþ"
|
||||
}
|
||||
|
||||
// ── 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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+2C80–U+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. 200–1000 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 e→ie 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 e→i 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 e→i 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 e→i + 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 "aß" }
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. 2000–1300 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 2–4 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "sé" }
|
||||
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 + "ió" }
|
||||
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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "nä" }
|
||||
if str_eq(base, "ksi") { return "ksi" }
|
||||
if str_eq(base, "tta") { return "ttä" }
|
||||
if str_eq(base, "ta") { return "tä" }
|
||||
if str_eq(base, "ja") { return "jä" }
|
||||
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 "kö" }
|
||||
if str_eq(base, "pa") { return "pä" }
|
||||
if str_eq(base, "va") { return "vä" }
|
||||
if str_eq(base, "ma") { return "mä" }
|
||||
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, "dä") {
|
||||
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 "kö" }
|
||||
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
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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 "né" }
|
||||
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, "té") { 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 + " ?"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. 900–1400 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. 1000–1300).
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. 300–900 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+1200–U+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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. 750–1050 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 (dū)
|
||||
// 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: ai→a, 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: i→a):
|
||||
// 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: i→a):
|
||||
// 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 "iþ" }
|
||||
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 "oþ" }
|
||||
if slot == 3 { return "om" }
|
||||
if slot == 4 { return "oþ" }
|
||||
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 nom≠acc 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "sé" }
|
||||
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 "sá" }
|
||||
if slot == 1 { return "sást" }
|
||||
if slot == 2 { return "sá" }
|
||||
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 + "ið" }
|
||||
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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "dā" }
|
||||
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, "dā") {
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 root→stem
|
||||
// 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+n→ena)
|
||||
// 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 + "yā" }
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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·cí (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 (mé)
|
||||
// 1 = 2nd singular (tú)
|
||||
// 2 = 3rd singular (é/sí/ed)
|
||||
// 3 = 1st plural (sní)
|
||||
// 4 = 2nd plural (sí)
|
||||
// 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·cí (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·cí (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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 dù "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 "dù" }
|
||||
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 "eš"
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "yä" }
|
||||
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, "yä") {
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. 1400–1200 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 (2–3 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
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn uga_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn uga_str_len(s: String) -> Int
|
||||
extern fn uga_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn uga_slot(person: String, number: String) -> Int
|
||||
extern fn uga_slot_g(person: String, gender: String, number: String) -> Int
|
||||
extern fn uga_kn_perfect(slot: Int) -> String
|
||||
extern fn uga_kn_imperfect(slot: Int) -> String
|
||||
extern fn uga_is_copula(verb: String) -> Bool
|
||||
extern fn uga_conjugate_copula(tense: String, slot: Int) -> String
|
||||
extern fn uga_hlk_perfect(slot: Int) -> String
|
||||
extern fn uga_hlk_imperfect(slot: Int) -> String
|
||||
extern fn uga_ray_perfect(slot: Int) -> String
|
||||
extern fn uga_ray_imperfect(slot: Int) -> String
|
||||
extern fn uga_amr_perfect(slot: Int) -> String
|
||||
extern fn uga_amr_imperfect(slot: Int) -> String
|
||||
extern fn uga_generic_perfect(base3sg: String, slot: Int) -> String
|
||||
extern fn uga_generic_imperfect(base3sg: String, slot: Int) -> String
|
||||
extern fn uga_known_verb(verb: String, tense: String, slot: Int) -> String
|
||||
extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn uga_strip_nom(noun: String) -> String
|
||||
extern fn uga_is_fem(noun: String) -> Bool
|
||||
extern fn uga_decline(noun: String, gram_case: String, number: String) -> String
|
||||
extern fn uga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
|
||||
extern fn uga_map_canonical(verb: String) -> String
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user