Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a816b119e7 | |||
| ba6e36c3f7 |
@@ -39,9 +39,9 @@ jobs:
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -54,9 +54,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.js
|
||||
--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
|
||||
@@ -291,9 +291,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
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
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ jobs:
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -176,9 +176,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--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
|
||||
@@ -275,9 +275,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
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
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ jobs:
|
||||
mkdir -p dist/platform
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -62,9 +62,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
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
|
||||
@@ -216,8 +216,8 @@ jobs:
|
||||
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/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/runtime/el_runtime.h dist/sdk/runtime/
|
||||
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"
|
||||
@@ -274,8 +274,8 @@ jobs:
|
||||
|
||||
# Per-file assets (downstream CI needs these individually)
|
||||
upload_asset lang/dist/platform/elc elc
|
||||
upload_asset lang/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/runtime/el_runtime.h el_runtime.h
|
||||
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
|
||||
@@ -319,7 +319,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.js
|
||||
--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
|
||||
@@ -367,9 +367,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
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
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
LANG_DIR="$ROOT/lang"
|
||||
RUNTIME="$LANG_DIR/runtime"
|
||||
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 runtime dist/elc-bootstrap.c runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I runtime /tmp/elc.c runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# ELP language consolidation — full-lexicon backfill (stage)
|
||||
|
||||
Branch: `stage-elp-lang-consolidation` (stage-bound; NOT the live soul :8742).
|
||||
|
||||
Consolidates scattered Python language-realizer work (`~/Desktop/lang-realizers`,
|
||||
`~/Desktop/lang-poetry-experiment`, `~/semitic_engine`) into the ELP `.el`
|
||||
structure, generating **full lexicons** (complete UniMorph + kaikki.org
|
||||
Wiktionary — real gender, real inflections) instead of the demo/curated subsets
|
||||
the prototypes shipped.
|
||||
|
||||
## ELP before this branch
|
||||
- 18 classical/ancient languages fully done (vocab + morphology + tests):
|
||||
akk ang cop egy enm fro gez goh got grc non peo pi sa sga sux txb uga.
|
||||
- 11 modern/classical languages had `morphology-<code>.el` in the build manifest
|
||||
but **no vocabulary and no lang_profile**: es fr de ja ar he hi ru fi sw la.
|
||||
- The ES port (`stage-elp-es-port`) had a *demo-scale* vocabulary-es.el (~350
|
||||
entries, s-expr form).
|
||||
|
||||
## Landed on this branch (full-lexicon seed-fn format, matching the 18 ancients)
|
||||
Vocabulary schema per row: `[lemma, pos, form0, form1, form2, en_gloss, hint]`.
|
||||
Files are ELP runtime **seed data** (loaded via the Engram at runtime), so — like
|
||||
all 18 classical `vocabulary-*.el` — they are intentionally NOT in the build
|
||||
manifest. Syntax validated: the chunked `fn vocab_<code>_seed_pN` format
|
||||
compiles cleanly to C via `elc` (correct UTF-8).
|
||||
|
||||
| code | in-ELP-morph? | vocab entries | verbs | nouns | adjs | profile |
|
||||
|------|---------------|--------------:|------:|------:|-----:|---------|
|
||||
| es | yes | 72,032 | 6,695 | 48,353 | 16,984 | yes |
|
||||
| fr | yes | 130,517 | 7,534 | 77,344 | 45,639 | yes |
|
||||
| de | yes | 144,692 | 6,661 | 133,162 | 4,869 | yes |
|
||||
| la | yes | 22,590 | 82 | 13,436 | 9,072 | yes |
|
||||
| it | no (bonus) | 193,675 | 10,008 | 109,459 | 74,208 | yes |
|
||||
| pt | no (bonus) | 115,772 | 4,001 | 72,073 | 39,698 | yes |
|
||||
| ro | no (bonus) | 86,504 | 1,216 | 65,915 | 19,373 | yes |
|
||||
| ca | no (bonus) | 47,112 | 1,547 | 28,830 | 16,735 | yes |
|
||||
|**total**| |**812,894** | | | | |
|
||||
|
||||
Generators (reproducible): `elp/tests/lang-gen/gen_elp_seed_full.py` (Romance),
|
||||
`gen_elp_seed_de_la.py` (German declension + Latin case-paradigm mapping). They
|
||||
read the pre-built morph caches in `~/Desktop/lang-realizers/data/` (UniMorph +
|
||||
kaikki), which are too large to commit.
|
||||
|
||||
## Remaining (honest)
|
||||
Of the 11 ELP backfill targets, 4 are done (es fr de la). The other 7 have **no
|
||||
full-lexicon engine** yet — cannot be generated honestly without engine work:
|
||||
- **ru**: only a 110-entry curated Slavic subset exists; full `rus.unimorph`
|
||||
present but no `morphology_ru_full` productive loader. Needs a full Russian
|
||||
morphology module (like the Romance ones) before vocab generation.
|
||||
- **ja / ko / zh**: validated demo engines (~66-104 hardcoded words) in
|
||||
`lang-poetry-experiment`, Python only. Agglutinative (ja/ko) + isolating (zh)
|
||||
need `.el` engine ports + full-lexicon wiring (ja: jpn_unimorph; zh: CC-CEDICT).
|
||||
- **ar / he (Semitic)**: template engines (16 AR / 8 HE patterns, ~6 roots) in
|
||||
`~/semitic_engine`, Python only. Root-and-pattern; full UniMorph ara/heb
|
||||
present but used only for validation. Needs productive root lexicon + `.el` port.
|
||||
- **hi (Hindi), fi (Finnish), sw (Swahili)**: `morphology-<code>.el` exists in
|
||||
ELP but there is NO scattered prototype and NO downloaded data for these —
|
||||
full-lexicon collection (UniMorph/kaikki) + generator still to do.
|
||||
|
||||
De/nl/sv Germanic and it/ro/ca/pt Romance verb coverage note: German verbs here
|
||||
are the ~6.6k caches carry; the it/ro/ca/pt bonus languages have full vocab but
|
||||
**no `morphology-<code>.el` in ELP yet** (Python realizer exists; `.el` port is
|
||||
the remaining engine work).
|
||||
|
||||
Construction coverage (separate from lexicon): French realizer was ~55%,
|
||||
Semitic ~3% in the prototypes — full construction coverage remains its own task.
|
||||
@@ -0,0 +1,72 @@
|
||||
;;; lang_profile_ca.el — Catalan language profile for ELP.
|
||||
;;; Mirrors lang_profile_it / _es / _pt; keys the realizer's construction switches.
|
||||
;;; Catalan is the CLOSEST Romance sibling to the shared engine (~85% conceptual
|
||||
;;; reuse). The deltas: PRONOMS FEBLES with four position allomorphs, l'-elision,
|
||||
;;; del/al/pel contractions, the periphrastic preterite (vaig+INF), and NO
|
||||
;;; essere/avere split (perfect aux is always HAVER; ser/estar is only the copula).
|
||||
|
||||
(lang_profile_ca
|
||||
(language "Catalan")
|
||||
(iso639 "ca")
|
||||
(family "Romance")
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
|
||||
(do-support no)
|
||||
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'; no inversion
|
||||
(article-selection "el/la/l'/els/les ; un/una/uns/unes") ; l'-ELISION:
|
||||
; el/la -> l' before vowel or (silent) h, glued to
|
||||
; the next word (l'home, l'illa); de -> d' before vowel
|
||||
(article-drives-contraction yes) ; article choice feeds prep+article contraction
|
||||
(adjective-position "postnominal-default + small prenominal class") ; bo/bon,
|
||||
; mal, gran, nou, vell, primer, molt... prenominal
|
||||
(question-punct plain) ; ? and ! only (no inverted ¿ ¡)
|
||||
|
||||
;; ── MANDATORY prep+article contractions ────────────────────────────────
|
||||
(contractions ((de el del) (de els dels)
|
||||
(a el al) (a els als)
|
||||
(per el pel) (per els pels)))
|
||||
(contraction-mandatory yes) ; *de el -> del obligatory
|
||||
(contraction-blocked-before-elision yes) ; de l'home / a l'home (NO *del home)
|
||||
|
||||
;; ── clitic system: PRONOMS FEBLES (the headline delta) ──────────────────
|
||||
(clitics yes)
|
||||
(clitic-allomorphy four-position) ; per pronoun, form varies by position+onset:
|
||||
; reinforced (em, et, el) proclitic before a consonant
|
||||
; elided (m', t', l', n') proclitic before a vowel/h
|
||||
; full (-me, -lo, -li) enclitic after a consonant/-r
|
||||
; reduced ('m, 't, 'l, 'ns) enclitic after a vowel
|
||||
(clitic-placement ((finite proclitic) ; el veig, no m'ho dóna
|
||||
(imperative-affirmative enclitic) ; dóna'm, digues-me
|
||||
(imperative-negative present-subjunctive) ; no parlis (delta)
|
||||
(infinitive enclitic) ; ajudar-me, veure'l
|
||||
(gerund enclitic))) ; fent-ho
|
||||
(clitic-combination ((me el "me'l") (te el "te'l") (se el "se'l")
|
||||
(me la "me la") (me en "me'n")
|
||||
(li el "l'hi") (li en "n'hi"))) ; dative+accusative clusters
|
||||
(clitic-particles (hi en ho)) ; locative hi, partitive/genitive en, neuter ho
|
||||
|
||||
;; ── verb / aspect system ───────────────────────────────────────────────
|
||||
(finite-agreement "person+number (6-way)")
|
||||
(tenses (present imperfet preterit-simple perifrastic-preterit futur
|
||||
condicional subjuntiu-present subjuntiu-imperfet imperatiu))
|
||||
(periphrastic-preterite "vaig/vas/va/vam/vau/van + INFINITIVE") ; << hallmark CA
|
||||
; (vaig cantar = 'I sang'); coexists w/ synthetic pret.
|
||||
(compound-past "pretèrit perfet = haver(present) + participle")
|
||||
(perfect-aux "HAVER only") ; << NO essere/avere split (simpler than IT)
|
||||
(participle-agreement ((haver preceding-acc-clitic))) ; les he vistes; else invariable
|
||||
(progressive-aux "estar + gerundi")
|
||||
(copula "ser / estar") ; ser: identity/essential/origin; estar:
|
||||
; location + transient state (estic cansat, és a casa)
|
||||
(passive-aux "ser (+ per-agent)")
|
||||
(future inflectional) ; cantaré, serà
|
||||
(comparative "més/menys ADJ que")
|
||||
|
||||
;; ── SACRED safety bar (shared with es/pt/it/en) ────────────────────────
|
||||
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
(negation "no (preverbal) + optional 'pas' + concord") ; no...res/
|
||||
; ningú/mai/cap/gens/enlloc
|
||||
(negative-concord yes) ; preverbal negative subject (ningú) keeps 'no'
|
||||
(neg-reinforcer pas)) ; optional (no ho faré pas)
|
||||
@@ -0,0 +1,41 @@
|
||||
;;; lang_profile_de.el — German language profile for ELP.
|
||||
;;; Mirrors lang_profile_en / lang_profile_es. Keys the realizer's construction
|
||||
;;; switches. German is the largest Germanic delta from the EN engine: V2 word
|
||||
;;; order, four morphological cases, and separable-prefix verbs.
|
||||
|
||||
(lang_profile_de
|
||||
(language "German")
|
||||
(iso639 "de")
|
||||
(family "Germanic")
|
||||
(neighbor-base "en") ; realized by extending the English (Germanic) engine
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop no) ; obligatory subject in finite clauses
|
||||
(obligatory-subject yes)
|
||||
(grammatical-gender (m f n)) ; three genders; drives article + adj declension
|
||||
(case-system (nom acc dat gen)) ; four cases on articles/adjs/nouns
|
||||
(word-order V2) ; finite verb 2nd in main clause
|
||||
(subordinate-order verb-final) ; "..., dass er den Hund SIEHT."
|
||||
(separable-verbs yes) ; aufstehen -> "steht ... auf"; ppart "aufgestanden"
|
||||
(do-support no) ; German negates/questions the finite verb directly
|
||||
(subject-verb-inversion yes) ; yes/no Q fronts finite verb; wh-Q fills Vorfeld
|
||||
(article-selection "der/die/das + ein/kein") ; declined by case x gender x number
|
||||
(adjective-position prenominal)
|
||||
(adjective-declension (strong weak mixed)) ; chosen by the determiner type
|
||||
(noun-capitalization yes)
|
||||
|
||||
;; ── verb / aspect system ───────────────────────────────────────────────
|
||||
(finite-agreement "person-and-number") ; full present/past paradigm
|
||||
(auxiliary-order (modal tense-aux perfect passive main))
|
||||
(perfect-aux (haben sein)) ; sein for intransitive motion/change verbs
|
||||
(passive-aux "werden")
|
||||
(future "werden + infinitive")
|
||||
(comparative "synthetic (-er / -st, with umlaut)")
|
||||
|
||||
;; ── negation ───────────────────────────────────────────────────────────
|
||||
(negation-markers (nicht kein)) ; kein- negates an indefinite NP; nicht else
|
||||
(negation-faithful yes) ; SACRED: polarity never dropped/inverted -> FLAG
|
||||
|
||||
;; ── lexicon provenance ─────────────────────────────────────────────────
|
||||
(lexicon-source "UniMorph deu (primary) + kaikki.org German (gender override)")
|
||||
(lexicon-license "CC-BY-SA 3.0 / GFDL"))
|
||||
@@ -0,0 +1,41 @@
|
||||
;;; lang_profile_en.el — English language profile for ELP.
|
||||
;;; Mirrors lang_profile_es / lang_profile_pt; keys the realizer's construction
|
||||
;;; switches. English is typologically distinct from the Romance builds, so the
|
||||
;;; flags differ where the grammar differs.
|
||||
|
||||
(lang_profile_en
|
||||
(language "English")
|
||||
(iso639 "en")
|
||||
(family "Germanic")
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop no) ; OBLIGATORY subjects — missing subject is FLAGGED
|
||||
(obligatory-subject yes)
|
||||
(grammatical-gender no) ; natural gender only (he/she/it), no NP agreement
|
||||
(do-support yes) ; negation & questions of lexical verbs insert do/does/did
|
||||
(subject-aux-inversion yes) ; yes/no + non-subject wh questions invert the operator
|
||||
(article-selection "a/an/the") ; a/an resolved PHONOLOGICALLY (an hour, a university)
|
||||
(adjective-position prenominal) ; attributive adjectives precede the noun; invariant
|
||||
(has-tag-questions yes) ; "...doesn't he?" — operator + reversed polarity
|
||||
(has-there-existential yes) ; "there is/are/have been ..."
|
||||
(possessive-clitic "'s") ; saxon genitive; plural in -s -> bare apostrophe
|
||||
(question-punct plain) ; ? and ! only (no inverted marks)
|
||||
|
||||
;; ── verb / aspect system ───────────────────────────────────────────────
|
||||
(finite-agreement "3sg-present-only") ; only 3sg present -s (+ suppletive be)
|
||||
(auxiliary-order (modal perfect progressive passive main))
|
||||
(perfect-aux "have") ; have + past participle
|
||||
(progressive-aux "be") ; be + present participle
|
||||
(passive-aux "be") ; be + past participle (+ by-agent)
|
||||
(future "will + base") ; no inflectional future
|
||||
(comparative "synthetic-or-periphrastic") ; -er/-est vs more/most by syllables
|
||||
|
||||
;; ── SACRED safety bar (shared with es/pt) ──────────────────────────────
|
||||
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
|
||||
;; ── DIALECT overlay (post-realization, one core -> US/UK/AU) ────────────
|
||||
(dialect US) ; default; profile field switches the overlay
|
||||
(dialects (US UK AU))
|
||||
(dialect-canonical US) ; core is authored in US orthography
|
||||
(dialect-overlay "dialect_en.to_dialect") ; orthography + lexis + grammar prefs
|
||||
(dialect-covers (spelling lexis collective-agreement gotten/got)))
|
||||
@@ -0,0 +1,45 @@
|
||||
;;; lang_profile_es.el — Spanish language profile for ELP.
|
||||
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
|
||||
|
||||
(lang_profile_es
|
||||
(language "Spanish")
|
||||
(iso639 "es")
|
||||
(family "Romance")
|
||||
|
||||
;; -- core typology flags -------------------------------------------------
|
||||
(pro-drop yes) ; subjects routinely dropped; agreement carries person
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
|
||||
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
|
||||
(do-support no)
|
||||
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
|
||||
(question-strategy intonation)
|
||||
(article-selection "el/la/los/las un/una/unos/unas")
|
||||
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
|
||||
(adjective-position postnominal) ; default post; a few prenominal + apocope
|
||||
(adjective-agreement "gender+number")
|
||||
(question-punct inverted) ; opening ¿ ¡ required
|
||||
|
||||
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
|
||||
(contractions ((de el "del") (a el "al")))
|
||||
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
|
||||
|
||||
;; -- verb / aspect system ------------------------------------------------
|
||||
(verb-classes (ar er ir))
|
||||
(tenses (present preterite imperfect future conditional))
|
||||
(moods (ind sbjv imp))
|
||||
(finite-agreement "person+number (6 slots)")
|
||||
(perfect-aux "haber") ; haber + past participle (invariant -o)
|
||||
(progressive-aux "estar") ; estar + gerund
|
||||
(passive-aux "ser") ; ser + participle (agrees) + por-agent
|
||||
(copula-split "ser/estar") ; permanent vs stage-level
|
||||
(future "infinitive + é/ás/á/emos/éis/án")
|
||||
|
||||
;; -- clitics / government ------------------------------------------------
|
||||
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
|
||||
(clitic-order "se II I III (le+lo -> se lo)")
|
||||
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
|
||||
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
|
||||
|
||||
;; -- SACRED safety bar (shared with en/pt) -------------------------------
|
||||
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
@@ -0,0 +1,74 @@
|
||||
;;; lang_profile_fr.el — French language profile for ELP.
|
||||
;;; Mirrors lang_profile_it / lang_profile_es; keys the realizer's construction
|
||||
;;; switches. French is a Romance sibling (~54% of the realizer code and the whole
|
||||
;;; clause-engine architecture reused), but carries the family's biggest surface
|
||||
;;; deltas: NOT pro-drop, DISCONTINUOUS negation, and an orthography/phonology
|
||||
;;; mismatch (elision, liaison) that makes exact-match genuinely hard.
|
||||
|
||||
(lang_profile_fr
|
||||
(language "French")
|
||||
(iso639 "fr")
|
||||
(family "Romance")
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop no) ; << French-specific: subject clitic OBLIGATORY
|
||||
(obligatory-subject yes) ; je/tu/il/elle/nous/vous/ils/elles always overt
|
||||
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
|
||||
(do-support no)
|
||||
(subject-aux-inversion optional) ; est-ce que (default) OR clitic inversion (vas-tu)
|
||||
(article-selection "le/la/l'/les ; un/une/des ; PARTITIVE du/de la/de l'/des")
|
||||
(article-drives-contraction yes) ; à+le=au, de+le=du feed off article choice
|
||||
(adjective-position "postnominal-default + prenominal-BAGS") ; beau/bon/grand/
|
||||
; petit/jeune/vieux/nouveau + ordinals prenominal
|
||||
; (beau->bel, nouveau->nouvel, vieux->vieil / vowel)
|
||||
(question-punct "space-before") ; French typography: ' ?' ' !' (no ¿¡)
|
||||
|
||||
;; ── elision (orthography/phonology mismatch — French-specific) ──────────
|
||||
(elision ((le l') (la l') (je j') (ne n') (de d') (que qu')
|
||||
(me m') (te t') (se s') (ce c'))) ; before vowel / h-muet
|
||||
(elision-h-muet yes) ; l'homme, l'hôpital (h-aspiré exception list kept)
|
||||
(liaison noted-not-modeled) ; phonological, not written in surface
|
||||
|
||||
;; ── MANDATORY prep+article contractions ────────────────────────────────
|
||||
(contractions ((à le au) (à les aux) (de le du) (de les des)))
|
||||
(contraction-mandatory yes) ; *à le -> au obligatory; à la / à l' uncontracted
|
||||
(partitive ((m-sg du) (f-sg "de la") (vowel "de l'") (pl des)))
|
||||
(partitive-under-neg "de") ; << gap in current build: 'ne … pas de pain'
|
||||
|
||||
;; ── clitic system ──────────────────────────────────────────────────────
|
||||
(clitics yes)
|
||||
(clitic-order (me te se nous vous | le la les | lui leur | y | en))
|
||||
(clitic-placement ((finite proclitic) ; je le lui donne
|
||||
(imperative-affirmative enclitic-hyphen) ; donne-le-moi
|
||||
(imperative-negative "ne+proclitic+verb+pas") ; ne le donne pas
|
||||
(infinitive enclitic))) ; PARTIAL: clitic-climbing
|
||||
; onto infinitive under modal
|
||||
(clitic-imperative-shift ((me moi) (te toi))) ; final me/te -> moi/toi (donne-moi)
|
||||
(clitic-particles (y en)) ; locative y, partitive/genitive en
|
||||
|
||||
;; ── verb / aspect system ───────────────────────────────────────────────
|
||||
(finite-agreement "person+number (written; many homophones)")
|
||||
(tenses (présent imparfait passé-simple futur conditionnel
|
||||
subjonctif-présent subjonctif-imparfait impératif))
|
||||
(compound-past "passé-composé = aux(present) + participe passé")
|
||||
(perfect-aux "être/avoir (LEXICAL selection)") ; << French-specific
|
||||
(etre-aux-class "intransitive motion/change (aller venir arriver partir
|
||||
entrer sortir monter descendre naître mourir rester
|
||||
tomber retourner passer devenir revenir rentrer) + ALL
|
||||
pronominal verbs")
|
||||
(participle-agreement ((être subject) ; elle est allée / elles venues
|
||||
(avoir preceding-direct-object))) ; je les ai vus
|
||||
(progressive "être en train de + infinitif") ; no dedicated aux
|
||||
(copula "être (single; no ser/estar, no essere/stare)")
|
||||
(passive-aux "être (+ par-agent)")
|
||||
(future inflectional) ; parlera, sera
|
||||
(comparative "plus/moins ADJ que")
|
||||
(superlative "le/la plus ADJ (de …)") ; PARTIAL word-order in build
|
||||
|
||||
;; ── SACRED safety bar (shared with es/pt/it/en) ────────────────────────
|
||||
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
(negation "DISCONTINUOUS: ne (preverbal) … pas/jamais/rien/personne/
|
||||
plus/guère/que (postverbal)") ; << biggest structural delta
|
||||
(negation-ne-elides yes) ; ne -> n' before vowel (n'ai pas vu)
|
||||
(negation-passe-composé "ne + aux + pas + participe") ; n'ai pas vu
|
||||
(negative-concord partial)) ; personne/rien as arguments post-participle
|
||||
@@ -0,0 +1,70 @@
|
||||
;;; lang_profile_it.el — Italian language profile for ELP.
|
||||
;;; Mirrors lang_profile_es / lang_profile_pt; keys the realizer's construction
|
||||
;;; switches. Italian is a Romance sibling, so ~85% of the flags match ES/PT; the
|
||||
;;; essere/avere auxiliary split and phonological article selection are the deltas.
|
||||
|
||||
(lang_profile_it
|
||||
(language "Italian")
|
||||
(iso639 "it")
|
||||
(family "Romance")
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
|
||||
(do-support no)
|
||||
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'; no inversion
|
||||
(article-selection "il/lo/l'/i/gli + la/l'/le ; un/uno/un'/una") ; PHONOLOGICAL:
|
||||
; lo/gli/uno before s+cons, z, gn, ps, pn, x, y, i+V;
|
||||
; l'/un' before a vowel (elision, glued to next word)
|
||||
(article-drives-contraction yes) ; article choice feeds the prep+art contraction
|
||||
(adjective-position "postnominal-default + prenominal-class") ; bello/buono/grande
|
||||
; /nuovo/vecchio/primo... prenominal (with apocope)
|
||||
(question-punct plain) ; ? and ! only (no inverted ¿ ¡)
|
||||
|
||||
;; ── MANDATORY prep+article contractions ────────────────────────────────
|
||||
(contractions ((di il del) (di lo dello) (di la della) (di i dei)
|
||||
(di gli degli) (di le delle) (di l' dell')
|
||||
(a il al) (a lo allo) (a la alla) (a i ai) (a gli agli)
|
||||
(a le alle) (a l' all')
|
||||
(da il dal) (da la dalla) (da gli dagli) (da l' dall')
|
||||
(in il nel) (in la nella) (in gli negli) (in l' nell')
|
||||
(su il sul) (su la sulla) (su gli sugli) (su l' sull')))
|
||||
(contraction-mandatory yes) ; *di il -> del is obligatory, never uncontracted
|
||||
(prep-no-contract (per tra fra)) ; per la strada (NOT *perla)
|
||||
|
||||
;; ── clitic system ──────────────────────────────────────────────────────
|
||||
(clitics yes)
|
||||
(clitic-placement ((finite proclitic) ; lo vedo, non me lo dà
|
||||
(imperative-affirmative enclitic) ; dammelo, guardalo
|
||||
(imperative-negative-tu non+infinitive) ; non parlare / non lo fare
|
||||
(infinitive enclitic) ; vederlo, aiutarmi (drop -e)
|
||||
(gerund enclitic))) ; dandolo
|
||||
(clitic-combination ((mi lo "me lo") (ti lo "te lo") (ci lo "ce lo")
|
||||
(vi lo "ve lo") (si lo "se lo")
|
||||
(gli lo "glielo") (le lo "glielo"))) ; glielo = ONE word
|
||||
(clitic-particles (ci ne)) ; locative ci, partitive ne
|
||||
(raddoppiamento (da fa di va sta)) ; monosyllabic imper double clitic: dammelo
|
||||
|
||||
;; ── verb / aspect system ───────────────────────────────────────────────
|
||||
(finite-agreement "person+number (6-way)")
|
||||
(tenses (presente imperfetto passato-remoto futuro condizionale
|
||||
congiuntivo-presente congiuntivo-imperfetto imperativo))
|
||||
(compound-past "passato-prossimo = aux(present) + participle")
|
||||
(perfect-aux "essere/avere (LEXICAL selection)") ; << Italian-specific
|
||||
(essere-aux-class unaccusative) ; motion/change-of-state/copular/pronominal
|
||||
; (andare venire nascere morire diventare piacere
|
||||
; + ALL reflexives) -> essere
|
||||
(participle-agreement ((essere subject) ; è andata / sono arrivati
|
||||
(avere preceding-acc-clitic))) ; li ho visti
|
||||
(progressive-aux "stare + gerundio") ; sto parlando
|
||||
(copula "essere (default) / stare (state: sto bene)")
|
||||
(passive-aux "essere / venire (+ da-agent)")
|
||||
(future inflectional) ; parlerò, sarà
|
||||
(comparative "più/meno ADJ di")
|
||||
|
||||
;; ── SACRED safety bar (shared with es/pt/en) ───────────────────────────
|
||||
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
(negation "non (preverbal) + concord") ; non...niente/nessuno/mai/più
|
||||
(negative-concord yes) ; preverbal negative word (nessuno/niente) suppresses non
|
||||
(neg-adverb-position between-aux-and-participle)) ; non ho MAI visto
|
||||
@@ -0,0 +1,30 @@
|
||||
;;; lang_profile_la.el — Latin language profile for ELP.
|
||||
;;; Keys the realizer's construction switches. Companion to morphology-la.el.
|
||||
|
||||
(lang_profile_la
|
||||
(language "Latin")
|
||||
(iso639 "la")
|
||||
(family "Italic")
|
||||
|
||||
;; -- core typology flags -------------------------------------------------
|
||||
(pro-drop yes) ; person carried by verb ending; subjects dropped
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m/f/n; adjective AGREES in case+gender+number
|
||||
(gender-source lexicon) ; REAL per-noun gender from UniMorph lat
|
||||
(articles none) ; Latin has no articles
|
||||
(case-system yes) ; NOM GEN DAT ACC ABL VOC (+ rare LOC)
|
||||
(cases (nom gen dat acc abl voc))
|
||||
(word-order "SOV (default; free order, case-marked)")
|
||||
(adjective-position "either (case agreement carries the link)")
|
||||
(adjective-agreement "case+gender+number")
|
||||
|
||||
;; -- verb / aspect system ------------------------------------------------
|
||||
(verb-classes (1 2 3 3io 4)) ; four conjugations + i-stem 3rd
|
||||
(tenses (present imperfect future perfect pluperfect futureperfect))
|
||||
(moods (indicative subjunctive imperative infinitive))
|
||||
(voices (active passive))
|
||||
(finite-agreement "person+number (6 slots)")
|
||||
(citation "principal parts: pres-1sg / pres-inf / perf-participle")
|
||||
|
||||
;; -- SACRED safety bar ---------------------------------------------------
|
||||
(negation-faithful yes)) ; polarity never dropped/inverted
|
||||
@@ -0,0 +1,40 @@
|
||||
;;; lang_profile_pt.el — Portuguese language profile for ELP.
|
||||
;;; Keys the realizer's construction switches. Mirrors lang_profile_es.
|
||||
|
||||
(lang_profile_pt
|
||||
(language "Portuguese")
|
||||
(iso639 "pt")
|
||||
(family "Romance")
|
||||
|
||||
;; -- core typology flags -------------------------------------------------
|
||||
(pro-drop yes) ; subjects routinely dropped; agreement carries person
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
|
||||
(gender-source lexicon) ; REAL per-noun gender from UniMorph por / kaikki
|
||||
(do-support no)
|
||||
(subject-aux-inversion no)
|
||||
(question-strategy intonation)
|
||||
(article-selection "o/a/os/as um/uma/uns/umas")
|
||||
(adjective-position postnominal)
|
||||
(adjective-agreement "gender+number")
|
||||
|
||||
;; -- MANDATORY CONTRACTIONS (prep + article) -----------------------------
|
||||
(contractions ((de o "do") (de a "da") (em o "no") (em a "na")
|
||||
(a o "ao") (a a "à") (por o "pelo") (por a "pela")))
|
||||
(contraction-mandatory yes)
|
||||
|
||||
;; -- verb / aspect system ------------------------------------------------
|
||||
(verb-classes (ar er ir))
|
||||
(tenses (present preterite imperfect future conditional))
|
||||
(moods (ind sbjv imp))
|
||||
(finite-agreement "person+number (6 slots)")
|
||||
(perfect-aux "ter") ; ter + past participle
|
||||
(copula-split "ser/estar")
|
||||
(personal-infinitive yes) ; distinctive PT inflected infinitive
|
||||
|
||||
;; -- clitics / government ------------------------------------------------
|
||||
(object-clitics yes) ; mesoclisis/enclisis/proclisis by context
|
||||
(verb-prep-government yes)
|
||||
|
||||
;; -- SACRED safety bar ---------------------------------------------------
|
||||
(negation-faithful yes))
|
||||
@@ -0,0 +1,71 @@
|
||||
;;; lang_profile_ro.el — Romanian language profile for ELP.
|
||||
;;; Romanian is the BIG typological delta of the Romance family. The verb/clause
|
||||
;;; engine and the SACRED negation contract mirror the ES/PT/IT core, but the
|
||||
;;; NOMINAL system is genuinely new: a SUFFIXED definite article, preserved CASE,
|
||||
;;; a NEUTER gender, and a VOCATIVE. Those flags mark where the shared engine was
|
||||
;;; extended rather than reused.
|
||||
|
||||
(lang_profile_ro
|
||||
(language "Romanian")
|
||||
(iso639 "ro")
|
||||
(family "Romance (Eastern / Balkan)")
|
||||
|
||||
;; ── core typology flags ────────────────────────────────────────────────
|
||||
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
|
||||
(obligatory-subject no)
|
||||
(grammatical-gender yes) ; m / f / NEUTER (n)
|
||||
(neuter-gender yes) ; << ROMANIAN-SPECIFIC: masc-agreeing SG, fem-agreeing PL
|
||||
; (un tren nou / două trenuri noi)
|
||||
(do-support no)
|
||||
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'
|
||||
(question-punct plain) ; ? and ! only
|
||||
|
||||
;; ── SUFFIXED DEFINITE ARTICLE (the headline engine extension) ───────────
|
||||
(definite-article suffixed) ; << UNIQUE IN ROMANCE: enclitic on the noun
|
||||
(definite-forms ((m/n sg "-ul / -le / -l : om->omul, câine->câinele, codru->codrul")
|
||||
(f sg "-a / -ea / -ua : casă->casa, carte->cartea, stea->steaua")
|
||||
(m pl "-i : oameni->oamenii")
|
||||
(f/n pl "-le : case->casele, trenuri->trenurile")))
|
||||
(article-host ((no-prenom-adj noun) ; omul bun
|
||||
(prenom-adj adjective))) ; bunul om (adj carries the article)
|
||||
(indefinite-article ((m/n "un") (f "o") (pl "niște") (gen/dat-pl "unor")))
|
||||
|
||||
;; ── CASE (preserved; NOM/ACC vs GEN/DAT) ────────────────────────────────
|
||||
(case (nom/acc gen/dat vocative)) ; << ROMANIAN-SPECIFIC
|
||||
(case-syncretism "nom=acc ; gen=dat")
|
||||
(genitive-marking "gen/dat definite: -lui (m/n), -ei/-i (f), -lor (pl)")
|
||||
(genitival-article ((m sg "al") (f sg "a") (m pl "ai") (f/n pl "ale"))) ; o carte a lui
|
||||
(possession "definite-head + gen/dat possessor: casa băiatului")
|
||||
(vocative ((m sg "-ule/-e : omule, băiete") (f sg "-o : Mario, fato")
|
||||
(pl "-lor")))
|
||||
|
||||
;; ── verb / aspect system ────────────────────────────────────────────────
|
||||
(finite-agreement "person+number (6-way)")
|
||||
(tenses (prezent imperfect perfect-simplu conjunctiv-prezent
|
||||
imperativ (periphrastic: perfect-compus viitor conditional)))
|
||||
(compound-past "perfectul compus = a-avea-clitic + INVARIABLE participle")
|
||||
(perfect-aux "a avea (am/ai/a/am/ați/au) — ONE auxiliary for ALL verbs")
|
||||
(perfect-aux-split no) ; << SIMPLER than Italian: no essere/avere selection
|
||||
(participle-agreement none) ; invariable in the perfect compus (agrees only as
|
||||
; an adjective / in the passive)
|
||||
(future "voi/vei/va/vom/veți/vor + infinitive (viitor literar)")
|
||||
(conditional "aș/ai/ar/am/ați/ar + infinitive")
|
||||
(subjunctive "conjunctiv: particle 'să' + subjunctive present")
|
||||
(modal-complement "modal + să + subjunctive (vreau să merg, poți să ajuți)")
|
||||
(copula "a fi")
|
||||
(passive "a fi + participle (participle AGREES like an adjective)")
|
||||
(comparative "mai / mai puțin ADJ decât")
|
||||
|
||||
;; ── clitic system (partial — see honest gaps) ───────────────────────────
|
||||
(clitics yes)
|
||||
(clitic-set ((acc mă te îl o ne vă îi le) (dat îmi îți îi ne vă le)
|
||||
(refl mă te se ne vă se)))
|
||||
(clitic-placement ((finite proclitic) ; îmi place, o văd
|
||||
(perfect-compus elision) ; << m-am, l-am, i-am (PARTIAL)
|
||||
(imperative-affirmative enclitic))) ; dă-mi (PARTIAL)
|
||||
|
||||
;; ── SACRED safety bar (shared with es/pt/it/en) ─────────────────────────
|
||||
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
|
||||
(negation "nu (single preverbal marker) + concord")
|
||||
(negative-concord yes) ; nu … nimic / nimeni / niciodată / niciun
|
||||
(negative-imperative "nu + INFINITIVE : nu pleca! (KNOWN GAP: uses imperative stem)"))
|
||||
File diff suppressed because it is too large
Load Diff
+144861
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+130676
File diff suppressed because it is too large
Load Diff
+193894
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+115916
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Full-lexicon vocabulary-{de,la}.el emitters (custom field mapping for the
|
||||
German declension/gender API and the Latin case-paradigm API). Reuses the
|
||||
chunked seed-fn writer from gen_elp_seed_full.
|
||||
"""
|
||||
import sys, importlib
|
||||
from gen_elp_seed_full import write_seed
|
||||
|
||||
def uw(x):
|
||||
"""Unwrap (form, source) tuples that some morphology fns return."""
|
||||
if isinstance(x, (tuple, list)):
|
||||
return x[0] if x else ""
|
||||
return x if x is not None else ""
|
||||
|
||||
def build_de():
|
||||
M = importlib.import_module("morphology_de_full")
|
||||
rows = []; st = {"verbs":0,"nouns":0,"adjs":0}
|
||||
# nouns: form0=nom-sg(lemma) form1=plural form2=gender
|
||||
for lem in sorted(M._NOUNS):
|
||||
if not lem: continue
|
||||
try:
|
||||
g = uw(M.noun_gender(lem))
|
||||
pl = uw(M.pluralize(lem))
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "noun", lem, pl, g or "", "", "gender:lexicon"])
|
||||
st["nouns"] += 1
|
||||
# adjs: form0=positive form1=comparative form2=superlative
|
||||
for lem in sorted(M._ADJS):
|
||||
if not lem: continue
|
||||
try:
|
||||
cmpr = uw(M.comparative(lem))
|
||||
sprl = uw(M.superlative(lem))
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "adj", lem, cmpr, sprl, "", "degree:lexicon"])
|
||||
st["adjs"] += 1
|
||||
# verbs (only the ~30 irregular/strong stems the cache carries):
|
||||
# form0=pres-3sg form1=past-3sg form2=past-participle
|
||||
if hasattr(M, "_VERBS"):
|
||||
for lem in sorted({k[0] if isinstance(k, tuple) else k for k in M._VERBS}):
|
||||
if not lem: continue
|
||||
try:
|
||||
f0 = uw(M.finite(lem, "present", "third", "singular"))
|
||||
f1 = uw(M.finite(lem, "past", "third", "singular"))
|
||||
pp = uw(M.past_participle(lem))
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "verb", f0, f1, pp, "", "class:strong/irregular"])
|
||||
st["verbs"] += 1
|
||||
return rows, st
|
||||
|
||||
def build_la():
|
||||
M = importlib.import_module("morphology_lat_full")
|
||||
rows = []; st = {"verbs":0,"nouns":0,"adjs":0}
|
||||
def dn(lem, c, n):
|
||||
try:
|
||||
r = M.decline_noun(lem, c, n)
|
||||
return uw(r)
|
||||
except Exception:
|
||||
return ""
|
||||
# nouns: dictionary citation — form0=nom-sg form1=gen-sg form2=gender
|
||||
for lem in sorted(M._NOUNS):
|
||||
if not lem: continue
|
||||
nom = dn(lem, "NOM", "SG") or lem
|
||||
gen = dn(lem, "GEN", "SG")
|
||||
try: g = uw(M.noun_gender(lem))
|
||||
except Exception: g = ""
|
||||
rows.append([lem, "noun", nom, gen, g, "", "case-paradigm nom/gen-sg"])
|
||||
st["nouns"] += 1
|
||||
# adjs: three-gender nom-sg citation — form0=masc form1=fem form2=neut
|
||||
for lem in sorted(M._ADJS):
|
||||
if not lem: continue
|
||||
try:
|
||||
m = uw(M.decline_adj(lem, "NOM", "MASC", "SG")) or lem
|
||||
f = uw(M.decline_adj(lem, "NOM", "FEM", "SG"))
|
||||
nt = uw(M.decline_adj(lem, "NOM", "NEUT", "SG"))
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "adj", m, f, nt, "", "3-gender nom-sg"])
|
||||
st["adjs"] += 1
|
||||
# verbs: principal parts — form0=pres-ind-1sg form1=pres-infinitive form2=perf-participle
|
||||
if hasattr(M, "_VERBS"):
|
||||
for lem in sorted({k[0] if isinstance(k, tuple) else k for k in M._VERBS}):
|
||||
if not lem: continue
|
||||
try:
|
||||
f0 = uw(M.conjugate(lem, "present", "indicative", "active", "first", "singular"))
|
||||
inf = uw(M.infinitive(lem, "present", "active"))
|
||||
pp = uw(M.participle(lem, "perfect", "nom", "m", "singular"))
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "verb", f0, inf, pp, "", "principal-parts pres1sg/inf/pfppl"])
|
||||
st["verbs"] += 1
|
||||
return rows, st
|
||||
|
||||
if __name__ == "__main__":
|
||||
lang = sys.argv[1]; out = sys.argv[2]
|
||||
rows, st = build_de() if lang == "de" else build_la()
|
||||
total, _ = write_seed(lang, rows, st, out)
|
||||
print(f"{lang}: wrote {out} total={total} verbs={st['verbs']} nouns={st['nouns']} adjs={st['adjs']}")
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""gen_elp_seed_full.py — emit a FULL-lexicon vocabulary-{lang}.el in the
|
||||
established ELP seed-fn format (same as vocabulary-non.el / the 18 classical
|
||||
languages), iterating the ENTIRE morphology_{lang}_full lexicon (every verb,
|
||||
noun, adjective lemma) — NOT a curated demo core.
|
||||
|
||||
Schema per row: [lemma, pos, form0, form1, form2, en_translation, semantic_hint]
|
||||
Verbs: form0=pres-ind-3sg form1=preterite-3sg form2=past-participle
|
||||
Nouns: form0=singular form1=plural form2=REAL gender (lexicon)
|
||||
Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
|
||||
|
||||
Output structure (chunked to stay within the proven ~5k-append/function scale):
|
||||
fn vocab_{lang}_seed_pN(v) -> [[String]] { ... appends ... return v }
|
||||
fn vocab_{lang}_seed() -> [[String]] { chains all chunks; return v }
|
||||
fn vocab_{lang}_lookup(w) -> [String] { linear scan }
|
||||
|
||||
Usage: python3 gen_elp_seed_full.py <lang> <out.el>
|
||||
"""
|
||||
import sys, importlib
|
||||
|
||||
CHUNK = 5000
|
||||
|
||||
def esc(s):
|
||||
return str(s).replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
def row(fields):
|
||||
return " let v = native_list_append(v, [" + ", ".join(f'"{esc(f)}"' for f in fields) + "])"
|
||||
|
||||
def build_rows(lang, M):
|
||||
rows = []
|
||||
stats = {"verbs":0,"nouns":0,"adjs":0}
|
||||
has = lambda n: hasattr(M, n)
|
||||
|
||||
# --- verbs ---
|
||||
if has("_VERBS") and has("conjugate"):
|
||||
verbs = sorted({k[0] for k in M._VERBS})
|
||||
for lem in verbs:
|
||||
if not lem: continue
|
||||
try:
|
||||
f0, s0 = M.conjugate(lem, "ind", "present", "third", "singular")
|
||||
f1, _ = M.conjugate(lem, "ind", "preterite", "third", "singular")
|
||||
pp, _ = (M.participle(lem) if has("participle") else ("",""))
|
||||
except Exception:
|
||||
continue
|
||||
vclass = lem[-2:] if lem[-2:] in ("ar","er","ir","re") else lem[-2:]
|
||||
rows.append([lem, "verb", f0 or "", f1 or "", pp or "", "", "class:"+vclass+" src:"+str(s0)])
|
||||
stats["verbs"] += 1
|
||||
|
||||
# --- nouns ---
|
||||
if has("_NOUNS") and has("inflect_noun"):
|
||||
for lem in sorted(M._NOUNS):
|
||||
if not lem: continue
|
||||
try:
|
||||
sg, _ = M.inflect_noun(lem, "singular")
|
||||
pl, _ = M.inflect_noun(lem, "plural")
|
||||
g = M.noun_gender(lem) if has("noun_gender") else ""
|
||||
except Exception:
|
||||
continue
|
||||
src = "lexicon" if (isinstance(M._NOUNS.get(lem), dict) and M._NOUNS[lem].get("g")) else "heuristic"
|
||||
rows.append([lem, "noun", sg or lem, pl or "", g or "", "", "gender:"+src])
|
||||
stats["nouns"] += 1
|
||||
|
||||
# --- adjectives ---
|
||||
if has("_ADJS") and has("inflect_adj"):
|
||||
for lem in sorted(M._ADJS):
|
||||
if not lem: continue
|
||||
try:
|
||||
m_sg, _ = M.inflect_adj(lem, "m", "singular")
|
||||
f_sg, _ = M.inflect_adj(lem, "f", "singular")
|
||||
m_pl, _ = M.inflect_adj(lem, "m", "plural")
|
||||
except Exception:
|
||||
continue
|
||||
rows.append([lem, "adj", m_sg or lem, f_sg or "", m_pl or "", "", "src:lexicon"])
|
||||
stats["adjs"] += 1
|
||||
|
||||
return rows, stats
|
||||
|
||||
def write_seed(lang, rows, stats, out_path):
|
||||
"""Write vocabulary-{lang}.el in the chunked seed-fn format from prebuilt rows.
|
||||
Each row is a 7-field list [lemma,pos,f0,f1,f2,gloss,hint]."""
|
||||
total = len(rows)
|
||||
chunks = [rows[i:i+CHUNK] for i in range(0, total, CHUNK)] or [[]]
|
||||
L = []
|
||||
L.append(f"// vocabulary-{lang}.el — FULL {lang} lexicon for ELP surface realization.")
|
||||
L.append(f"// Generated by gen_elp_seed_full.py from morphology_{lang}_full")
|
||||
L.append(f"// (real UniMorph + kaikki.org Wiktionary forms; gender from lexicon, not heuristic).")
|
||||
L.append(f"// Entries: {total} (verbs={stats['verbs']} nouns={stats['nouns']} adjs={stats['adjs']})")
|
||||
L.append(f"// Schema: [lemma, pos, form0, form1, form2, en_translation, semantic_hint]")
|
||||
L.append(f"// verbs: form0=pres-3sg form1=pret-3sg form2=past-participle")
|
||||
L.append(f"// nouns: form0=sg form1=pl form2=REAL gender adjs: form0=m-sg form1=f-sg form2=m-pl")
|
||||
L.append("")
|
||||
for ci, ch in enumerate(chunks):
|
||||
L.append(f"fn vocab_{lang}_seed_p{ci}(v: [[String]]) -> [[String]] {{")
|
||||
for r in ch:
|
||||
L.append(row(r))
|
||||
L.append(" return v")
|
||||
L.append("}")
|
||||
L.append("")
|
||||
L.append(f"fn vocab_{lang}_seed() -> [[String]] {{")
|
||||
L.append(" let v: [[String]] = native_list_empty()")
|
||||
for ci in range(len(chunks)):
|
||||
L.append(f" let v = vocab_{lang}_seed_p{ci}(v)")
|
||||
L.append(" return v")
|
||||
L.append("}")
|
||||
L.append("")
|
||||
L.append(f"fn vocab_{lang}_lookup(word: String) -> [String] {{")
|
||||
L.append(f" let vocab: [[String]] = vocab_{lang}_seed()")
|
||||
L.append(" let n: Int = native_list_len(vocab)")
|
||||
L.append(" let i: Int = 0")
|
||||
L.append(" while i < n {")
|
||||
L.append(" let entry: [String] = native_list_get(vocab, i)")
|
||||
L.append(' if str_eq(native_list_get(entry, 0), word) { return entry }')
|
||||
L.append(" let i = i + 1")
|
||||
L.append(" }")
|
||||
L.append(" return native_list_empty()")
|
||||
L.append("}")
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(L) + "\n")
|
||||
return total, stats
|
||||
|
||||
def emit(lang, out_path):
|
||||
M = importlib.import_module(f"morphology_{lang}_full")
|
||||
rows, stats = build_rows(lang, M)
|
||||
return write_seed(lang, rows, stats, out_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
lang, out = sys.argv[1], sys.argv[2]
|
||||
total, stats = emit(lang, out)
|
||||
print(f"{lang}: wrote {out} total={total} verbs={stats['verbs']} nouns={stats['nouns']} adjs={stats['adjs']}")
|
||||
@@ -0,0 +1,572 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""morphology_ca_full.py — production-grade Catalan morphological generator.
|
||||
|
||||
Same design as morphology_it_full.py (its Romance sibling); Catalan-specific data.
|
||||
|
||||
VERBS
|
||||
UniMorph Catalan (github.com/unimorph/cat, CC-BY-SA 3.0)
|
||||
7,535 verb lemmas × paradigm, CLEAN orthography:
|
||||
present, imperfet (PST;IPFV), pretèrit simple (PST;PFV), futur,
|
||||
condicional (COND), subjuntiu present (SBJV;PRS) / imperfet (SBJV;PST),
|
||||
imperatiu (POS;IMP), infinitiu (NFIN), gerundi (V.CVB;PRS),
|
||||
participi (V.PTCP;PST) — WITH full gender+number agreement forms
|
||||
(cantat/cantada/cantats/cantades) stored directly.
|
||||
ca_irreg_verbs.json — verbs UniMorph MISSES or under-populates
|
||||
(anar, fer, plus core auxiliaries ser/haver/estar/tenir…), extracted from
|
||||
kaikki.org Catalan by build_ca_irreg.py. Priority layer. Supplies anar,
|
||||
whose present (vaig/vas/va/anem/aneu/van) is ALSO the PERIPHRASTIC-PRETERITE
|
||||
auxiliary (vaig cantar = 'I sang') — a hallmark Catalan construction.
|
||||
|
||||
NOUNS + ADJECTIVES — kaikki.org Catalan (Wiktionary extract, CC-BY-SA 3.0)
|
||||
noun lemmas WITH inherent gender + real plural (resolved PER LEMMA).
|
||||
adjective lemmas with real feminine + plural forms.
|
||||
|
||||
Fallbacks degrade, never crash:
|
||||
verbs : regular -ar/-er/-re/-ir rule generator (+ -car/-gar/-çar spelling).
|
||||
nouns : gender heuristic + rule pluralization (-a→-es with ç/c/g/j/qu/gu
|
||||
spelling changes; sibilant-final → -os; else -s). Ambiguous → FLAG.
|
||||
adjs : -o? no (Catalan masc often consonant/-e); fem -a rule + plural rule.
|
||||
|
||||
Confidence flag per form: "lexicon" | "rule" | "fallback" (low → FLAG).
|
||||
|
||||
Public API (used by realizer_ca.py):
|
||||
conjugate(lemma, mood, tense, person, number) -> (form, conf)
|
||||
peri_pret_aux(person, number) -> form # anar-present, for vaig+INF
|
||||
participle(lemma, gender, number) -> (form, conf)
|
||||
gerund(lemma) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"
|
||||
inflect_noun(lemma, number, gender=None) -> (form, conf)
|
||||
inflect_adj(lemma, gender, number) -> (form, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "cat.unimorph")
|
||||
_IRREG = os.path.join(_HERE, "data", "ca_irreg_verbs.json")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_ca.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "ca_morph_cache.pkl")
|
||||
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): {"IND", "PRS"},
|
||||
("ind", "imperfect"): {"IND", "PST", "IPFV"},
|
||||
("ind", "preterite"): {"IND", "PST", "PFV"},
|
||||
("ind", "future"): {"IND", "FUT"},
|
||||
("ind", "conditional"): {"COND"},
|
||||
("sbjv", "present"): {"SBJV", "PRS"},
|
||||
("sbjv", "imperfect"): {"SBJV", "PST"},
|
||||
("imp", "affirmative"): {"POS", "IMP"},
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
# ── verbs from UniMorph ──────────────────────────────────────────────────────────
|
||||
def _build_verbs():
|
||||
verbs = {}
|
||||
part = {} # lemma -> {("m","SG"):form, ("f","SG"):..., ("m","PL"):..., ("f","PL"):...}
|
||||
ger = {}
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
if head == "V.PTCP":
|
||||
if "PST" in f:
|
||||
g = "f" if "FEM" in f else "m"
|
||||
n = "PL" if "PL" in f else "SG"
|
||||
part.setdefault(lemma, {})[(g, n)] = form
|
||||
continue
|
||||
if head == "V.CVB":
|
||||
if "PRS" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
if head != "V":
|
||||
continue
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
for (mood, tense), req in _VERB_KEYMAP.items():
|
||||
if not req <= f:
|
||||
continue
|
||||
if tense == "imperfect" and "PFV" in f:
|
||||
continue
|
||||
if tense == "preterite" and "IPFV" in f:
|
||||
continue
|
||||
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
|
||||
break
|
||||
return verbs, part, ger
|
||||
|
||||
|
||||
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
|
||||
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
|
||||
"diminutive", "augmentative", "pejorative", "comparative",
|
||||
"superlative", "misspelling", "rare", "informal", "literary",
|
||||
"poetic", "error-unrecognized-form", "Balearic", "Valencian",
|
||||
"dated", "nonstandard"}
|
||||
|
||||
|
||||
def _kaikki_gender(arg):
|
||||
if not arg:
|
||||
return None
|
||||
a = str(arg).lower()
|
||||
if a.startswith("f"):
|
||||
return "f"
|
||||
if a.startswith("m"):
|
||||
return "m"
|
||||
return None
|
||||
|
||||
|
||||
def _build_nouns_adjs():
|
||||
nouns = {}
|
||||
adjs = {}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
pos = d.get("pos")
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word:
|
||||
continue
|
||||
forms = d.get("forms", []) or []
|
||||
if pos == "noun":
|
||||
ht = d.get("head_templates") or []
|
||||
g = None
|
||||
if ht:
|
||||
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
|
||||
if g is None:
|
||||
tags = d.get("tags") or []
|
||||
if "feminine" in tags:
|
||||
g = "f"
|
||||
elif "masculine" in tags:
|
||||
g = "m"
|
||||
pl = None
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
if "plural" in t and not (t & _EXCL_FORM_TAGS):
|
||||
fm = x.get("form")
|
||||
if fm and " " not in fm and fm not in ("#", "—", "-"):
|
||||
pl = fm
|
||||
break
|
||||
if word not in nouns:
|
||||
nouns[word] = {"g": g, "SG": word, "PL": pl}
|
||||
else:
|
||||
cur = nouns[word]
|
||||
if cur.get("g") is None and g:
|
||||
cur["g"] = g
|
||||
if not cur.get("PL") and pl:
|
||||
cur["PL"] = pl
|
||||
elif pos == "adj":
|
||||
d0 = adjs.setdefault(word, {})
|
||||
d0.setdefault(("m", "SG"), word)
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
fm = x.get("form")
|
||||
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
|
||||
continue
|
||||
if "feminine" in t and "plural" in t:
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
elif "masculine" in t and "plural" in t:
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
elif "feminine" in t:
|
||||
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
|
||||
elif "plural" in t:
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
return nouns, adjs
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs, part, ger = _build_verbs()
|
||||
nouns, adjs = _build_nouns_adjs()
|
||||
with open(_IRREG, encoding="utf-8") as fh:
|
||||
irreg = json.load(fh)
|
||||
data = {"verbs": verbs, "part": part, "ger": ger,
|
||||
"nouns": nouns, "adjs": adjs, "irreg": irreg}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
|
||||
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
|
||||
if os.path.getmtime(_CACHE) >= newest:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
|
||||
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
|
||||
_LEX["irreg"])
|
||||
_PERI = _IRREGV.get("_peri_pret_aux", {})
|
||||
|
||||
|
||||
# ── regular verb rule fallback ───────────────────────────────────────────────────
|
||||
def _vclass(lemma):
|
||||
if lemma.endswith("ar"):
|
||||
return "ar"
|
||||
if lemma.endswith("re"):
|
||||
return "re"
|
||||
if lemma.endswith("er"):
|
||||
return "er"
|
||||
if lemma.endswith("ir"):
|
||||
return "ir"
|
||||
return None
|
||||
|
||||
|
||||
# endings [1sg,2sg,3sg,1pl,2pl,3pl] — central Catalan
|
||||
_REG = {
|
||||
("ind", "present", "ar"): ["o", "es", "a", "em", "eu", "en"],
|
||||
("ind", "present", "re"): ["o", "s", "", "em", "eu", "en"],
|
||||
("ind", "present", "er"): ["o", "s", "", "em", "eu", "en"],
|
||||
("ind", "present", "ir"): ["o", "es", "", "im", "iu", "en"], # pure -ir (dormir)
|
||||
("ind", "imperfect", "ar"): ["ava", "aves", "ava", "àvem", "àveu", "aven"],
|
||||
("ind", "imperfect", "re"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
|
||||
("ind", "imperfect", "er"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
|
||||
("ind", "imperfect", "ir"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
|
||||
("ind", "preterite", "ar"): ["í", "ares", "à", "àrem", "àreu", "aren"],
|
||||
("ind", "preterite", "re"): ["í", "eres", "é", "érem", "éreu", "eren"],
|
||||
("ind", "preterite", "er"): ["í", "eres", "é", "érem", "éreu", "eren"],
|
||||
("ind", "preterite", "ir"): ["í", "ires", "í", "írem", "íreu", "iren"],
|
||||
("sbjv", "present", "ar"): ["i", "is", "i", "em", "eu", "in"],
|
||||
("sbjv", "present", "re"): ["i", "is", "i", "em", "eu", "in"],
|
||||
("sbjv", "present", "er"): ["i", "is", "i", "em", "eu", "in"],
|
||||
("sbjv", "present", "ir"): ["i", "is", "i", "im", "iu", "in"],
|
||||
("sbjv", "imperfect", "ar"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
|
||||
("sbjv", "imperfect", "re"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
|
||||
("sbjv", "imperfect", "er"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
|
||||
("sbjv", "imperfect", "ir"): ["ís", "issis", "ís", "íssim", "íssiu", "issin"],
|
||||
("imp", "affirmative", "ar"): [None, "a", "i", "em", "eu", "in"],
|
||||
("imp", "affirmative", "re"): [None, "", "i", "em", "eu", "in"],
|
||||
("imp", "affirmative", "er"): [None, "", "i", "em", "eu", "in"],
|
||||
("imp", "affirmative", "ir"): [None, "", "i", "im", "iu", "in"],
|
||||
}
|
||||
_FUT = ["é", "às", "à", "em", "eu", "an"]
|
||||
_COND = ["ia", "ies", "ia", "íem", "íeu", "ien"]
|
||||
|
||||
|
||||
def _slot_idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _apply_ar_spelling(stem, ending):
|
||||
"""-car/-gar/-çar/-jar spelling before front (e/i) endings."""
|
||||
front = ending[:1] in ("e", "i", "é", "í")
|
||||
if not front:
|
||||
# ç before back vowel stays; but -çar stem already ends ç
|
||||
return stem + ending
|
||||
if stem.endswith("c"):
|
||||
return stem[:-1] + "qu" + ending
|
||||
if stem.endswith("g"):
|
||||
return stem[:-1] + "gu" + ending
|
||||
if stem.endswith("ç"):
|
||||
return stem[:-1] + "c" + ending
|
||||
if stem.endswith("j"):
|
||||
return stem[:-1] + "g" + ending
|
||||
if stem.endswith("qu"):
|
||||
return stem + ending
|
||||
return stem + ending
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
vc = _vclass(lemma)
|
||||
if vc is None:
|
||||
return None
|
||||
body = lemma[:-2]
|
||||
i = _slot_idx(person, number)
|
||||
if mood == "ind" and tense in ("future", "conditional"):
|
||||
# future/cond stem = infinitive (for -re verbs drop final -e)
|
||||
stem = lemma[:-1] if vc == "re" else lemma
|
||||
end = (_FUT if tense == "future" else _COND)[i]
|
||||
return stem + end
|
||||
table = _REG.get((mood, tense, vc))
|
||||
if not table:
|
||||
return None
|
||||
end = table[i]
|
||||
if end is None:
|
||||
return None
|
||||
if vc == "ar":
|
||||
return _apply_ar_spelling(body, end)
|
||||
# -re/-er/-ir: guard double vowel
|
||||
if body and body[-1:] == end[:1] and end[:1] in "ií":
|
||||
return body[:-1] + end
|
||||
return body + end
|
||||
|
||||
|
||||
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number):
|
||||
lemma = lemma.strip().lower()
|
||||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number and number[:2].upper()}"
|
||||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{_NUMBER.get(number,'?')}"
|
||||
# UniMorph (cleanly accented) takes priority; the kaikki irregulars layer is a
|
||||
# FALLBACK for verbs/slots UniMorph lacks (anar, fer, and rarer paradigm cells).
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
if p and n:
|
||||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and key in ir:
|
||||
return ir[key], "lexicon"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r is not None:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def peri_pret_aux(person, number):
|
||||
"""anar-present auxiliary for the periphrastic preterite (vaig cantar)."""
|
||||
return _PERI.get(f"{_PERSON.get(person,'3')}|{_NUMBER.get(number,'SG')}", "va")
|
||||
|
||||
|
||||
# ── PUBLIC: participle + gerund ──────────────────────────────────────────────────
|
||||
def participle(lemma, gender="m", number="singular"):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
ir = _IRREGV.get(lemma)
|
||||
base = None
|
||||
if ir and "part" in ir:
|
||||
# prefer explicit irregular agreement form (part_mSG/part_fSG/...)
|
||||
exact = ir.get("part_" + g + num)
|
||||
if exact:
|
||||
return exact, "lexicon"
|
||||
base = ir["part"]
|
||||
elif lemma in _PART:
|
||||
table = _PART[lemma]
|
||||
if (g, num) in table:
|
||||
return table[(g, num)], "lexicon"
|
||||
base = table.get(("m", "SG"))
|
||||
if base is None:
|
||||
vc = _vclass(lemma)
|
||||
if vc == "ar":
|
||||
base = lemma[:-2] + "at"
|
||||
elif vc == "ir":
|
||||
base = lemma[:-2] + "it"
|
||||
elif vc in ("er", "re"):
|
||||
base = lemma[:-2] + "ut"
|
||||
else:
|
||||
return lemma, "fallback"
|
||||
conf = "rule"
|
||||
else:
|
||||
conf = "lexicon"
|
||||
# agreement on -t/-ut/-at/-it participles: m.sg base, f.sg +a (-da? no: -ada),
|
||||
# Catalan: cantat/cantada/cantats/cantades; -t → f -da, pl -ts/-des
|
||||
if base.endswith("t"):
|
||||
stem = base[:-1]
|
||||
forms = {"m|SG": base, "f|SG": stem + "da",
|
||||
"m|PL": base + "s", "f|PL": stem + "des"}
|
||||
return forms[f"{g}|{num}"], conf
|
||||
if base.endswith("s"): # after sibilant participle (rare): pres->presa
|
||||
stem = base
|
||||
forms = {"m|SG": base, "f|SG": base + "a",
|
||||
"m|PL": base + "os", "f|PL": base + "es"}
|
||||
return forms[f"{g}|{num}"], conf
|
||||
return base, conf
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and "ger" in ir:
|
||||
return ir["ger"], "lexicon"
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
vc = _vclass(lemma)
|
||||
if vc == "ar":
|
||||
return lemma[:-2] + "ant", "rule"
|
||||
if vc in ("er", "re"):
|
||||
return lemma[:-2] + "ent", "rule"
|
||||
if vc == "ir":
|
||||
return lemma[:-2] + "int", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
|
||||
_FEM_SUF = ("ció", "sió", "tat", "tud", "esa", "esa", "dat", "ança", "ència",
|
||||
"ància", "tud", "ícia", "esa", "or") # note -or is mixed; kaikki wins
|
||||
_MASC_SUF = ("atge", "ment", " isme", "or")
|
||||
|
||||
|
||||
def _gender_heuristic(noun):
|
||||
for suf in ("ció", "sió", "tat", "tud", "esa", "ança", "ència", "ància",
|
||||
"ícia", "etat"):
|
||||
if noun.endswith(suf):
|
||||
return "f"
|
||||
if noun.endswith("a") and not noun.endswith("ma"):
|
||||
return "f"
|
||||
return "m"
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g") in ("m", "f"):
|
||||
return d["g"]
|
||||
return _gender_heuristic(lemma)
|
||||
|
||||
|
||||
def _rule_plural(noun, gender):
|
||||
"""Deterministic Catalan pluralization. (form, ok); ok=False FLAGS ambiguity."""
|
||||
if not noun:
|
||||
return noun, True
|
||||
# stressed final vowel with accent → +ns (mà→mans is irregular; but capità→capitans)
|
||||
if noun[-1:] in ("à", "é", "í", "ó", "ú"):
|
||||
return noun + "ns", True
|
||||
if noun.endswith("ça"):
|
||||
return noun[:-2] + "ces", True # plaça→places
|
||||
if noun.endswith("ca"):
|
||||
return noun[:-2] + "ques", True # branca→branques
|
||||
if noun.endswith("ga"):
|
||||
return noun[:-2] + "gues", True # amiga→amigues
|
||||
if noun.endswith("ja"):
|
||||
return noun[:-2] + "ges", True # pluja→pluges
|
||||
if noun.endswith("qua"):
|
||||
return noun[:-3] + "qües", True
|
||||
if noun.endswith("gua"):
|
||||
return noun[:-3] + "gües", True
|
||||
if noun.endswith("a"):
|
||||
return noun[:-1] + "es", True # casa→cases
|
||||
# sibilant-final → -os
|
||||
if noun.endswith(("s", "ç", "x", "ig")) or noun.endswith(("ix", "tx", "tj")):
|
||||
if noun.endswith("ç"):
|
||||
return noun[:-1] + "ços", True # braç→braços
|
||||
return noun + "os", True # peix→peixos, gas→gasos
|
||||
if noun[-1:] in ("e", "i", "o", "u"):
|
||||
return noun + "s", True
|
||||
# consonant-final
|
||||
return noun + "s", True
|
||||
|
||||
|
||||
def inflect_noun(lemma, number, gender=None):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if number == "singular":
|
||||
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
|
||||
if d and d.get("PL"):
|
||||
return d["PL"], "lexicon"
|
||||
g = gender or noun_gender(lemma)
|
||||
form, ok = _rule_plural(lemma, g)
|
||||
return form, ("rule" if ok else "fallback")
|
||||
|
||||
|
||||
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
|
||||
def _fem_of(adj):
|
||||
"""Regular Catalan feminine: consonant/-o? Catalan masc usually consonant or -e.
|
||||
default +a with spelling changes; -e→-a for some; but many are invariable."""
|
||||
a = adj
|
||||
if a.endswith("a"):
|
||||
return a
|
||||
if a.endswith("e"):
|
||||
return a[:-1] + "a" # ample→? actually 'ample' invariable; kaikki wins
|
||||
if a.endswith("u"):
|
||||
return a + "a"
|
||||
if a.endswith("c"):
|
||||
return a[:-1] + "ca" # ric→rica
|
||||
if a.endswith("t"):
|
||||
return a + "a" # alt→alta
|
||||
return a + "a"
|
||||
|
||||
|
||||
def inflect_adj(lemma, gender, number):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
d = _ADJS.get(lemma)
|
||||
if d:
|
||||
form = d.get((g, num))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
|
||||
if num == "PL":
|
||||
pl, ok = _rule_plural(sg, g)
|
||||
return pl, ("rule" if ok else "fallback")
|
||||
return sg, "lexicon"
|
||||
# rule fallback
|
||||
base = lemma if g == "m" else _fem_of(lemma)
|
||||
if num == "SG":
|
||||
return base, "rule"
|
||||
pl, ok = _rule_plural(base, g)
|
||||
return pl, ("rule" if ok else "fallback")
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"verb_source": "UniMorph Catalan (github.com/unimorph/cat) + kaikki.org "
|
||||
"irregulars (anar/fer/auxiliaries)",
|
||||
"noun_adj_source": "kaikki.org Catalan (Wiktionary extract)",
|
||||
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
|
||||
"unimorph_verb_forms": len(_VERBS),
|
||||
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"irregular_verb_lemmas": len([k for k in _IRREGV if not k.startswith("_")]),
|
||||
"participle_lemmas": len(_PART),
|
||||
"gerund_lemmas": len(_GER),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
tests = [
|
||||
("cantar", "ind", "present", "first", "singular", "canto"),
|
||||
("cantar", "ind", "present", "third", "plural", "canten"),
|
||||
("ser", "ind", "present", "third", "singular", "és"),
|
||||
("haver", "ind", "present", "first", "singular", "he"),
|
||||
("anar", "ind", "present", "first", "singular", "vaig"),
|
||||
("fer", "ind", "present", "third", "singular", "fa"),
|
||||
("perdre", "ind", "present", "first", "singular", "perdo"),
|
||||
("dormir", "ind", "present", "third", "plural", "dormen"),
|
||||
("cantar", "ind", "future", "first", "singular", "cantaré"),
|
||||
("cantar", "ind", "preterite", "third", "singular", "cantà"),
|
||||
("tenir", "sbjv", "present", "first", "singular", "tingui"),
|
||||
]
|
||||
ok = 0
|
||||
for lemma, mood, tense, per, num, exp in tests:
|
||||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||||
flag = "OK " if got == exp else "XX "
|
||||
ok += got == exp
|
||||
print(f" {flag}{lemma:8} {mood}/{tense:11} {per[:3]}.{num[:2]} -> {got:10} ({conf}) exp={exp}")
|
||||
print(f"verb tests {ok}/{len(tests)}")
|
||||
print(" peri-pret anar: 1sg=", peri_pret_aux("first", "singular"),
|
||||
"3pl=", peri_pret_aux("third", "plural"))
|
||||
print(" gender casa=", noun_gender("casa"), "home=", noun_gender("home"),
|
||||
"cavall=", noun_gender("cavall"), "cançó=", noun_gender("cançó"))
|
||||
print(" plural casa->", inflect_noun("casa", "plural"),
|
||||
"| plaça->", inflect_noun("plaça", "plural"),
|
||||
"| peix->", inflect_noun("peix", "plural"),
|
||||
"| braç->", inflect_noun("braç", "plural"),
|
||||
"| home->", inflect_noun("home", "plural"))
|
||||
print(" adj: alt/f/sg->", inflect_adj("alt", "f", "singular"),
|
||||
"| bonic/f/pl->", inflect_adj("bonic", "f", "plural"),
|
||||
"| vermell/f/sg->", inflect_adj("vermell", "f", "singular"))
|
||||
print(" part: cantar/f/sg->", participle("cantar", "f", "singular"),
|
||||
"| veure/f/pl->", participle("veure", "f", "plural"),
|
||||
"| fer/m/sg->", participle("fer", "m", "singular"))
|
||||
print(" ger: fer->", gerund("fer"), "| cantar->", gerund("cantar"))
|
||||
@@ -0,0 +1,423 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""morphology_de_full.py — production German morphological generator.
|
||||
|
||||
Real data, no toy tables:
|
||||
|
||||
PRIMARY — UniMorph German (github.com/unimorph/deu, CC-BY-SA 3.0).
|
||||
~219k noun forms, ~199k verb forms. Supplies:
|
||||
nouns : gender (MASC/FEM/NEUT) + case×number paradigm
|
||||
(N;NOM/ACC/DAT/GEN; MASC/FEM/NEUT; SG/PL) — the genitive -(e)s,
|
||||
dative-plural -n and the five plural classes are REAL forms, not
|
||||
guessed.
|
||||
verbs : full finite paradigm IND;{SG,PL};{1,2,3};{PRS,PST}, the past
|
||||
participle (V.PTCP;PST, incl. reattached separable prefix
|
||||
'zugefügt'), and — crucially for V2 — the SEPARATED finite form
|
||||
UniMorph records directly ('füge zu', 'steht auf').
|
||||
adjs : comparative / superlative (ADJ;CMPR, ADJ;SPRL).
|
||||
|
||||
SECONDARY — kaikki.org German (Wiktionary, CC-BY-SA/GFDL). Gap-fills noun
|
||||
gender + plural where UniMorph is thin. Never overrides UniMorph.
|
||||
|
||||
Rule fallbacks (flagged 'rule'/'fallback') for lemmas absent from both lexicons:
|
||||
present : -e/-st/-t/-en/-t/-en with e-epenthesis after -t/-d/-chn stems
|
||||
plural : gender heuristic (fem -> -(e)n, else -e / umlaut left to lexicon)
|
||||
ppart : weak ge-…-t
|
||||
Adjective ENDINGS are rule-computed by the realizer (regular closed table);
|
||||
this module only supplies the comparative/superlative STEM.
|
||||
|
||||
Perfect auxiliary (haben vs sein): sein for a curated set of intransitive
|
||||
motion / change-of-state verbs (real German lexical property), else haben.
|
||||
|
||||
Public API:
|
||||
noun_gender(lemma) -> 'm'|'f'|'n'
|
||||
decline_noun(lemma, case, number) -> (form, conf)
|
||||
pluralize(lemma) -> (form, conf)
|
||||
finite(lemma, tense, person, number) -> (form, conf) # may contain ' prefix'
|
||||
nonfinite(lemma, req) -> (form, conf) # req: 'inf'|'ppart'
|
||||
past_participle(lemma) -> (form, conf)
|
||||
separable_prefix(lemma) -> str|None
|
||||
perfect_aux(lemma) -> 'haben'|'sein'
|
||||
comparative(lemma)/superlative(lemma) -> (stem, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "deu.unimorph")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_de.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "de_morph_cache.pkl")
|
||||
|
||||
_GENDER = {"MASC": "m", "FEM": "f", "NEUT": "n"}
|
||||
|
||||
# intransitive motion / change-of-state verbs that take SEIN in the perfect
|
||||
_SEIN = {"gehen", "kommen", "fahren", "laufen", "rennen", "reisen", "fallen",
|
||||
"steigen", "sinken", "wachsen", "sterben", "geschehen", "passieren",
|
||||
"werden", "bleiben", "sein", "aufstehen", "einschlafen", "aufwachen",
|
||||
"ankommen", "abfahren", "aufsteigen", "erscheinen", "verschwinden",
|
||||
"fliegen", "schwimmen", "springen", "begegnen", "folgen", "gelingen",
|
||||
"wandern", "ziehen", "flüchten", "eintreten", "einsteigen", "aussteigen"}
|
||||
|
||||
|
||||
# hardcoded high-frequency irregular / auxiliary / modal paradigms (closed class,
|
||||
# verified) — consulted before the lexicon so aux+modal chains are always correct.
|
||||
_CORE = {
|
||||
"sein": {"prs": {("first", "singular"): "bin", ("second", "singular"): "bist",
|
||||
("third", "singular"): "ist", ("first", "plural"): "sind",
|
||||
("second", "plural"): "seid", ("third", "plural"): "sind"},
|
||||
"pst": {("first", "singular"): "war", ("second", "singular"): "warst",
|
||||
("third", "singular"): "war", ("first", "plural"): "waren",
|
||||
("second", "plural"): "wart", ("third", "plural"): "waren"},
|
||||
"ppart": "gewesen"},
|
||||
"haben": {"prs": {("first", "singular"): "habe", ("second", "singular"): "hast",
|
||||
("third", "singular"): "hat", ("first", "plural"): "haben",
|
||||
("second", "plural"): "habt", ("third", "plural"): "haben"},
|
||||
"pst": {("first", "singular"): "hatte", ("second", "singular"): "hattest",
|
||||
("third", "singular"): "hatte", ("first", "plural"): "hatten",
|
||||
("second", "plural"): "hattet", ("third", "plural"): "hatten"},
|
||||
"ppart": "gehabt"},
|
||||
"werden": {"prs": {("first", "singular"): "werde", ("second", "singular"): "wirst",
|
||||
("third", "singular"): "wird", ("first", "plural"): "werden",
|
||||
("second", "plural"): "werdet", ("third", "plural"): "werden"},
|
||||
"pst": {("first", "singular"): "wurde", ("second", "singular"): "wurdest",
|
||||
("third", "singular"): "wurde", ("first", "plural"): "wurden",
|
||||
("second", "plural"): "wurdet", ("third", "plural"): "wurden"},
|
||||
"ppart": "geworden"},
|
||||
}
|
||||
_MODAL_PRS = {
|
||||
"können": ("kann", "kannst", "kann", "können", "könnt", "können"),
|
||||
"müssen": ("muss", "musst", "muss", "müssen", "müsst", "müssen"),
|
||||
"wollen": ("will", "willst", "will", "wollen", "wollt", "wollen"),
|
||||
"sollen": ("soll", "sollst", "soll", "sollen", "sollt", "sollen"),
|
||||
"dürfen": ("darf", "darfst", "darf", "dürfen", "dürft", "dürfen"),
|
||||
"mögen": ("mag", "magst", "mag", "mögen", "mögt", "mögen"),
|
||||
}
|
||||
_MODAL_PST = {
|
||||
"können": ("konnte", "konntest", "konnte", "konnten", "konntet", "konnten"),
|
||||
"müssen": ("musste", "musstest", "musste", "mussten", "musstet", "mussten"),
|
||||
"wollen": ("wollte", "wolltest", "wollte", "wollten", "wolltet", "wollten"),
|
||||
"sollen": ("sollte", "solltest", "sollte", "sollten", "solltet", "sollten"),
|
||||
"dürfen": ("durfte", "durftest", "durfte", "durften", "durftet", "durften"),
|
||||
"mögen": ("mochte", "mochtest", "mochte", "mochten", "mochtet", "mochten"),
|
||||
}
|
||||
_PN_ORDER = [("first", "singular"), ("second", "singular"), ("third", "singular"),
|
||||
("first", "plural"), ("second", "plural"), ("third", "plural")]
|
||||
_MODAL_PPART = {"können": "gekonnt", "müssen": "gemusst", "wollen": "gewollt",
|
||||
"sollen": "gesollt", "dürfen": "gedurft", "mögen": "gemocht"}
|
||||
for _m, _forms in _MODAL_PRS.items():
|
||||
_CORE[_m] = {"prs": dict(zip(_PN_ORDER, _forms)),
|
||||
"pst": dict(zip(_PN_ORDER, _MODAL_PST[_m])),
|
||||
"ppart": _MODAL_PPART[_m]}
|
||||
|
||||
|
||||
def _person_num(tags):
|
||||
p = n = None
|
||||
for t in tags:
|
||||
if t in ("1", "2", "3"):
|
||||
p = {"1": "first", "2": "second", "3": "third"}[t]
|
||||
elif t == "SG":
|
||||
n = "singular"
|
||||
elif t == "PL":
|
||||
n = "plural"
|
||||
return p, n
|
||||
|
||||
|
||||
def _build_from_unimorph():
|
||||
nouns, verbs, adjs = {}, {}, {}
|
||||
if not os.path.exists(_UNIMORPH):
|
||||
return nouns, verbs, adjs
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tagstr = parts
|
||||
tags = tagstr.split(";")
|
||||
head = tags[0]
|
||||
tset = set(tags)
|
||||
if head == "N":
|
||||
rec = nouns.setdefault(lemma, {"g": None, "cases": {}, "pl": None})
|
||||
g = next((_GENDER[t] for t in tags if t in _GENDER), None)
|
||||
if g and not rec["g"]:
|
||||
rec["g"] = g
|
||||
case = next((t for t in tags if t in ("NOM", "ACC", "DAT", "GEN")), None)
|
||||
num = "plural" if "PL" in tset else ("singular" if "SG" in tset else None)
|
||||
if case and num:
|
||||
rec["cases"].setdefault((case, num), form)
|
||||
if case == "NOM" and num == "plural" and not rec["pl"]:
|
||||
rec["pl"] = form
|
||||
elif head.startswith("V"):
|
||||
rec = verbs.setdefault(lemma, {"prs": {}, "pst": {}, "ppart": None})
|
||||
if "PTCP" in head and "PST" in tset:
|
||||
rec["ppart"] = rec["ppart"] or form
|
||||
elif "IND" in tset and ("PRS" in tset or "PST" in tset):
|
||||
p, n = _person_num(tags)
|
||||
if p and n:
|
||||
slot = "prs" if "PRS" in tset else "pst"
|
||||
rec[slot].setdefault((p, n), form)
|
||||
elif head == "ADJ":
|
||||
rec = adjs.setdefault(lemma, {})
|
||||
if "CMPR" in tset:
|
||||
rec.setdefault("cmpr", form.replace("am ", "").strip())
|
||||
elif "SPRL" in tset:
|
||||
rec.setdefault("sprl", form.replace("am ", "").replace("sten", "st")
|
||||
if form.endswith("sten") else form.replace("am ", ""))
|
||||
return nouns, verbs, adjs
|
||||
|
||||
|
||||
def _build_from_kaikki(nouns):
|
||||
"""Gap-fill noun gender + plural from kaikki German."""
|
||||
if not os.path.exists(_KAIKKI):
|
||||
return
|
||||
_g = {"masculine": "m", "feminine": "f", "neuter": "n", "m": "m", "f": "f", "n": "n"}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("pos") != "noun":
|
||||
continue
|
||||
w = d.get("word", "")
|
||||
if not w or not w[0].isalpha() or " " in w:
|
||||
continue
|
||||
rec = nouns.setdefault(w, {"g": None, "cases": {}, "pl": None})
|
||||
# GENDER: Wiktionary gender is hand-curated and OVERRIDES UniMorph's
|
||||
# auto-tagged gender, which has known errors (e.g. UniMorph deu mis-
|
||||
# records Zeit=MASC, Wagen=NEUT; Wiktionary has f, m correctly).
|
||||
for h in d.get("head_templates", []) or []:
|
||||
a = h.get("args", {}) or {}
|
||||
raw = a.get("1") or a.get("g") or ""
|
||||
code = str(raw).split(",")[0].strip().lower()
|
||||
if code in _g:
|
||||
rec["g"] = _g[code]
|
||||
break
|
||||
if not rec["pl"]:
|
||||
for f in d.get("forms", []) or []:
|
||||
t = set(f.get("tags", []) or [])
|
||||
if "plural" in t and f.get("form") and "genitive" not in t:
|
||||
rec["pl"] = f["form"]
|
||||
break
|
||||
|
||||
|
||||
def _build_cache():
|
||||
nouns, verbs, adjs = _build_from_unimorph()
|
||||
_build_from_kaikki(nouns)
|
||||
data = {"nouns": nouns, "verbs": verbs, "adjs": adjs}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
srcs = [p for p in (_UNIMORPH, _KAIKKI) if os.path.exists(p)]
|
||||
newest = max((os.path.getmtime(p) for p in srcs), default=0)
|
||||
if os.path.getmtime(_CACHE) >= newest:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_NOUNS, _VERBS, _ADJS = _LEX["nouns"], _LEX["verbs"], _LEX["adjs"]
|
||||
|
||||
|
||||
# ── nouns ────────────────────────────────────────────────────────────────────────
|
||||
def noun_gender(lemma):
|
||||
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
|
||||
if rec and rec.get("g"):
|
||||
return rec["g"]
|
||||
# last-resort rule: -ung/-heit/-keit/-schaft/-tät/-ion -> f ; -chen/-lein -> n
|
||||
low = lemma.lower()
|
||||
if low.endswith(("ung", "heit", "keit", "schaft", "tät", "ion", "ik", "ei")):
|
||||
return "f"
|
||||
if low.endswith(("chen", "lein", "ment", "um")):
|
||||
return "n"
|
||||
return "m"
|
||||
|
||||
|
||||
def pluralize(lemma):
|
||||
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
|
||||
if rec and rec.get("pl"):
|
||||
return rec["pl"], "lexicon"
|
||||
g = noun_gender(lemma)
|
||||
if g == "f":
|
||||
return (lemma + "en" if not lemma.endswith("e") else lemma + "n"), "rule"
|
||||
return (lemma if lemma.endswith(("er", "en", "el")) else lemma + "e"), "rule"
|
||||
|
||||
|
||||
def decline_noun(lemma, case, number):
|
||||
"""case in NOM/ACC/DAT/GEN, number in singular/plural."""
|
||||
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
|
||||
if case == "DAT" and number == "singular":
|
||||
# modern German drops the archaic dative -e ('dem Kinde' -> 'dem Kind');
|
||||
# the article carries the case. Keep bare nominative form.
|
||||
base = (rec or {}).get("cases", {}).get(("NOM", "singular")) or lemma
|
||||
return base, ("lexicon" if rec else "rule")
|
||||
if rec and rec.get("cases", {}).get((case, number)):
|
||||
return rec["cases"][(case, number)], "lexicon"
|
||||
if number == "plural":
|
||||
pl, c = pluralize(lemma)
|
||||
if case == "DAT" and not pl.endswith("n") and not pl.endswith("s"):
|
||||
return pl + "n", c # dative plural -n
|
||||
return pl, c
|
||||
# singular
|
||||
g = noun_gender(lemma)
|
||||
if case == "GEN" and g in ("m", "n"):
|
||||
return (lemma + "es" if lemma.endswith(("s", "ß", "z", "x")) else lemma + "s"), "rule"
|
||||
return lemma, "lexicon" if rec else "rule"
|
||||
|
||||
|
||||
# ── verbs ──────────────────────────────────────────────────────────────────────--
|
||||
_PRS_ENDINGS = {("first", "singular"): "e", ("second", "singular"): "st",
|
||||
("third", "singular"): "t", ("first", "plural"): "en",
|
||||
("second", "plural"): "t", ("third", "plural"): "en"}
|
||||
|
||||
|
||||
def _stem(lemma):
|
||||
if lemma.endswith("en"):
|
||||
return lemma[:-2]
|
||||
if lemma.endswith("n"):
|
||||
return lemma[:-1]
|
||||
return lemma
|
||||
|
||||
|
||||
def separable_prefix(lemma):
|
||||
"""Return the separable prefix if the lemma is a separable-prefix verb."""
|
||||
rec = _VERBS.get(lemma)
|
||||
if rec:
|
||||
for (_p, _n), form in rec.get("prs", {}).items():
|
||||
if " " in form:
|
||||
return form.rsplit(" ", 1)[1]
|
||||
_SEP = ("auf", "aus", "ab", "an", "ein", "mit", "nach", "vor", "zu", "zurück",
|
||||
"weg", "hin", "her", "los", "bei", "fest", "fort", "um", "zusammen")
|
||||
_INSEP = ("be", "ge", "er", "ver", "zer", "ent", "emp", "miss")
|
||||
for p in sorted(_SEP, key=len, reverse=True):
|
||||
if lemma.startswith(p) and len(lemma) > len(p) + 2 \
|
||||
and not lemma.startswith(_INSEP):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def finite(lemma, tense, person, number):
|
||||
"""Present/past finite. For separable verbs the returned string is the
|
||||
UniMorph SEPARATED form 'stem prefix' (realizer places prefix per V2)."""
|
||||
slot = "prs" if tense == "present" else "pst"
|
||||
if lemma in _CORE and _CORE[lemma].get(slot, {}).get((person, number)):
|
||||
return _CORE[lemma][slot][(person, number)], "lexicon"
|
||||
rec = _VERBS.get(lemma)
|
||||
if rec and rec.get(slot, {}).get((person, number)):
|
||||
return rec[slot][(person, number)], "lexicon"
|
||||
# rule fallback (present only reliable; past weak -te)
|
||||
stem = _stem(lemma)
|
||||
pref = separable_prefix(lemma)
|
||||
if pref:
|
||||
stem = _stem(lemma[len(pref):])
|
||||
if tense == "present":
|
||||
end = _PRS_ENDINGS[(person, number)]
|
||||
if stem.endswith(("t", "d", "chn", "ffn", "gn")) and end in ("st", "t"):
|
||||
end = "e" + end
|
||||
form = stem + end
|
||||
else:
|
||||
form = stem + ("ete" if stem.endswith(("t", "d")) else "te")
|
||||
if (person, number) == ("second", "singular"):
|
||||
form += "st"
|
||||
elif number == "plural" and person != "second":
|
||||
form += "n"
|
||||
elif (person, number) == ("second", "plural"):
|
||||
form += "t"
|
||||
if pref:
|
||||
return f"{form} {pref}", "rule"
|
||||
return form, "rule"
|
||||
|
||||
|
||||
def _weak_t(stem):
|
||||
return stem + ("et" if stem.endswith(("t", "d", "chn", "ffn", "gn")) else "t")
|
||||
|
||||
|
||||
def past_participle(lemma):
|
||||
if lemma in _CORE:
|
||||
return _CORE[lemma]["ppart"], "lexicon"
|
||||
rec = _VERBS.get(lemma)
|
||||
if rec and rec.get("ppart"):
|
||||
return rec["ppart"], "lexicon"
|
||||
stem = _stem(lemma)
|
||||
pref = separable_prefix(lemma)
|
||||
_INSEP = ("be", "ge", "er", "ver", "zer", "ent", "emp", "miss")
|
||||
if pref:
|
||||
inner = _stem(lemma[len(pref):])
|
||||
return pref + "ge" + _weak_t(inner), "rule"
|
||||
if lemma.startswith(_INSEP):
|
||||
return _weak_t(stem), "rule"
|
||||
return "ge" + _weak_t(stem), "rule"
|
||||
|
||||
|
||||
def nonfinite(lemma, req):
|
||||
if req == "ppart":
|
||||
return past_participle(lemma)
|
||||
return lemma, "lexicon" if lemma in _VERBS else "rule" # infinitive
|
||||
|
||||
|
||||
def perfect_aux(lemma):
|
||||
return "sein" if lemma in _SEIN else "haben"
|
||||
|
||||
|
||||
# ── adjectives ────────────────────────────────────────────────────────────────---
|
||||
_ADJ_IRREG_SPRL = {"gut": "best", "groß": "größt", "hoch": "höchst",
|
||||
"nah": "nächst", "viel": "meist", "gern": "liebst"}
|
||||
|
||||
|
||||
def comparative(lemma):
|
||||
rec = _ADJS.get(lemma)
|
||||
if rec and rec.get("cmpr"):
|
||||
return rec["cmpr"], "lexicon"
|
||||
return lemma + "er", "rule"
|
||||
|
||||
|
||||
def superlative(lemma):
|
||||
"""Return the bare superlative STEM (realizer adds 'am ...en' or '-e' ending)."""
|
||||
if lemma in _ADJ_IRREG_SPRL:
|
||||
return _ADJ_IRREG_SPRL[lemma], "lexicon"
|
||||
# derive from the comparative so umlaut is carried (alt->älter->ältest)
|
||||
cmpr, cconf = comparative(lemma)
|
||||
base = cmpr[:-2] if cmpr.endswith("er") else lemma
|
||||
end = "est" if base.endswith(("t", "d", "s", "ß", "z", "sch")) else "st"
|
||||
return base + end, cconf
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"source": "UniMorph deu (primary) + kaikki.org German (gap-fill gender/plural)",
|
||||
"license": "CC-BY-SA 3.0 (UniMorph); CC-BY-SA/GFDL (Wiktionary)",
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"nouns_with_gender": sum(1 for v in _NOUNS.values() if v.get("g")),
|
||||
"nouns_with_plural": sum(1 for v in _NOUNS.values() if v.get("pl")),
|
||||
"verb_lemmas": len(_VERBS),
|
||||
"verbs_with_ppart": sum(1 for v in _VERBS.values() if v.get("ppart")),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
for w in ("Hund", "Frau", "Kind", "Mann", "Buch", "Blume"):
|
||||
print(f" {w}: gender={noun_gender(w)} pl={pluralize(w)} "
|
||||
f"gen.sg={decline_noun(w, 'GEN', 'singular')} "
|
||||
f"dat.pl={decline_noun(w, 'DAT', 'plural')}")
|
||||
for v in ("machen", "gehen", "aufstehen", "sein", "haben", "arbeiten"):
|
||||
print(f" {v}: 3sg.prs={finite(v, 'present', 'third', 'singular')} "
|
||||
f"3sg.pst={finite(v, 'past', 'third', 'singular')} "
|
||||
f"ppart={past_participle(v)} aux={perfect_aux(v)} sep={separable_prefix(v)}")
|
||||
for a in ("schnell", "gut", "groß", "alt"):
|
||||
print(f" {a}: cmpr={comparative(a)} sprl={superlative(a)}")
|
||||
@@ -0,0 +1,562 @@
|
||||
"""morphology_es_full.py — production-grade Spanish morphological generator.
|
||||
|
||||
NOT a toy. Backed by a real, broad, licensed lexicon:
|
||||
|
||||
UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0, Wiktionary-derived)
|
||||
1,196,245 inflected forms:
|
||||
6,695 verb lemmas — full paradigms: indicative (present/preterite/
|
||||
imperfect/future), conditional, present & imperfect
|
||||
subjunctive, affirmative imperative, formal/informal
|
||||
48,353 noun lemmas — WITH inherent gender (N;FEM/MASC;SG/PL)
|
||||
16,984 adj lemmas — gender + number paradigms
|
||||
|
||||
Fallbacks (so we degrade, never crash, on out-of-vocabulary input):
|
||||
- verbs : mlconjug3 (ML paradigm model, conjugates ANY Spanish verb) then a
|
||||
hand-rolled regular-ending generator
|
||||
- nouns : gender heuristic (endings) + regular pluralization
|
||||
- adjs : -o/-a gender rule + regular pluralization
|
||||
|
||||
Every generated form carries a CONFIDENCE flag:
|
||||
"lexicon" form came straight from UniMorph (trust: high)
|
||||
"model" form came from mlconjug3 (trust: high)
|
||||
"rule" form came from a deterministic rule (trust: medium)
|
||||
"fallback" we could not inflect; returned lemma as-is (trust: low → FLAG)
|
||||
|
||||
Public API (used by realizer_es.py):
|
||||
conjugate(lemma, mood, tense, person, number, formality="informal") -> (form, conf)
|
||||
participle(lemma) -> (form, conf) # past participle (compound tenses)
|
||||
gerund(lemma) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"
|
||||
inflect_noun(lemma, number) -> (form, conf)
|
||||
inflect_adj(lemma, gender, number) -> (form, conf)
|
||||
attach_enclitics(verb_form, clitics) -> str # accent-correct enclisis
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
import unicodedata
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "spa.unimorph")
|
||||
_CACHE = os.path.join(_HERE, "data", "es_morph_cache.pkl")
|
||||
|
||||
# ── canonical feature keys the realizer speaks, mapped to UniMorph tags ─────────
|
||||
# mood/tense pair -> the UniMorph feature substring that identifies it
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): ("IND", "PRS", None),
|
||||
("ind", "preterite"): ("IND", "PST", "PFV"),
|
||||
("ind", "imperfect"): ("IND", "PST", "IPFV"),
|
||||
("ind", "future"): ("IND", "FUT", None),
|
||||
("ind", "conditional"):("COND", None, None),
|
||||
("sbjv", "present"): ("SBJV", "PRS", None),
|
||||
("sbjv", "imperfect"): ("SBJV", "PST", "LGSPEC1"), # -ra form
|
||||
("imp", "present"): ("POS", "IMP", None),
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
# ── build / load the compact lexicon ───────────────────────────────────────────
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs = {} # (lemma, canonkey) -> form canonkey e.g. "ind|present|1|SG|infm"
|
||||
nouns = {} # lemma -> {"g": "m"/"f", "SG": form, "PL": form}
|
||||
adjs = {} # lemma -> {("m","SG"): form, ...}
|
||||
part = {} # lemma -> masc-sg participle
|
||||
ger = {} # lemma -> gerund
|
||||
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
|
||||
if head == "V":
|
||||
# skip clitic-bearing rows (we generate clitics ourselves)
|
||||
if "PRO" in f:
|
||||
continue
|
||||
if "V.PTCP" in f and "PST" in f and "MASC" in f and "SG" in f:
|
||||
part.setdefault(lemma, form)
|
||||
continue
|
||||
if "V.CVB" in f or "NFIN" in f or "V.PTCP" in f:
|
||||
if "V.CVB" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
# identify mood/tense
|
||||
mt = None
|
||||
for (mood, tense), (a, b, c) in _VERB_KEYMAP.items():
|
||||
if a not in f:
|
||||
continue
|
||||
if b is not None and b not in f:
|
||||
continue
|
||||
if c is not None and c not in f:
|
||||
continue
|
||||
# disambiguate IND;PST needing PFV vs IPFV
|
||||
if a == "IND" and b == "PST" and c not in f:
|
||||
continue
|
||||
mt = (mood, tense)
|
||||
break
|
||||
if mt is None:
|
||||
continue
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
formal = "form" if "FORM" in f else ("infm" if "INFM" in f else "any")
|
||||
key = f"{mt[0]}|{mt[1]}|{person}|{number}|{formal}"
|
||||
verbs.setdefault((lemma, key), form)
|
||||
|
||||
elif head == "N":
|
||||
# substring test handles epicene "MASC+FEM" (-> masc citation)
|
||||
g = "m" if "MASC" in tag else ("f" if "FEM" in tag else None)
|
||||
num = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if num is None:
|
||||
continue
|
||||
# store forms keyed by (gender,number); animate nouns list BOTH
|
||||
# genders under one lemma (niño -> niño/niña). Resolve citation
|
||||
# gender in a post-pass (gender of the row whose form == lemma).
|
||||
d = nouns.setdefault(lemma, {})
|
||||
d.setdefault("_rows", []).append((g, num, form))
|
||||
|
||||
elif head == "ADJ":
|
||||
g = "m" if "MASC" in tag else ("f" if "FEM" in tag else "m")
|
||||
num = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if num is None:
|
||||
continue
|
||||
adjs.setdefault(lemma, {})[(g, num)] = form
|
||||
|
||||
# post-pass: resolve noun citation gender + default SG/PL forms
|
||||
for lemma, d in nouns.items():
|
||||
rows = d.pop("_rows", [])
|
||||
# citation gender = gender of the row whose form == lemma; else first MASC;
|
||||
# else first seen gender.
|
||||
cite_g = None
|
||||
for g, num, form in rows:
|
||||
if form == lemma and g:
|
||||
cite_g = g
|
||||
break
|
||||
if cite_g is None:
|
||||
for g, num, form in rows:
|
||||
if g == "m":
|
||||
cite_g = "m"
|
||||
break
|
||||
if cite_g is None:
|
||||
cite_g = next((g for g, _, _ in rows if g), "m")
|
||||
d["g"] = cite_g
|
||||
for g, num, form in rows:
|
||||
d[(g, num)] = form
|
||||
d["SG"] = d.get((cite_g, "SG")) or next((f for g, n, f in rows if n == "SG"), lemma)
|
||||
d["PL"] = d.get((cite_g, "PL")) or next((f for g, n, f in rows if n == "PL"), None)
|
||||
|
||||
# post-pass: UniMorph omits the identity inflection (masc-sg == lemma) for
|
||||
# adjectives, so fill it in; without this a fem-sg row wrongly satisfies a
|
||||
# masc-sg request (alto -> alta bug).
|
||||
for lemma, d in adjs.items():
|
||||
d.setdefault(("m", "SG"), lemma)
|
||||
|
||||
data = {"verbs": verbs, "nouns": nouns, "adjs": adjs, "part": part, "ger": ger}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE) and os.path.getmtime(_CACHE) >= os.path.getmtime(_UNIMORPH):
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _NOUNS, _ADJS, _PART, _GER = (
|
||||
_LEX["verbs"], _LEX["nouns"], _LEX["adjs"], _LEX["part"], _LEX["ger"])
|
||||
|
||||
# ── mlconjug3 fallback (lazy) ───────────────────────────────────────────────────
|
||||
_MLC = None
|
||||
_MLC_TENSE = { # (mood,tense) -> (mlconjug mood label, tense label)
|
||||
("ind", "present"): ("Indicativo", "Indicativo presente"),
|
||||
("ind", "preterite"): ("Indicativo", "Indicativo pretérito perfecto simple"),
|
||||
("ind", "imperfect"): ("Indicativo", "Indicativo pretérito imperfecto"),
|
||||
("ind", "future"): ("Indicativo", "Indicativo futuro"),
|
||||
("ind", "conditional"): ("Condicional", "Condicional Condicional"),
|
||||
("sbjv", "present"): ("Subjuntivo", "Subjuntivo presente"),
|
||||
("sbjv", "imperfect"): ("Subjuntivo", "Subjuntivo pretérito imperfecto 1"),
|
||||
("imp", "present"): ("Imperativo", "Imperativo Afirmativo"),
|
||||
}
|
||||
_MLC_SLOT = { # (person,number) -> mlconjug slot key
|
||||
("first", "singular"): "1s", ("second", "singular"): "2s",
|
||||
("third", "singular"): "3s", ("first", "plural"): "1p",
|
||||
("second", "plural"): "2p", ("third", "plural"): "3p",
|
||||
}
|
||||
|
||||
|
||||
def _mlc_conjugate(lemma, mood, tense, person, number):
|
||||
global _MLC
|
||||
try:
|
||||
if _MLC is None:
|
||||
from mlconjug3 import Conjugator
|
||||
_MLC = Conjugator(language="es")
|
||||
v = _MLC.conjugate(lemma)
|
||||
if v is None:
|
||||
return None
|
||||
info = v.conjug_info
|
||||
m, t = _MLC_TENSE.get((mood, tense), (None, None))
|
||||
if m is None or m not in info or t not in info[m]:
|
||||
return None
|
||||
block = info[m][t]
|
||||
slot = _MLC_SLOT.get((person, number))
|
||||
if isinstance(block, dict) and slot in block and block[slot]:
|
||||
return block[slot]
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ── regular-ending rule fallback (last resort, deterministic) ───────────────────
|
||||
def _vclass(lemma):
|
||||
return lemma[-2:] if lemma[-2:] in ("ar", "er", "ir") else "ar"
|
||||
|
||||
|
||||
def _stem(lemma):
|
||||
return lemma[:-2]
|
||||
|
||||
|
||||
_REG = {
|
||||
("ind", "present", "ar"): ["o", "as", "a", "amos", "áis", "an"],
|
||||
("ind", "present", "er"): ["o", "es", "e", "emos", "éis", "en"],
|
||||
("ind", "present", "ir"): ["o", "es", "e", "imos", "ís", "en"],
|
||||
("ind", "preterite", "ar"): ["é", "aste", "ó", "amos", "asteis", "aron"],
|
||||
("ind", "preterite", "er"): ["í", "iste", "ió", "imos", "isteis", "ieron"],
|
||||
("ind", "preterite", "ir"): ["í", "iste", "ió", "imos", "isteis", "ieron"],
|
||||
("ind", "imperfect", "ar"): ["aba", "abas", "aba", "ábamos", "abais", "aban"],
|
||||
("ind", "imperfect", "er"): ["ía", "ías", "ía", "íamos", "íais", "ían"],
|
||||
("ind", "imperfect", "ir"): ["ía", "ías", "ía", "íamos", "íais", "ían"],
|
||||
("sbjv", "present", "ar"): ["e", "es", "e", "emos", "éis", "en"],
|
||||
("sbjv", "present", "er"): ["a", "as", "a", "amos", "áis", "an"],
|
||||
("sbjv", "present", "ir"): ["a", "as", "a", "amos", "áis", "an"],
|
||||
("sbjv", "imperfect", "ar"): ["ara", "aras", "ara", "áramos", "arais", "aran"],
|
||||
("sbjv", "imperfect", "er"): ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
|
||||
("sbjv", "imperfect", "ir"): ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
|
||||
}
|
||||
_FUT = ["é", "ás", "á", "emos", "éis", "án"]
|
||||
_COND = ["ía", "ías", "ía", "íamos", "íais", "ían"]
|
||||
|
||||
|
||||
def _slot_idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
if len(lemma) < 3 or lemma[-2:] not in ("ar", "er", "ir"):
|
||||
return None
|
||||
vc, st, i = _vclass(lemma), _stem(lemma), _slot_idx(person, number)
|
||||
if tense == "future":
|
||||
return lemma + _FUT[i]
|
||||
if tense == "conditional":
|
||||
return lemma + _COND[i]
|
||||
table = _REG.get((mood, tense, vc))
|
||||
if table:
|
||||
return st + table[i]
|
||||
if mood == "imp" and tense == "present":
|
||||
# affirmative tú imperative = 3sg present indicative
|
||||
pres = _REG.get(("ind", "present", vc))
|
||||
return st + pres[2] if number == "singular" else st + pres[5]
|
||||
return None
|
||||
|
||||
|
||||
# ── PUBLIC: verb conjugation ────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number, formality="informal"):
|
||||
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
|
||||
lemma = lemma.strip().lower()
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
formal = "form" if formality == "formal" else "infm"
|
||||
if p and n:
|
||||
for fkey in (formal, "any", "infm" if formal == "form" else "form"):
|
||||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}|{fkey}"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
m = _mlc_conjugate(lemma, mood, tense, person, number)
|
||||
if m:
|
||||
return m, "model"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
_IRREG_PART = { # guarantee the common irregular participles
|
||||
"escribir": "escrito", "describir": "descrito", "abrir": "abierto",
|
||||
"cubrir": "cubierto", "descubrir": "descubierto", "morir": "muerto",
|
||||
"poner": "puesto", "ver": "visto", "volver": "vuelto", "devolver": "devuelto",
|
||||
"hacer": "hecho", "deshacer": "deshecho", "decir": "dicho", "romper": "roto",
|
||||
"resolver": "resuelto", "freír": "frito", "imprimir": "impreso",
|
||||
"satisfacer": "satisfecho", "prever": "previsto", "revolver": "revuelto",
|
||||
}
|
||||
|
||||
|
||||
def participle(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
if lemma in _IRREG_PART:
|
||||
return _IRREG_PART[lemma], "lexicon"
|
||||
if lemma in _PART:
|
||||
return _PART[lemma], "lexicon"
|
||||
if lemma.endswith("ar"):
|
||||
return lemma[:-2] + "ado", "rule"
|
||||
if lemma[-2:] in ("er", "ir"):
|
||||
return lemma[:-2] + "ido", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
_IRREG_GER = {"dormir": "durmiendo", "morir": "muriendo", "pedir": "pidiendo",
|
||||
"sentir": "sintiendo", "mentir": "mintiendo", "servir": "sirviendo",
|
||||
"venir": "viniendo", "decir": "diciendo", "poder": "pudiendo",
|
||||
"ir": "yendo", "leer": "leyendo", "creer": "creyendo",
|
||||
"oír": "oyendo", "traer": "trayendo", "caer": "cayendo",
|
||||
"construir": "construyendo", "huir": "huyendo", "reír": "riendo"}
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
if lemma in _IRREG_GER:
|
||||
return _IRREG_GER[lemma], "lexicon"
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
if lemma.endswith("ar"):
|
||||
return lemma[:-2] + "ando", "rule"
|
||||
if lemma[-2:] in ("er", "ir"):
|
||||
return lemma[:-2] + "iendo", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: noun gender + number ────────────────────────────────────────────────
|
||||
_INVARIANT_PL = {"lunes", "martes", "miércoles", "jueves", "viernes",
|
||||
"crisis", "tesis", "análisis", "dosis", "virus", "paraguas"}
|
||||
|
||||
|
||||
def _gender_heuristic(noun):
|
||||
for suf, g in (("ión", "f"), ("dad", "f"), ("tad", "f"), ("umbre", "f"),
|
||||
("sis", "f"), ("ez", "f"), ("triz", "f"),
|
||||
("ema", "m"), ("ama", "m"), ("oma", "m"), ("aje", "m"),
|
||||
("or", "m"), ("án", "m"), ("ín", "m")):
|
||||
if noun.endswith(suf):
|
||||
return g
|
||||
if noun.endswith("o"):
|
||||
return "m"
|
||||
if noun.endswith("a"):
|
||||
return "f"
|
||||
return "m"
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g"):
|
||||
return d["g"]
|
||||
return _gender_heuristic(lemma)
|
||||
|
||||
|
||||
def _regular_plural(noun):
|
||||
if noun in _INVARIANT_PL:
|
||||
return noun
|
||||
if not noun:
|
||||
return noun
|
||||
last = noun[-1]
|
||||
if last == "z":
|
||||
return noun[:-1] + "ces"
|
||||
if last in "aeiouáéíóú":
|
||||
# stressed final vowel í/ú -> +es (rubí->rubíes), else +s
|
||||
if last in "íú":
|
||||
return noun + "es"
|
||||
return noun + "s"
|
||||
if last == "s":
|
||||
# esdrújula / stress-final handled crudely; most polysyllables invariant
|
||||
return noun
|
||||
return noun + "es"
|
||||
|
||||
|
||||
def inflect_noun(lemma, number, gender=None):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
if d:
|
||||
# honor a requested gender for animate nouns (gato -> gata)
|
||||
if gender and (gender, num) in d:
|
||||
return d[(gender, num)], "lexicon"
|
||||
if d.get(num):
|
||||
return d[num], "lexicon"
|
||||
if number == "singular":
|
||||
return lemma, "rule" if not d else "lexicon"
|
||||
return _regular_plural(lemma), "rule"
|
||||
|
||||
|
||||
# ── PUBLIC: adjective agreement ─────────────────────────────────────────────────
|
||||
_INV_GENDER_ADJ = {"español": "española", "trabajador": "trabajadora",
|
||||
"hablador": "habladora", "encantador": "encantadora",
|
||||
"alemán": "alemana", "francés": "francesa", "inglés": "inglesa"}
|
||||
|
||||
|
||||
def inflect_adj(lemma, gender, number):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _ADJS.get(lemma)
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
if d:
|
||||
form = d.get((gender, num))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# gender-invariant adjective (grande, feliz, azul): fem == masc.
|
||||
# For a missing plural, pluralize this gender's singular form.
|
||||
sg = d.get((gender, "SG")) or d.get(("m", "SG")) or lemma
|
||||
if number == "plural":
|
||||
return _regular_plural(sg), "rule"
|
||||
return sg, "lexicon"
|
||||
# rule fallback
|
||||
a = lemma
|
||||
if gender == "f":
|
||||
if a in _INV_GENDER_ADJ:
|
||||
a = _INV_GENDER_ADJ[a]
|
||||
elif a.endswith("o"):
|
||||
a = a[:-1] + "a"
|
||||
if number == "plural":
|
||||
a = _regular_plural(a)
|
||||
return a, ("rule" if (a != lemma or gender == "m") else "rule")
|
||||
|
||||
|
||||
# ── PUBLIC: clitic enclisis (dá + me + lo -> dámelo) ────────────────────────────
|
||||
def _strip_accents(s):
|
||||
return "".join(c for c in unicodedata.normalize("NFD", s)
|
||||
if unicodedata.category(c) != "Mn")
|
||||
|
||||
|
||||
def _count_syllables_vowelgroups(word):
|
||||
# crude: count vowel groups
|
||||
w = _strip_accents(word).lower()
|
||||
groups, prev = 0, False
|
||||
for ch in w:
|
||||
isv = ch in "aeiou"
|
||||
if isv and not prev:
|
||||
groups += 1
|
||||
prev = isv
|
||||
return groups
|
||||
|
||||
|
||||
def _host_stress_from_end(word):
|
||||
"""Stressed-syllable index counted from the end (1=last) of a verb host."""
|
||||
syls = _count_syllables_vowelgroups(word)
|
||||
if any(c in "áéíóú" for c in word):
|
||||
return None # already carries its own accent
|
||||
if word[-2:] in ("ar", "er", "ir"): # infinitive: oxytone
|
||||
return 1
|
||||
if word.endswith("ndo"): # gerund: paroxytone
|
||||
return 2
|
||||
if word[-1:] in "aeiouns" and syls >= 2: # default paroxytone
|
||||
return 2
|
||||
return 1 # monosyllable / consonant-final oxytone
|
||||
|
||||
|
||||
def attach_enclitics(verb_form, clitics):
|
||||
"""Append clitic pronouns to a verb (imperative/infinitive/gerund enclisis)
|
||||
and add a written accent when the resulting word becomes esdrújula/
|
||||
sobreesdrújula (stress >= 3 syllables from the end): dá+me+lo -> dámelo,
|
||||
lleva+me -> llévame, but dar+te -> darte and da+me -> dame (no accent)."""
|
||||
if not clitics:
|
||||
return verb_form
|
||||
tail = "".join(clitics)
|
||||
if any(c in "áéíóú" for c in verb_form): # host already accented
|
||||
return verb_form + tail
|
||||
sfe = _host_stress_from_end(verb_form)
|
||||
total_sfe = sfe + len(clitics) # each clitic = 1 syllable
|
||||
if total_sfe >= 3:
|
||||
return _accentuate_nucleus(verb_form, sfe) + tail
|
||||
return verb_form + tail
|
||||
|
||||
|
||||
def _accentuate_nucleus(word, sfe):
|
||||
"""Put a written accent on the syllable `sfe` positions from the word's end."""
|
||||
vowels = "aeiou"
|
||||
nuclei = [i for i, ch in enumerate(word) if ch in vowels]
|
||||
if not nuclei or sfe > len(nuclei):
|
||||
return word
|
||||
i = nuclei[-sfe]
|
||||
acc = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"}
|
||||
return word[:i] + acc[word[i]] + word[i + 1:]
|
||||
|
||||
|
||||
def _accentuate_last_stressed(word):
|
||||
# Restore the host's ORIGINAL lexical stress with a written accent.
|
||||
# Default Spanish stress: word ending in vowel/n/s -> penultimate syllable;
|
||||
# otherwise (e.g. infinitives in -r) -> last syllable.
|
||||
vowels = "aeiou"
|
||||
nuclei = [i for i, ch in enumerate(word) if ch in vowels]
|
||||
if not nuclei:
|
||||
return word
|
||||
if word[-1] in "aeiouns" and len(nuclei) >= 2:
|
||||
i = nuclei[-2] # paroxytone: penult nucleus
|
||||
else:
|
||||
i = nuclei[-1] # oxytone / monosyllable: last nucleus
|
||||
acc = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"}
|
||||
return word[:i] + acc[word[i]] + word[i + 1:]
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"source": "UniMorph Spanish (github.com/unimorph/spa)",
|
||||
"license": "CC-BY-SA 3.0 (Wiktionary-derived)",
|
||||
"total_forms": sum(len(v) for v in (_VERBS, _NOUNS, _ADJS)) if False else None,
|
||||
"verb_forms": len(_VERBS),
|
||||
"verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
"participles": len(_PART),
|
||||
"gerunds": len(_GER),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
tests = [
|
||||
("hablar", "ind", "present", "first", "singular", "hablo"),
|
||||
("comer", "ind", "present", "third", "plural", "comen"),
|
||||
("vivir", "ind", "present", "first", "plural", "vivimos"),
|
||||
("ser", "ind", "present", "third", "singular", "es"),
|
||||
("ir", "ind", "preterite", "first", "singular", "fui"),
|
||||
("tener", "ind", "future", "first", "singular", "tendré"),
|
||||
("hacer", "sbjv", "present", "first", "singular", "haga"),
|
||||
("dormir", "ind", "present", "first", "singular", "duermo"),
|
||||
("pensar", "sbjv", "present", "third", "singular", "piense"),
|
||||
("dar", "ind", "preterite", "third", "singular", "dio"),
|
||||
("poner", "ind", "conditional", "first", "singular", "pondría"),
|
||||
]
|
||||
ok = 0
|
||||
for lemma, mood, tense, per, num, exp in tests:
|
||||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||||
flag = "OK " if got == exp else "XX "
|
||||
if got == exp:
|
||||
ok += 1
|
||||
print(f" {flag}{lemma:8} {mood}/{tense} {per[:3]}.{num[:2]:3} -> {got:14} ({conf}) exp={exp}")
|
||||
print(f"verb tests {ok}/{len(tests)}")
|
||||
print(" gender casa:", noun_gender("casa"), "| problema:", noun_gender("problema"),
|
||||
"| agua:", noun_gender("agua"), "| mano:", noun_gender("mano"))
|
||||
print(" plural: luz->", inflect_noun("luz", "plural"), "| rey->", inflect_noun("rey", "plural"))
|
||||
print(" adj: rojo/f/pl->", inflect_adj("rojo", "f", "plural"),
|
||||
"| feliz/m/pl->", inflect_adj("feliz", "m", "plural"),
|
||||
"| grande/f/pl->", inflect_adj("grande", "f", "plural"))
|
||||
print(" enclisis: da+[me,lo]->", attach_enclitics("da", ["me", "lo"]),
|
||||
"| di+[me]->", attach_enclitics("di", ["me"]),
|
||||
"| dar+[se,lo]->", attach_enclitics("dar", ["se", "lo"]))
|
||||
@@ -0,0 +1,629 @@
|
||||
"""morphology_fr_full.py — production-grade French morphological generator.
|
||||
|
||||
Same architecture as morphology_it_full.py (shared Romance engine); French-specific
|
||||
data and rules swapped in. Backed by three real, Wiktionary-lineage sources:
|
||||
|
||||
VERBS
|
||||
UniMorph French (github.com/unimorph/fra, CC-BY-SA 3.0)
|
||||
7,535 verb lemmas × full paradigm, CLEAN orthography:
|
||||
indicatif présent / imparfait (PST;IPFV) / passé simple (PST;PFV) /
|
||||
futur, conditionnel (COND), subjonctif présent (SBJV;PRS) /
|
||||
subjonctif imparfait (SBJV;PST), impératif (POS;IMP), infinitif (NFIN),
|
||||
participe présent (V.CVB/V.PTCP;PRS), participe passé (V.PTCP;PST, m.sg).
|
||||
fr_irreg_verbs.json — high-frequency verbs UniMorph MISSES or mis-slots,
|
||||
above all ÊTRE (absent from UniMorph fra), plus avoir/aller/faire/… — the
|
||||
auxiliaries the passé-composé + être-agreement system depends on. Extracted
|
||||
from kaikki.org French (build_fr_irreg.py), reflexive/multiword forms
|
||||
dropped. This layer takes PRIORITY.
|
||||
|
||||
NOUNS + ADJECTIVES — kaikki.org French (Wiktionary extract, CC-BY-SA 3.0)
|
||||
noun lemmas WITH inherent gender (head-template arg) + real plural
|
||||
(cheval->chevaux, œil->yeux, invariable -s/-x/-z), resolved PER LEMMA.
|
||||
adjective lemmas with real feminine + plural (petit->petite/petits/petites,
|
||||
beau->belle/beaux/belles, heureux->heureuse, rouge invariant-gender).
|
||||
|
||||
Fallbacks (degrade, never crash, on OOV input):
|
||||
verbs : rule generator for -er / -ir(-iss-) / -re (with -cer/-ger spelling,
|
||||
future/conditional stems, imparfait/subjonctif endings)
|
||||
nouns : gender heuristic (endings) + rule pluralization (-al->-aux, -eau->-eaux)
|
||||
adjs : fem/plural agreement rules (-er->-ère, -eux->-euse, -f->-ve, +e default)
|
||||
|
||||
Confidence flag on every form: "lexicon" | "rule" | "fallback".
|
||||
|
||||
Public API (used by realizer_fr.py): identical signature to morphology_it_full.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "fra.unimorph")
|
||||
_IRREG = os.path.join(_HERE, "data", "fr_irreg_verbs.json")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_fr.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "fr_morph_cache.pkl")
|
||||
|
||||
# ── (mood, tense) -> UniMorph feature set that must ALL be present ────────────────
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): {"IND", "PRS"},
|
||||
("ind", "imperfect"): {"IND", "PST", "IPFV"}, # imparfait
|
||||
("ind", "passe_simple"): {"IND", "PST", "PFV"}, # passé simple
|
||||
("ind", "future"): {"IND", "FUT"},
|
||||
("ind", "conditional"): {"COND"}, # French: V;COND;1;SG
|
||||
("sbjv", "present"): {"SBJV", "PRS"},
|
||||
("sbjv", "imperfect"): {"SBJV", "PST"},
|
||||
("imp", "affirmative"): {"POS", "IMP"},
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
# ── build verb lexicon from UniMorph ─────────────────────────────────────────────
|
||||
def _build_verbs():
|
||||
verbs = {}
|
||||
part = {}
|
||||
ger = {}
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
|
||||
if head == "V.PTCP":
|
||||
if "PST" in f:
|
||||
part.setdefault(lemma, form)
|
||||
elif "PRS" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
if head == "V.CVB":
|
||||
if "PRS" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
if head != "V":
|
||||
continue
|
||||
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
for (mood, tense), req in _VERB_KEYMAP.items():
|
||||
if not req <= f:
|
||||
continue
|
||||
if tense == "imperfect" and "PFV" in f:
|
||||
continue
|
||||
if tense == "passe_simple" and "IPFV" in f:
|
||||
continue
|
||||
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
|
||||
break
|
||||
return verbs, part, ger
|
||||
|
||||
|
||||
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
|
||||
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
|
||||
"diminutive", "augmentative", "pejorative", "comparative",
|
||||
"superlative", "misspelling", "rare", "informal", "literary",
|
||||
"poetic", "error-unrecognized-form", "construed", "collective",
|
||||
"nonstandard", "dated", "Louisiana", "Switzerland", "Belgium"}
|
||||
|
||||
|
||||
def _kaikki_gender(arg):
|
||||
if not arg:
|
||||
return None
|
||||
a = str(arg).lower()
|
||||
if a.startswith("f"):
|
||||
return "f"
|
||||
if a.startswith("m"):
|
||||
return "m"
|
||||
return None
|
||||
|
||||
|
||||
def _build_nouns_adjs():
|
||||
nouns = {}
|
||||
adjs = {}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
pos = d.get("pos")
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word:
|
||||
continue
|
||||
forms = d.get("forms", []) or []
|
||||
|
||||
if pos == "noun":
|
||||
ht = d.get("head_templates") or []
|
||||
g = None
|
||||
if ht:
|
||||
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
|
||||
if g is None:
|
||||
tags = d.get("tags") or []
|
||||
if "feminine" in tags:
|
||||
g = "f"
|
||||
elif "masculine" in tags:
|
||||
g = "m"
|
||||
pl = None
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
if "plural" in t and not (t & _EXCL_FORM_TAGS):
|
||||
fm = x.get("form")
|
||||
if fm and " " not in fm and fm not in ("#", "-", "—"):
|
||||
pl = fm
|
||||
break
|
||||
if word not in nouns:
|
||||
nouns[word] = {"g": g, "SG": word, "PL": pl}
|
||||
else:
|
||||
cur = nouns[word]
|
||||
if cur.get("g") is None and g:
|
||||
cur["g"] = g
|
||||
if not cur.get("PL") and pl:
|
||||
cur["PL"] = pl
|
||||
|
||||
elif pos == "adj":
|
||||
d0 = adjs.setdefault(word, {})
|
||||
d0.setdefault(("m", "SG"), word)
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
fm = x.get("form")
|
||||
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
|
||||
continue
|
||||
if "feminine" in t and "plural" in t:
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
elif "masculine" in t and "plural" in t:
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
elif "feminine" in t:
|
||||
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
|
||||
elif "plural" in t:
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
return nouns, adjs
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs, part, ger = _build_verbs()
|
||||
nouns, adjs = _build_nouns_adjs()
|
||||
with open(_IRREG, encoding="utf-8") as fh:
|
||||
irreg = json.load(fh)
|
||||
data = {"verbs": verbs, "part": part, "ger": ger,
|
||||
"nouns": nouns, "adjs": adjs, "irreg": irreg}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
|
||||
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
|
||||
if os.path.getmtime(_CACHE) >= newest:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
|
||||
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
|
||||
_LEX["irreg"])
|
||||
|
||||
|
||||
# ── regular-ending rule fallback ─────────────────────────────────────────────────
|
||||
def _vclass(lemma):
|
||||
if lemma.endswith("er"):
|
||||
return "er"
|
||||
if lemma.endswith("ir"):
|
||||
return "ir"
|
||||
if lemma.endswith("re"):
|
||||
return "re"
|
||||
if lemma.endswith("oir"):
|
||||
return "oir"
|
||||
return None
|
||||
|
||||
|
||||
# present-tense endings [1sg,2sg,3sg,1pl,2pl,3pl]
|
||||
_REG_PRES = {
|
||||
"er": ["e", "es", "e", "ons", "ez", "ent"],
|
||||
"ir": ["is", "is", "it", "issons", "issez", "issent"], # -iss- class (finir)
|
||||
"re": ["s", "s", "", "ons", "ez", "ent"], # vendre: vends/vend
|
||||
}
|
||||
_REG_IMPF = ["ais", "ais", "ait", "ions", "iez", "aient"] # attaches to pres-1pl stem
|
||||
_REG_SUBJ = ["e", "es", "e", "ions", "iez", "ent"] # attaches to 3pl stem
|
||||
_REG_PS = { # passé simple
|
||||
"er": ["ai", "as", "a", "âmes", "âtes", "èrent"],
|
||||
"ir": ["is", "is", "it", "îmes", "îtes", "irent"],
|
||||
"re": ["is", "is", "it", "îmes", "îtes", "irent"],
|
||||
}
|
||||
_FUT = ["ai", "as", "a", "ons", "ez", "ont"]
|
||||
_COND = ["ais", "ais", "ait", "ions", "iez", "aient"]
|
||||
|
||||
|
||||
def _slot_idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _fut_stem(lemma, vc):
|
||||
"""Future/conditional stem = infinitive (drop final -e of -re)."""
|
||||
if vc == "re":
|
||||
return lemma[:-1] # vendre -> vendr-
|
||||
return lemma # parler-, finir-
|
||||
|
||||
|
||||
def _pres_1pl_stem(lemma, vc):
|
||||
"""Imparfait stem = present 1pl minus -ons (parlons->parl-, finissons->finiss-)."""
|
||||
if vc == "er":
|
||||
stem = lemma[:-2]
|
||||
if stem.endswith("g"):
|
||||
return stem + "e" # mangeons -> mange- (imparfait mangeais)
|
||||
if stem.endswith("c"):
|
||||
return stem[:-1] + "ç" # commençons -> commenç-
|
||||
return stem
|
||||
if vc == "ir":
|
||||
return lemma[:-1] + "iss" # finir -> finiss-
|
||||
if vc == "re":
|
||||
return lemma[:-2] # vendre -> vend-
|
||||
return lemma[:-2]
|
||||
|
||||
|
||||
def _apply_er_spelling(stem, ending):
|
||||
"""-cer/-ger softening before a/o (commençons, mangeons)."""
|
||||
if ending and ending[0] in ("a", "o"):
|
||||
if stem.endswith("c"):
|
||||
return stem[:-1] + "ç" + ending
|
||||
if stem.endswith("g"):
|
||||
return stem + "e" + ending
|
||||
return stem + ending
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
vc = _vclass(lemma)
|
||||
if vc is None:
|
||||
return None
|
||||
i = _slot_idx(person, number)
|
||||
|
||||
if mood == "ind" and tense in ("future", "conditional"):
|
||||
stem = _fut_stem(lemma, vc)
|
||||
end = (_FUT if tense == "future" else _COND)[i]
|
||||
return stem + end
|
||||
|
||||
if mood == "ind" and tense == "present":
|
||||
table = _REG_PRES.get("ir" if vc == "ir" else vc)
|
||||
if not table:
|
||||
return None
|
||||
body = lemma[:-2] if vc in ("er", "re") else lemma[:-1] if vc == "ir" else lemma[:-2]
|
||||
if vc == "ir":
|
||||
body = lemma[:-2] # fin- ; endings carry -iss-
|
||||
end = table[i]
|
||||
return body + end
|
||||
end = table[i]
|
||||
if vc == "er":
|
||||
return _apply_er_spelling(body, end)
|
||||
return body + end
|
||||
|
||||
if mood == "ind" and tense == "imperfect":
|
||||
stem = _pres_1pl_stem(lemma, vc)
|
||||
return stem + _REG_IMPF[i]
|
||||
|
||||
if mood == "ind" and tense == "passe_simple":
|
||||
table = _REG_PS.get("ir" if vc == "ir" else vc)
|
||||
if not table:
|
||||
return None
|
||||
body = lemma[:-2] if vc in ("er", "re") else lemma[:-2]
|
||||
end = table[i]
|
||||
if vc == "er":
|
||||
return _apply_er_spelling(body, end)
|
||||
return body + end
|
||||
|
||||
if mood == "sbjv" and tense == "present":
|
||||
# subjonctif: present-3pl stem + e/es/e/ions/iez/ent
|
||||
stem3 = _pres_1pl_stem(lemma, vc) if vc == "ir" else (
|
||||
lemma[:-2] if vc in ("er", "re") else lemma[:-2])
|
||||
if vc == "ir":
|
||||
stem3 = lemma[:-2] + "iss"
|
||||
end = _REG_SUBJ[i]
|
||||
if vc == "er":
|
||||
return _apply_er_spelling(stem3, end)
|
||||
return stem3 + end
|
||||
|
||||
if mood == "imp" and tense == "affirmative":
|
||||
# impératif ~ present indicative (tu drops -s for -er verbs)
|
||||
pres = _rule_conjugate(lemma, "ind", "present", person, number)
|
||||
if pres and vc == "er" and person == "second" and number == "singular":
|
||||
return pres[:-1] if pres.endswith("es") else pres
|
||||
return pres
|
||||
return None
|
||||
|
||||
|
||||
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number):
|
||||
"""Return (surface, confidence)."""
|
||||
lemma = lemma.strip().lower()
|
||||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number}"
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and key in ir:
|
||||
return ir[key], "lexicon"
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
if p and n:
|
||||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: participle + gerund/participe présent ────────────────────────────────
|
||||
def _participle_msg(lemma):
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and "part" in ir:
|
||||
return ir["part"], "lexicon"
|
||||
if lemma in _PART:
|
||||
return _PART[lemma], "lexicon"
|
||||
return None, None
|
||||
|
||||
|
||||
# irregular participle fem/plural quirks (drop circonflexe: dû->due, dus)
|
||||
_PART_FIX = {"dû": {"f|SG": "due", "m|PL": "dus", "f|PL": "dues"}}
|
||||
|
||||
|
||||
def participle(lemma, gender="m", number="singular"):
|
||||
"""Past participle with French gender/number agreement.
|
||||
m.sg = base; f.sg = base+e; m.pl = base+s (invariable if base ends s/x);
|
||||
f.pl = f.sg+s."""
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
msg, src = _participle_msg(lemma)
|
||||
conf = "lexicon"
|
||||
if msg is None:
|
||||
vc = _vclass(lemma)
|
||||
if vc == "er":
|
||||
msg = lemma[:-2] + "é"
|
||||
elif vc == "ir":
|
||||
msg = lemma[:-1] # finir -> fini, partir -> parti
|
||||
elif vc == "re":
|
||||
msg = lemma[:-2] + "u" # vendre -> vendu
|
||||
elif vc == "oir":
|
||||
msg = lemma[:-3] + "u" # (rough) recevoir handled by irreg
|
||||
else:
|
||||
return lemma, "fallback"
|
||||
conf = "rule"
|
||||
fix = _PART_FIX.get(msg)
|
||||
if fix and f"{g}|{num}" in fix:
|
||||
return fix[f"{g}|{num}"], conf
|
||||
if g == "m" and num == "SG":
|
||||
return msg, conf
|
||||
fem = msg + "e" if not msg.endswith("e") else msg
|
||||
if g == "f" and num == "SG":
|
||||
return fem, conf
|
||||
if g == "m" and num == "PL":
|
||||
return msg if msg.endswith(("s", "x")) else msg + "s", conf
|
||||
# f|PL
|
||||
return fem + "s", conf
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
"""Participe présent (base for gérondif 'en -ant')."""
|
||||
lemma = lemma.strip().lower()
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and "ger" in ir:
|
||||
return ir["ger"], "lexicon"
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
vc = _vclass(lemma)
|
||||
if vc == "er":
|
||||
stem = lemma[:-2]
|
||||
if stem.endswith("g"):
|
||||
return stem + "eant", "rule"
|
||||
if stem.endswith("c"):
|
||||
return stem[:-1] + "çant", "rule"
|
||||
return stem + "ant", "rule"
|
||||
if vc == "ir":
|
||||
return lemma[:-2] + "issant", "rule"
|
||||
if vc == "re":
|
||||
return lemma[:-2] + "ant", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
|
||||
_FEM_SUF = ("tion", "sion", "aison", "ance", "ence", "ette", "elle", "esse",
|
||||
"ude", "ade", "ée", "té", "tié", "ie", "ise", "ure", "eur")
|
||||
_MASC_SUF = ("ment", "age", "eau", "isme", "oir", "ier", "eur", "in", "on")
|
||||
|
||||
|
||||
def _gender_heuristic(noun):
|
||||
for suf in _FEM_SUF:
|
||||
if noun.endswith(suf):
|
||||
return "f"
|
||||
for suf in _MASC_SUF:
|
||||
if noun.endswith(suf):
|
||||
return "m"
|
||||
if noun.endswith("e"):
|
||||
return "f"
|
||||
return "m"
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g") in ("m", "f"):
|
||||
return d["g"]
|
||||
return _gender_heuristic(lemma)
|
||||
|
||||
|
||||
# closed sets for French plural irregularities
|
||||
_OU_X = {"bijou", "caillou", "chou", "genou", "hibou", "joujou", "pou"}
|
||||
_AIL_AUX = {"travail", "vitrail", "corail", "émail", "bail", "soupirail", "vantail"}
|
||||
_AL_S = {"bal", "carnaval", "festival", "récital", "chacal", "régal", "cal", "aval"}
|
||||
|
||||
|
||||
def _rule_plural(noun, gender):
|
||||
"""Deterministic French pluralization. (form, ok); ok=False FLAGS ambiguity."""
|
||||
if not noun:
|
||||
return noun, True
|
||||
if noun[-1:] in ("s", "x", "z"):
|
||||
return noun, True # invariable
|
||||
if noun in _OU_X:
|
||||
return noun + "x", True
|
||||
if noun.endswith(("eau", "au", "eu")):
|
||||
if noun in ("pneu", "bleu", "landau", "sarrau"):
|
||||
return noun + "s", True
|
||||
return noun + "x", True # bateau->bateaux, jeu->jeux
|
||||
if noun.endswith("al"):
|
||||
if noun in _AL_S:
|
||||
return noun + "s", True
|
||||
return noun[:-2] + "aux", True # cheval->chevaux
|
||||
if noun.endswith("ail"):
|
||||
if noun in _AIL_AUX:
|
||||
return noun[:-3] + "aux", True # travail->travaux
|
||||
return noun + "s", True
|
||||
return noun + "s", True # default
|
||||
|
||||
|
||||
def inflect_noun(lemma, number, gender=None):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if number == "singular":
|
||||
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
|
||||
if d and d.get("PL"):
|
||||
return d["PL"], "lexicon"
|
||||
g = gender or noun_gender(lemma)
|
||||
form, ok = _rule_plural(lemma, g)
|
||||
return form, ("rule" if ok else "fallback")
|
||||
|
||||
|
||||
# adjectives whose kaikki entries are unreliable: audited forms
|
||||
_ADJ_FIX = {
|
||||
"beau": {("m", "SG"): "beau", ("f", "SG"): "belle",
|
||||
("m", "PL"): "beaux", ("f", "PL"): "belles"},
|
||||
"nouveau": {("m", "SG"): "nouveau", ("f", "SG"): "nouvelle",
|
||||
("m", "PL"): "nouveaux", ("f", "PL"): "nouvelles"},
|
||||
"vieux": {("m", "SG"): "vieux", ("f", "SG"): "vieille",
|
||||
("m", "PL"): "vieux", ("f", "PL"): "vieilles"},
|
||||
"fou": {("m", "SG"): "fou", ("f", "SG"): "folle",
|
||||
("m", "PL"): "fous", ("f", "PL"): "folles"},
|
||||
"blanc": {("m", "SG"): "blanc", ("f", "SG"): "blanche",
|
||||
("m", "PL"): "blancs", ("f", "PL"): "blanches"},
|
||||
"long": {("m", "SG"): "long", ("f", "SG"): "longue",
|
||||
("m", "PL"): "longs", ("f", "PL"): "longues"},
|
||||
"bon": {("m", "SG"): "bon", ("f", "SG"): "bonne",
|
||||
("m", "PL"): "bons", ("f", "PL"): "bonnes"},
|
||||
}
|
||||
|
||||
|
||||
def _rule_fem(a):
|
||||
if a.endswith("e"):
|
||||
return a
|
||||
if a.endswith("er"):
|
||||
return a[:-2] + "ère"
|
||||
if a.endswith("eau"):
|
||||
return a[:-3] + "elle"
|
||||
if a.endswith("eux"):
|
||||
return a[:-3] + "euse"
|
||||
if a.endswith("f"):
|
||||
return a[:-1] + "ve"
|
||||
if a.endswith(("on", "en", "el", "eil", "et")):
|
||||
return a + a[-1] + "e" # bon->bonne, ancien->ancienne, muet->muette
|
||||
if a.endswith("c"):
|
||||
return a[:-1] + "che" # blanc->blanche (public->publique via FIX)
|
||||
return a + "e" # grand->grande, petit->petite, vert->verte
|
||||
|
||||
|
||||
def inflect_adj(lemma, gender, number):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
fix = _ADJ_FIX.get(lemma)
|
||||
if fix and (g, num) in fix:
|
||||
return fix[(g, num)], "lexicon"
|
||||
d = _ADJS.get(lemma)
|
||||
if d and d.get((g, num)):
|
||||
return d[(g, num)], "lexicon"
|
||||
# derive
|
||||
msc = (d.get(("m", "SG")) if d else None) or lemma
|
||||
if g == "m" and num == "SG":
|
||||
return msc, "lexicon" if d else "rule"
|
||||
fem = (d.get(("f", "SG")) if d else None) or _rule_fem(msc)
|
||||
if g == "f" and num == "SG":
|
||||
return fem, "lexicon" if (d and d.get(("f", "SG"))) else "rule"
|
||||
if g == "m" and num == "PL":
|
||||
if msc.endswith(("s", "x")):
|
||||
return msc, "rule"
|
||||
if msc.endswith("al"):
|
||||
return msc[:-2] + "aux", "rule"
|
||||
if msc.endswith("eau"):
|
||||
return msc + "x", "rule"
|
||||
return msc + "s", "rule"
|
||||
# f|PL
|
||||
return (fem if fem.endswith("s") else fem + "s"), "rule"
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"verb_source": "UniMorph French (github.com/unimorph/fra) + kaikki.org "
|
||||
"irregulars (être + high-frequency)",
|
||||
"noun_adj_source": "kaikki.org French (Wiktionary extract)",
|
||||
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
|
||||
"unimorph_verb_forms": len(_VERBS),
|
||||
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"irregular_verb_lemmas": len(_IRREGV),
|
||||
"participle_lemmas": len(_PART),
|
||||
"gerund_lemmas": len(_GER),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
tests = [
|
||||
("parler", "ind", "present", "first", "singular", "parle"),
|
||||
("être", "ind", "present", "third", "singular", "est"),
|
||||
("avoir", "ind", "present", "first", "singular", "ai"),
|
||||
("aller", "ind", "present", "third", "plural", "vont"),
|
||||
("finir", "ind", "present", "first", "singular", "finis"),
|
||||
("finir", "ind", "present", "first", "plural", "finissons"),
|
||||
("manger", "ind", "present", "first", "plural", "mangeons"),
|
||||
("faire", "ind", "future", "first", "singular", "ferai"),
|
||||
("pouvoir", "sbjv", "present", "third", "singular", "puisse"),
|
||||
("prendre", "ind", "passe_simple", "third", "singular", "prit"),
|
||||
("vendre", "ind", "present", "third", "singular", "vend"),
|
||||
("commencer", "ind", "imperfect", "first", "singular", "commençais"),
|
||||
]
|
||||
ok = 0
|
||||
for lemma, mood, tense, per, num, exp in tests:
|
||||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||||
flag = "OK " if got == exp else "XX "
|
||||
ok += got == exp
|
||||
print(f" {flag}{lemma:10} {mood}/{tense:12} {per[:3]}.{num[:2]} -> {got:12} ({conf}) exp={exp}")
|
||||
print(f"verb tests {ok}/{len(tests)}")
|
||||
print(" gender: maison=", noun_gender("maison"), "chat=", noun_gender("chat"),
|
||||
"cheval=", noun_gender("cheval"), "nation=", noun_gender("nation"))
|
||||
print(" plural: cheval->", inflect_noun("cheval", "plural"),
|
||||
"| bateau->", inflect_noun("bateau", "plural"),
|
||||
"| prix->", inflect_noun("prix", "plural"),
|
||||
"| chat->", inflect_noun("chat", "plural"))
|
||||
print(" adj: petit/f/sg->", inflect_adj("petit", "f", "singular"),
|
||||
"| beau/f/sg->", inflect_adj("beau", "f", "singular"),
|
||||
"| heureux/f/sg->", inflect_adj("heureux", "f", "singular"),
|
||||
"| national/m/pl->", inflect_adj("national", "m", "plural"))
|
||||
print(" part: aller/f/sg->", participle("aller", "f", "singular"),
|
||||
"| prendre/f/pl->", participle("prendre", "f", "plural"),
|
||||
"| finir/m/pl->", participle("finir", "m", "plural"))
|
||||
print(" ger: manger->", gerund("manger"), "| finir->", gerund("finir"))
|
||||
@@ -0,0 +1,588 @@
|
||||
"""morphology_it_full.py — production-grade Italian morphological generator.
|
||||
|
||||
NOT a toy. Backed by three real, Wiktionary-lineage lexical sources:
|
||||
|
||||
VERBS
|
||||
UniMorph Italian (github.com/unimorph/ita, CC-BY-SA 3.0)
|
||||
10,009 verb lemmas × full paradigm, CLEAN orthography (no stress marks):
|
||||
indicative present / imperfetto (PST;IPFV) / passato remoto (PST;PFV) /
|
||||
futuro, condizionale (COND),
|
||||
congiuntivo presente (SBJV;PRS) / imperfetto (SBJV;PST),
|
||||
affirmative imperative, infinitive, gerundio (V.CVB;PRS),
|
||||
past participle (masc-sg; fem/plural derived by vowel rule).
|
||||
it_irreg_verbs.json — 66 high-frequency verbs UniMorph MISSES
|
||||
(essere, avere, potere, uscire, tenere, prendere, piacere, …), extracted
|
||||
from kaikki.org Italian, filtered to standard forms, and DE-STRESSED to
|
||||
real orthography (kaikki marks tonic stress everywhere: pàrlo->parlo,
|
||||
avùto->avuto; final legit accents kept: sarò, è). Built by build_it_irreg.py.
|
||||
This layer takes priority — it supplies the two auxiliaries essere/avere,
|
||||
which the whole passato-prossimo / essere-agreement system depends on.
|
||||
|
||||
NOUNS + ADJECTIVES — kaikki.org Italian (Wiktionary extract, CC-BY-SA 3.0)
|
||||
noun lemmas WITH inherent gender (head-template arg) + real (often irregular)
|
||||
plural — uomo->uomini, uovo->uova, dito->dita, città invariant — resolved
|
||||
PER LEMMA, never guessed.
|
||||
adjective lemmas with real feminine + masc/fem plural (italiano->italiana/
|
||||
italiani/italiane, felice->felici invariant).
|
||||
|
||||
Fallbacks (degrade, never crash, on OOV input):
|
||||
verbs : rule generator for regular -are/-ere/-ire (with -care/-gare h-insertion
|
||||
and -ciare/-giare/-iare i-drop spelling rules)
|
||||
nouns : gender heuristic (endings) + rule pluralization (ambiguous -co/-go FLAGGED)
|
||||
adjs : -o/-a/-e gender rule + rule pluralization
|
||||
|
||||
Confidence flag on every form:
|
||||
"lexicon" from UniMorph / kaikki-irregular / kaikki noun-adj (trust: high)
|
||||
"rule" deterministic rule (trust: medium)
|
||||
"fallback" could not inflect; returned lemma / ambiguous (trust: low -> FLAG)
|
||||
|
||||
Public API (used by realizer_it.py):
|
||||
conjugate(lemma, mood, tense, person, number) -> (form, conf)
|
||||
participle(lemma, gender="m", number="singular") -> (form, conf)
|
||||
gerund(lemma) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"
|
||||
inflect_noun(lemma, number, gender=None) -> (form, conf)
|
||||
inflect_adj(lemma, gender, number) -> (form, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "ita.unimorph")
|
||||
_IRREG = os.path.join(_HERE, "data", "it_irreg_verbs.json")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_it.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "it_morph_cache.pkl")
|
||||
|
||||
# ── (mood, tense) -> UniMorph feature set that must ALL be present ────────────────
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): {"IND", "PRS"},
|
||||
("ind", "imperfect"): {"IND", "PST", "IPFV"},
|
||||
("ind", "passato_remoto"): {"IND", "PST", "PFV"},
|
||||
("ind", "future"): {"IND", "FUT"},
|
||||
("ind", "conditional"): {"COND"},
|
||||
("sbjv", "present"): {"SBJV", "PRS"},
|
||||
("sbjv", "imperfect"): {"SBJV", "PST"},
|
||||
("imp", "affirmative"): {"POS", "IMP"},
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
# ── build verb lexicon from UniMorph ─────────────────────────────────────────────
|
||||
def _build_verbs():
|
||||
verbs = {} # (lemma, "mood|tense|person|number") -> form
|
||||
part = {} # lemma -> masc-sg past participle
|
||||
ger = {} # lemma -> gerundio
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
|
||||
if head == "V.PTCP":
|
||||
if "PST" in f:
|
||||
part.setdefault(lemma, form)
|
||||
continue
|
||||
if head == "V.CVB": # gerundio (converb, present)
|
||||
if "PRS" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
if head != "V":
|
||||
continue
|
||||
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
for (mood, tense), req in _VERB_KEYMAP.items():
|
||||
# exact-set discipline: PST;PFV must not match PST;IPFV, etc.
|
||||
if not req <= f:
|
||||
continue
|
||||
# guard IND;PST ambiguity: require the specific aspect feature
|
||||
if tense == "imperfect" and "PFV" in f:
|
||||
continue
|
||||
if tense == "passato_remoto" and "IPFV" in f:
|
||||
continue
|
||||
# COND must not also be a subjunctive/imperative slot
|
||||
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
|
||||
break
|
||||
return verbs, part, ger
|
||||
|
||||
|
||||
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
|
||||
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
|
||||
"diminutive", "augmentative", "pejorative", "comparative",
|
||||
"superlative", "misspelling", "rare", "informal", "literary",
|
||||
"poetic", "error-unrecognized-form", "apocopic", "obsolete",
|
||||
"construed", "collective"}
|
||||
|
||||
|
||||
def _kaikki_gender(arg):
|
||||
if not arg:
|
||||
return None
|
||||
a = str(arg).lower()
|
||||
if a.startswith("f"):
|
||||
return "f"
|
||||
if a.startswith("m"):
|
||||
return "m"
|
||||
return None
|
||||
|
||||
|
||||
def _build_nouns_adjs():
|
||||
nouns = {} # lemma -> {"g","SG","PL"}
|
||||
adjs = {} # lemma -> {("m","SG"),("f","SG"),("m","PL"),("f","PL")}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
pos = d.get("pos")
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word:
|
||||
continue
|
||||
forms = d.get("forms", []) or []
|
||||
|
||||
if pos == "noun":
|
||||
ht = d.get("head_templates") or []
|
||||
g = None
|
||||
if ht:
|
||||
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
|
||||
if g is None:
|
||||
tags = d.get("tags") or []
|
||||
if "feminine" in tags:
|
||||
g = "f"
|
||||
elif "masculine" in tags:
|
||||
g = "m"
|
||||
pl = None
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
if "plural" in t and not (t & _EXCL_FORM_TAGS):
|
||||
fm = x.get("form")
|
||||
if fm and " " not in fm and fm != "#":
|
||||
pl = fm
|
||||
break
|
||||
if word not in nouns:
|
||||
nouns[word] = {"g": g, "SG": word, "PL": pl}
|
||||
else:
|
||||
cur = nouns[word]
|
||||
if cur.get("g") is None and g:
|
||||
cur["g"] = g
|
||||
if not cur.get("PL") and pl:
|
||||
cur["PL"] = pl
|
||||
|
||||
elif pos == "adj":
|
||||
d0 = adjs.setdefault(word, {})
|
||||
d0.setdefault(("m", "SG"), word)
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
fm = x.get("form")
|
||||
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
|
||||
continue
|
||||
if "feminine" in t and "plural" in t:
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
elif "masculine" in t and "plural" in t:
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
elif "feminine" in t:
|
||||
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
|
||||
elif "plural" in t: # invariant-gender adj (felice -> felici)
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
return nouns, adjs
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs, part, ger = _build_verbs()
|
||||
nouns, adjs = _build_nouns_adjs()
|
||||
with open(_IRREG, encoding="utf-8") as fh:
|
||||
irreg = json.load(fh)
|
||||
data = {"verbs": verbs, "part": part, "ger": ger,
|
||||
"nouns": nouns, "adjs": adjs, "irreg": irreg}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
|
||||
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
|
||||
if os.path.getmtime(_CACHE) >= newest:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
|
||||
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
|
||||
_LEX["irreg"])
|
||||
|
||||
|
||||
# ── regular-ending rule fallback ─────────────────────────────────────────────────
|
||||
def _vclass(lemma):
|
||||
if lemma.endswith("are"):
|
||||
return "are"
|
||||
if lemma.endswith("ere"):
|
||||
return "ere"
|
||||
if lemma.endswith("ire"):
|
||||
return "ire"
|
||||
return None
|
||||
|
||||
|
||||
# endings [1sg,2sg,3sg,1pl,2pl,3pl]
|
||||
_REG = {
|
||||
("ind", "present", "are"): ["o", "i", "a", "iamo", "ate", "ano"],
|
||||
("ind", "present", "ere"): ["o", "i", "e", "iamo", "ete", "ono"],
|
||||
("ind", "present", "ire"): ["o", "i", "e", "iamo", "ite", "ono"],
|
||||
("ind", "imperfect", "are"): ["avo", "avi", "ava", "avamo", "avate", "avano"],
|
||||
("ind", "imperfect", "ere"): ["evo", "evi", "eva", "evamo", "evate", "evano"],
|
||||
("ind", "imperfect", "ire"): ["ivo", "ivi", "iva", "ivamo", "ivate", "ivano"],
|
||||
("ind", "passato_remoto", "are"): ["ai", "asti", "ò", "ammo", "aste", "arono"],
|
||||
("ind", "passato_remoto", "ere"): ["ei", "esti", "é", "emmo", "este", "erono"],
|
||||
("ind", "passato_remoto", "ire"): ["ii", "isti", "ì", "immo", "iste", "irono"],
|
||||
("sbjv", "present", "are"): ["i", "i", "i", "iamo", "iate", "ino"],
|
||||
("sbjv", "present", "ere"): ["a", "a", "a", "iamo", "iate", "ano"],
|
||||
("sbjv", "present", "ire"): ["a", "a", "a", "iamo", "iate", "ano"],
|
||||
("sbjv", "imperfect", "are"): ["assi", "assi", "asse", "assimo", "aste", "assero"],
|
||||
("sbjv", "imperfect", "ere"): ["essi", "essi", "esse", "essimo", "este", "essero"],
|
||||
("sbjv", "imperfect", "ire"): ["issi", "issi", "isse", "issimo", "iste", "issero"],
|
||||
# imperative: 2sg,3sg(Lei),1pl,2pl,3pl (1sg has none)
|
||||
("imp", "affirmative", "are"): [None, "a", "i", "iamo", "ate", "ino"],
|
||||
("imp", "affirmative", "ere"): [None, "i", "a", "iamo", "ete", "ano"],
|
||||
("imp", "affirmative", "ire"): [None, "i", "a", "iamo", "ite", "ano"],
|
||||
}
|
||||
# future / conditional attach to a stem = infinitive minus final -e, with
|
||||
# -are -> -er (parlare->parler-), -ere/-ire keep (credere->creder-, dormir-)
|
||||
_FUT = ["ò", "ai", "à", "emo", "ete", "anno"]
|
||||
_COND = ["ei", "esti", "ebbe", "emmo", "este", "ebbero"]
|
||||
|
||||
|
||||
def _slot_idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _fut_stem(lemma, vc):
|
||||
body = lemma[:-3] # drop are/ere/ire
|
||||
if vc == "are":
|
||||
return body + "er"
|
||||
return body + vc[0] + "r" # ere->er? no: keep vowel: creder-, dormir-
|
||||
# NOTE corrected below
|
||||
|
||||
|
||||
def _apply_are_spelling(stem, ending):
|
||||
"""-care/-gare insert h before front endings; -ciare/-giare/-sciare/-iare drop i."""
|
||||
front = ending[:1] in ("i", "e")
|
||||
if stem.endswith(("c", "g")) and front:
|
||||
return stem + "h" + ending
|
||||
if stem.endswith(("ci", "gi", "sci")) and ending[:1] == "i":
|
||||
return stem[:-1] + ending # mangi+iamo -> mangiamo
|
||||
if stem.endswith("i") and ending[:1] == "i":
|
||||
return stem[:-1] + ending # studi+iamo -> studiamo
|
||||
return stem + ending
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
vc = _vclass(lemma)
|
||||
if vc is None:
|
||||
return None
|
||||
body = lemma[:-3]
|
||||
i = _slot_idx(person, number)
|
||||
if mood == "ind" and tense in ("future", "conditional"):
|
||||
stem = body + "er" if vc == "are" else body + vc[0] + "r"
|
||||
# ere: creder-, ire: dormir- -> body + 'e'/'i' + 'r'
|
||||
if vc == "ere":
|
||||
stem = body + "er"
|
||||
elif vc == "ire":
|
||||
stem = body + "ir"
|
||||
end = (_FUT if tense == "future" else _COND)[i]
|
||||
# spelling: -care/-gare -> cherò/gherò ; -ciare/-giare -> cerò/gerò
|
||||
if vc == "are":
|
||||
if body.endswith(("c", "g")):
|
||||
stem = body + "her"
|
||||
elif body.endswith(("ci", "gi", "sci")):
|
||||
stem = body[:-1] + "er"
|
||||
elif body.endswith("i"):
|
||||
stem = body[:-1] + "er"
|
||||
return stem + end
|
||||
table = _REG.get((mood, tense, vc))
|
||||
if not table:
|
||||
return None
|
||||
end = table[i]
|
||||
if end is None:
|
||||
return None
|
||||
if vc == "are":
|
||||
return _apply_are_spelling(body, end)
|
||||
# -ere/-ire: guard against double-i (dormi+iamo -> dormiamo)
|
||||
if body.endswith("i") and end[:1] == "i":
|
||||
return body[:-1] + end
|
||||
return body + end
|
||||
|
||||
|
||||
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number):
|
||||
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
|
||||
lemma = lemma.strip().lower()
|
||||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number}"
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and key in ir:
|
||||
return ir[key], "lexicon"
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
if p and n:
|
||||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: participle + gerund ──────────────────────────────────────────────────
|
||||
def _participle_msg(lemma):
|
||||
"""Return (masc-sg participle, source) or (None, None)."""
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and "part" in ir:
|
||||
return ir["part"], "lexicon"
|
||||
if lemma in _PART:
|
||||
return _PART[lemma], "lexicon"
|
||||
return None, None
|
||||
|
||||
|
||||
def participle(lemma, gender="m", number="singular"):
|
||||
"""Past participle with gender/number agreement (for essere-perfect & passives).
|
||||
UniMorph/irregular give masc-sg; fem/plural derived by final-vowel swap
|
||||
(-o -> -a/-i/-e), valid for regular -ato/-uto/-ito AND irregulars
|
||||
(preso->presa/presi/prese, aperto->aperta/aperti/aperte, morto->morta/...)."""
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
msg, src = _participle_msg(lemma)
|
||||
conf = "lexicon"
|
||||
if msg is None:
|
||||
vc = _vclass(lemma)
|
||||
if vc == "are":
|
||||
msg = lemma[:-3] + "ato"
|
||||
elif vc == "ere":
|
||||
msg = lemma[:-3] + "uto"
|
||||
elif vc == "ire":
|
||||
msg = lemma[:-3] + "ito"
|
||||
else:
|
||||
return lemma, "fallback"
|
||||
conf = "rule"
|
||||
# agreement: only -o participles inflect for gender+number
|
||||
if msg.endswith("o"):
|
||||
stem = msg[:-1]
|
||||
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
|
||||
return stem + suf, conf
|
||||
return msg, conf # non -o participle: leave as-is (rare)
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
ir = _IRREGV.get(lemma)
|
||||
if ir and "ger" in ir:
|
||||
return ir["ger"], "lexicon"
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
vc = _vclass(lemma)
|
||||
if vc == "are":
|
||||
return lemma[:-3] + "ando", "rule"
|
||||
if vc in ("ere", "ire"):
|
||||
return lemma[:-3] + "endo", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
|
||||
_FEM_SUF = ("zione", "sione", "gione", "tà", "tù", "trice", "aggine", "udine",
|
||||
"igine", "ie", "essa", "izia", "ezza")
|
||||
_MASC_SUF = ("ore", "ame", "iere", "ale", "ile")
|
||||
|
||||
|
||||
def _gender_heuristic(noun):
|
||||
for suf in _FEM_SUF:
|
||||
if noun.endswith(suf):
|
||||
return "f"
|
||||
for suf in _MASC_SUF:
|
||||
if noun.endswith(suf):
|
||||
return "m"
|
||||
if noun.endswith("o"):
|
||||
return "m"
|
||||
if noun.endswith("a"):
|
||||
return "f"
|
||||
if noun.endswith("à") or noun.endswith("ù"):
|
||||
return "f"
|
||||
return "m" # -e and consonant-final loanwords default masculine
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g") in ("m", "f"):
|
||||
return d["g"]
|
||||
return _gender_heuristic(lemma)
|
||||
|
||||
|
||||
def _rule_plural(noun, gender):
|
||||
"""Deterministic Italian pluralization. Returns (form, ok); ok=False FLAGS an
|
||||
ambiguous case the lexicon would normally resolve (-co/-go palatalization)."""
|
||||
if not noun:
|
||||
return noun, True
|
||||
# invariant: accented final vowel, consonant-final, monosyllable, -i final
|
||||
if noun[-1:] in ("à", "è", "é", "ì", "í", "ò", "ó", "ù", "ú"):
|
||||
return noun, True
|
||||
if noun[-1:] not in ("a", "e", "o", "i", "u"):
|
||||
return noun, True # consonant-final loanword: invariant
|
||||
if noun.endswith("i"):
|
||||
return noun, True # e.g. crisi, analisi: invariant
|
||||
if noun.endswith("io"):
|
||||
return noun[:-2] + "i", True # figlio->figli (unstressed i)
|
||||
if noun.endswith("cia") or noun.endswith("gia"):
|
||||
# vowel before cia/gia -> -cie/-gie ; consonant -> -ce/-ge (approx)
|
||||
return noun[:-2] + "e", True # arancia->arance (majority)
|
||||
if noun.endswith("ca"):
|
||||
return noun[:-2] + "che", True # amica->amiche
|
||||
if noun.endswith("ga"):
|
||||
return noun[:-2] + "ghe", True
|
||||
if noun.endswith("co"):
|
||||
return noun[:-2] + "chi", False # AMBIGUOUS (amico->amici) -> flag
|
||||
if noun.endswith("go"):
|
||||
return noun[:-2] + "ghi", False # AMBIGUOUS (psicologo->psicologi)
|
||||
if noun.endswith("a"):
|
||||
return noun[:-1] + "e", True # casa->case (m -a: -i, but rare)
|
||||
if noun.endswith("o"):
|
||||
return noun[:-1] + "i", True # libro->libri
|
||||
if noun.endswith("e"):
|
||||
return noun[:-1] + "i", True # cane->cani, chiave->chiavi
|
||||
return noun, True
|
||||
|
||||
|
||||
def inflect_noun(lemma, number, gender=None):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if number == "singular":
|
||||
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
|
||||
if d and d.get("PL"):
|
||||
return d["PL"], "lexicon"
|
||||
g = gender or noun_gender(lemma)
|
||||
form, ok = _rule_plural(lemma, g)
|
||||
return form, ("rule" if ok else "fallback")
|
||||
|
||||
|
||||
# adjectives whose kaikki entries are unreliable (messy inflection templates):
|
||||
# supply audited regular agreement forms (prenominal apocope handled in realizer).
|
||||
_ADJ_FIX = {
|
||||
"bello": {("m", "SG"): "bello", ("f", "SG"): "bella",
|
||||
("m", "PL"): "belli", ("f", "PL"): "belle"},
|
||||
"quello": {("m", "SG"): "quello", ("f", "SG"): "quella",
|
||||
("m", "PL"): "quelli", ("f", "PL"): "quelle"},
|
||||
}
|
||||
|
||||
|
||||
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
|
||||
def inflect_adj(lemma, gender, number):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
fix = _ADJ_FIX.get(lemma)
|
||||
if fix and (g, num) in fix:
|
||||
return fix[(g, num)], "lexicon"
|
||||
d = _ADJS.get(lemma)
|
||||
if d:
|
||||
form = d.get((g, num))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
|
||||
if num == "PL":
|
||||
pl, ok = _rule_plural(sg, g)
|
||||
return pl, ("rule" if ok else "fallback")
|
||||
return sg, "lexicon"
|
||||
# rule fallback
|
||||
a = lemma
|
||||
if a.endswith("o"): # -o/-a/-i/-e class
|
||||
base = a[:-1]
|
||||
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
|
||||
return base + suf, "rule"
|
||||
if a.endswith("e"): # felice-class: SG invariant, PL -i
|
||||
if num == "PL":
|
||||
return a[:-1] + "i", "rule"
|
||||
return a, "rule"
|
||||
if num == "PL":
|
||||
p, ok = _rule_plural(a, g)
|
||||
return p, ("rule" if ok else "fallback")
|
||||
return a, "rule"
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"verb_source": "UniMorph Italian (github.com/unimorph/ita) + kaikki.org "
|
||||
"irregulars (de-stressed)",
|
||||
"noun_adj_source": "kaikki.org Italian (Wiktionary extract)",
|
||||
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
|
||||
"unimorph_verb_forms": len(_VERBS),
|
||||
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"irregular_verb_lemmas": len(_IRREGV),
|
||||
"participle_lemmas": len(_PART),
|
||||
"gerund_lemmas": len(_GER),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
tests = [
|
||||
("parlare", "ind", "present", "first", "singular", "parlo"),
|
||||
("essere", "ind", "present", "third", "singular", "è"),
|
||||
("avere", "ind", "present", "first", "singular", "ho"),
|
||||
("mangiare", "ind", "present", "second", "singular", "mangi"),
|
||||
("finire", "ind", "present", "first", "singular", "finisco"),
|
||||
("andare", "ind", "present", "third", "plural", "vanno"),
|
||||
("fare", "ind", "future", "first", "singular", "farò"),
|
||||
("potere", "sbjv", "present", "third", "singular", "possa"),
|
||||
("prendere", "ind", "passato_remoto", "first", "singular", "presi"),
|
||||
("cercare", "ind", "present", "second", "singular", "cerchi"),
|
||||
("dormire", "ind", "present", "third", "plural", "dormono"),
|
||||
("credere", "ind", "future", "first", "singular", "crederò"),
|
||||
]
|
||||
ok = 0
|
||||
for lemma, mood, tense, per, num, exp in tests:
|
||||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||||
flag = "OK " if got == exp else "XX "
|
||||
ok += got == exp
|
||||
print(f" {flag}{lemma:9} {mood}/{tense:14} {per[:3]}.{num[:2]} -> {got:12} ({conf}) exp={exp}")
|
||||
print(f"verb tests {ok}/{len(tests)}")
|
||||
print(" gender: casa=", noun_gender("casa"), "problema=", noun_gender("problema"),
|
||||
"mano=", noun_gender("mano"), "città=", noun_gender("città"),
|
||||
"cane=", noun_gender("cane"))
|
||||
print(" plural: uomo->", inflect_noun("uomo", "plural"),
|
||||
"| uovo->", inflect_noun("uovo", "plural"),
|
||||
"| città->", inflect_noun("città", "plural"),
|
||||
"| amico->", inflect_noun("amico", "plural"),
|
||||
"| casa->", inflect_noun("casa", "plural"))
|
||||
print(" adj: italiano/f/pl->", inflect_adj("italiano", "f", "plural"),
|
||||
"| felice/m/pl->", inflect_adj("felice", "m", "plural"),
|
||||
"| bello/f/sg->", inflect_adj("bello", "f", "singular"))
|
||||
print(" part: aprire/f/sg->", participle("aprire", "f", "singular"),
|
||||
"| prendere/m/pl->", participle("prendere", "m", "plural"),
|
||||
"| andare/f/sg->", participle("andare", "f", "singular"))
|
||||
print(" ger: fare->", gerund("fare"), "| parlare->", gerund("parlare"))
|
||||
@@ -0,0 +1,666 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""morphology_lat_full.py — production-grade Latin morphological generator.
|
||||
|
||||
Latin is the FLAGSHIP dead-language realizer. It rides the *architecture* of the
|
||||
Romance/Italic engine (the same Realization / spec-driven design and the UniMorph
|
||||
loader pattern from morphology_it_full.py) but with the CASE SYSTEM RESTORED —
|
||||
the feature Romance lost. Latin therefore exercises machinery the modern Romance
|
||||
siblings never needed: 5 declensions x 6 cases x 2 numbers x 3 genders, plus a
|
||||
4-conjugation verb system with tense/mood/voice.
|
||||
|
||||
DATA (real, attested — no fabrication):
|
||||
|
||||
NOUNS + ADJECTIVES — UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0)
|
||||
163,182 N forms across ~thousands of lemmas, each with the full case paradigm
|
||||
N;NOM/GEN/DAT/ACC/ABL/VOC;SG/PL (real inflected forms, WITH macrons:
|
||||
puella->puellam, rēx->rēgis, corpus->corporis).
|
||||
244,197 ADJ forms with case x GENDER x number, incl. UniMorph's combined
|
||||
tags (GEN+DAT, MASC+FEM, MASC+FEM+NEUT) which are split on load.
|
||||
462,668 V.PTCP forms (participles) also carry case/gender/number.
|
||||
UniMorph N tags DO NOT encode inherent gender, so noun gender is inferred
|
||||
from the declension (nom-sg + gen-sg endings) with a curated exceptions
|
||||
map — the standard, attestable rule (1st decl -a/-ae = fem, 2nd -us/-i =
|
||||
masc, -um = neut, ...).
|
||||
|
||||
VERBS — RULE ENGINE (honest gap: UniMorph Latin's verb list is a 947-lemma
|
||||
sample of rare/prefixed verbs that MISSES every core textbook verb — amō,
|
||||
videō, sum, regō, ... are all absent). Latin conjugation is, however, highly
|
||||
regular, so verbs are generated by a deterministic 4-conjugation engine over
|
||||
curated principal parts (present / perfect / supine stems), sourced from
|
||||
standard references. Irregulars (sum, possum, eō, ferō, volō, nōlō, mālō)
|
||||
are curated full tables. Forms are flagged "rule" (not "lexicon") for honesty.
|
||||
|
||||
Confidence flag on every form (same contract as the Romance engine):
|
||||
"lexicon" from UniMorph (trust: high)
|
||||
"rule" deterministic morphology rule (trust: medium)
|
||||
"fallback" could not inflect; returned lemma (trust: low -> FLAG)
|
||||
|
||||
Public API (used by realizer_lat.py):
|
||||
decline_noun(lemma, case, number) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"|"n"
|
||||
decline_adj(lemma, case, gender, number) -> (form, conf)
|
||||
conjugate(lemma, tense, mood, voice, person, number) -> (form, conf)
|
||||
participle(lemma, kind, case, gender, number) -> (form, conf) # kind: prs|pfv|fut
|
||||
infinitive(lemma, tense="present", voice="active") -> (form, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "lat.unimorph")
|
||||
_CACHE = os.path.join(_HERE, "data", "lat_morph_cache.pkl")
|
||||
|
||||
_CASES = ("NOM", "GEN", "DAT", "ACC", "ABL", "VOC")
|
||||
_CASE_MAP = {"nom": "NOM", "gen": "GEN", "dat": "DAT", "acc": "ACC",
|
||||
"abl": "ABL", "voc": "VOC"}
|
||||
_NUM = {"singular": "SG", "plural": "PL"}
|
||||
_GEN = {"m": "MASC", "f": "FEM", "n": "NEUT"}
|
||||
|
||||
|
||||
# ── UniMorph loader: noun + adjective + participle case paradigms ────────────────
|
||||
def _build_cache():
|
||||
nouns = {} # lemma -> {(CASE, NUM): form}
|
||||
adjs = {} # lemma -> {(CASE, GEN, NUM): form}
|
||||
ptcps = {} # lemma -> {(CASE, GEN, NUM): form} (from V.PTCP; keyed loosely)
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
feats = tag.split(";")
|
||||
head = feats[0]
|
||||
fs = set(feats)
|
||||
case = next((c for c in _CASES if c in fs), None)
|
||||
# handle combined case tags like GEN+DAT
|
||||
if case is None:
|
||||
for f in feats:
|
||||
if "+" in f and any(c in f.split("+") for c in _CASES):
|
||||
case = [c for c in _CASES if c in f.split("+")]
|
||||
break
|
||||
num = "SG" if "SG" in fs else ("PL" if "PL" in fs else None)
|
||||
if case is None or num is None:
|
||||
continue
|
||||
cases = case if isinstance(case, list) else [case]
|
||||
|
||||
if head == "N":
|
||||
d = nouns.setdefault(lemma, {})
|
||||
for c in cases:
|
||||
d.setdefault((c, num), form)
|
||||
elif head == "ADJ":
|
||||
# gender may be combined: MASC+FEM+NEUT, MASC+FEM
|
||||
genders = []
|
||||
for g in ("MASC", "FEM", "NEUT"):
|
||||
if any(g == x or (g in x.split("+")) for x in feats):
|
||||
genders.append(g)
|
||||
if not genders:
|
||||
genders = ["MASC", "FEM", "NEUT"]
|
||||
d = adjs.setdefault(lemma, {})
|
||||
for c in cases:
|
||||
for g in genders:
|
||||
d.setdefault((c, g, num), form)
|
||||
data = {"nouns": nouns, "adjs": adjs, "ptcps": ptcps}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE) and os.path.exists(_UNIMORPH):
|
||||
if os.path.getmtime(_CACHE) >= os.path.getmtime(_UNIMORPH):
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_NOUNS, _ADJS = _LEX["nouns"], _LEX["adjs"]
|
||||
|
||||
|
||||
# ── noun gender inference (declension-based, curated exceptions) ─────────────────
|
||||
# Real, attestable rule: gender follows declension + nominative shape, with the
|
||||
# standard closed set of exceptions.
|
||||
_GENDER_EXC = {
|
||||
# 1st-declension masculines (people/agents)
|
||||
"agricola": "m", "poēta": "m", "nauta": "m", "incola": "m", "scrība": "m",
|
||||
"auriga": "m", "pīrāta": "m", "athlēta": "m",
|
||||
# 2nd-declension neuters / feminines
|
||||
"vīrus": "n", "vulgus": "n", "pelagus": "n", "humus": "f",
|
||||
# common 3rd-declension whose gender the ending would mispredict
|
||||
"rēx": "m", "dux": "m", "mīles": "m", "pater": "m", "frāter": "m",
|
||||
"homō": "m", "leō": "m", "sōl": "m", "mōns": "m", "pōns": "m", "fōns": "m",
|
||||
"sanguis": "m", "ōrdō": "m", "sermō": "m", "amor": "m", "dolor": "m",
|
||||
"labor": "m", "timor": "m", "honor": "m", "color": "m", "pēs": "m",
|
||||
"dēns": "m", "flōs": "m", "mōs": "m", "mensis": "m", "orbis": "m",
|
||||
"piscis": "m", "ignis": "m", "collis": "m", "grex": "m", "prīnceps": "m",
|
||||
"māter": "f", "soror": "f", "uxor": "f", "mulier": "f", "virgō": "f",
|
||||
"urbs": "f", "arx": "f", "pāx": "f", "lēx": "f", "lūx": "f", "vōx": "f",
|
||||
"nox": "f", "nix": "f", "vīs": "f", "salūs": "f", "virtūs": "f",
|
||||
"aetās": "f", "cīvitās": "f", "lībertās": "f", "vēritās": "f", "voluptās": "f",
|
||||
"nātiō": "f", "ratiō": "f", "ōrātiō": "f", "legiō": "f", "regiō": "f",
|
||||
"mens": "f", "gens": "f", "ars": "f", "pars": "f", "mors": "f", "sors": "f",
|
||||
"nāvis": "f", "turris": "f", "avis": "f", "vallis": "f", "classis": "f",
|
||||
"corpus": "n", "tempus": "n", "opus": "n", "genus": "n", "onus": "n",
|
||||
"pectus": "n", "latus": "n", "vulnus": "n", "scelus": "n", "sīdus": "n",
|
||||
"caput": "n", "iter": "n", "flūmen": "n", "nōmen": "n", "carmen": "n",
|
||||
"agmen": "n", "certāmen": "n", "lūmen": "n", "ōmen": "n", "cōgnōmen": "n",
|
||||
"mare": "n", "animal": "n", "exemplar": "n", "rēte": "n",
|
||||
# 4th-declension exceptions
|
||||
"manus": "f", "domus": "f", "tribus": "f", "porticus": "f", "īdūs": "f",
|
||||
"cornū": "n", "genū": "n", "gelū": "n", "verū": "n",
|
||||
# 5th-declension
|
||||
"diēs": "m", "merīdiēs": "m",
|
||||
}
|
||||
|
||||
|
||||
def _infer_gender(lemma):
|
||||
if lemma in _GENDER_EXC:
|
||||
return _GENDER_EXC[lemma]
|
||||
d = _NOUNS.get(lemma)
|
||||
nom = d.get(("NOM", "SG")) if d else lemma
|
||||
gen = d.get(("GEN", "SG")) if d else None
|
||||
nom = nom or lemma
|
||||
# 5th declension: gen -eī / -ēī
|
||||
if gen and (gen.endswith("eī") or gen.endswith("ēī")):
|
||||
return "f"
|
||||
# 1st declension: nom -a, gen -ae
|
||||
if nom.endswith("a") and (not gen or gen.endswith("ae")):
|
||||
return "f"
|
||||
# 2nd declension neuter: nom -um
|
||||
if nom.endswith("um"):
|
||||
return "n"
|
||||
# 2nd declension masc: nom -us/-er/-ir, gen -ī
|
||||
if (nom.endswith("us") or nom.endswith("er") or nom.endswith("ir")) and \
|
||||
(not gen or gen.endswith("ī")):
|
||||
return "m"
|
||||
# 4th declension: gen -ūs
|
||||
if gen and gen.endswith("ūs"):
|
||||
return "n" if nom.endswith("ū") else "m"
|
||||
# 3rd declension neuters by common nom endings
|
||||
if nom.endswith(("men", "us", "ur", "al", "ar", "e", "ma")):
|
||||
# -us here is 3rd-decl neuter type (corpus) only if gen shows -oris/-eris
|
||||
if nom.endswith("us") and gen and (gen.endswith("oris") or gen.endswith("eris")
|
||||
or gen.endswith("uris")):
|
||||
return "n"
|
||||
if nom.endswith(("men", "al", "ar", "e")):
|
||||
return "n"
|
||||
# default 3rd-declension: masculine (most common)
|
||||
return "m"
|
||||
|
||||
|
||||
_GENDER_CACHE = {}
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip()
|
||||
if lemma not in _GENDER_CACHE:
|
||||
_GENDER_CACHE[lemma] = _infer_gender(lemma)
|
||||
return _GENDER_CACHE[lemma]
|
||||
|
||||
|
||||
# ── PUBLIC: noun declension ─────────────────────────────────────────────────────
|
||||
def decline_noun(lemma, case, number):
|
||||
lemma = lemma.strip()
|
||||
C = _CASE_MAP.get(case, case.upper())
|
||||
N = _NUM.get(number, number)
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and (C, N) in d:
|
||||
return d[(C, N)], "lexicon"
|
||||
# abl sg often == the -e/-o form; try nom fallback
|
||||
if d:
|
||||
# try VOC==NOM, ACC neuter==NOM etc are already in data; last resort lemma
|
||||
return lemma, "fallback"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: adjective declension ────────────────────────────────────────────────
|
||||
def decline_adj(lemma, case, gender, number):
|
||||
lemma = lemma.strip()
|
||||
C = _CASE_MAP.get(case, case.upper())
|
||||
G = _GEN.get(gender, gender.upper())
|
||||
N = _NUM.get(number, number)
|
||||
d = _ADJS.get(lemma)
|
||||
if d and (C, G, N) in d:
|
||||
return d[(C, G, N)], "lexicon"
|
||||
# try other gender (some adjs listed only under MASC+FEM etc handled at load)
|
||||
if d:
|
||||
for altG in ("MASC", "FEM", "NEUT"):
|
||||
if (C, altG, N) in d:
|
||||
return d[(C, altG, N)], "lexicon"
|
||||
return lemma, "fallback"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# VERB RULE ENGINE (4 conjugations + curated irregulars)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Curated principal parts for common attested verbs:
|
||||
# lemma -> (conj, present_stem, perfect_stem, supine_stem)
|
||||
# conj in {1,2,3,"3io",4}. Stems carry macrons (matching UniMorph orthography).
|
||||
_VERBS = {
|
||||
"amō": (1, "am", "amāv", "amāt"),
|
||||
"laudō": (1, "laud", "laudāv", "laudāt"),
|
||||
"portō": (1, "port", "portāv", "portāt"),
|
||||
"vocō": (1, "voc", "vocāv", "vocāt"),
|
||||
"dō": (1, "d", "ded", "dat"),
|
||||
"spectō": (1, "spect", "spectāv", "spectāt"),
|
||||
"pugnō": (1, "pugn", "pugnāv", "pugnāt"),
|
||||
"labōrō": (1, "labōr", "labōrāv", "labōrāt"),
|
||||
"necō": (1, "nec", "necāv", "necāt"),
|
||||
"parō": (1, "par", "parāv", "parāt"),
|
||||
"cōgitō": (1, "cōgit", "cōgitāv", "cōgitāt"),
|
||||
"habitō": (1, "habit", "habitāv", "habitāt"),
|
||||
"nārrō": (1, "nārr", "nārrāv", "nārrāt"),
|
||||
"servō": (1, "serv", "servāv", "servāt"),
|
||||
"superō": (1, "super", "superāv", "superāt"),
|
||||
"oppugnō": (1, "oppugn", "oppugnāv", "oppugnāt"),
|
||||
"ambulō": (1, "ambul", "ambulāv", "ambulāt"),
|
||||
"clāmō": (1, "clām", "clāmāv", "clāmāt"),
|
||||
"vulnerō": (1, "vulner", "vulnerāv", "vulnerāt"),
|
||||
"aedificō": (1, "aedific", "aedificāv", "aedificāt"),
|
||||
"expugnō": (1, "expugn", "expugnāv", "expugnāt"),
|
||||
"dēfendō": (3, "dēfend", "dēfend", "dēfēns"),
|
||||
"petō": (3, "pet", "petīv", "petīt"),
|
||||
"occīdō": (3, "occīd", "occīd", "occīs"),
|
||||
"interficiō": ("3io", "interfic", "interfēc", "interfect"),
|
||||
"timeō": (2, "tim", "timu", None),
|
||||
"iaceō": (2, "iac", "iacu", None),
|
||||
"pāreō": (2, "pār", "pāru", "pārit"),
|
||||
"respondeō": (2, "respond", "respond", "respōns"),
|
||||
"vertō": (3, "vert", "vert", "vers"),
|
||||
"ostendō": (3, "ostend", "ostend", "ostent"),
|
||||
"cōnstituō": (3, "cōnstitu", "cōnstitu", "cōnstitūt"),
|
||||
"cōgnōscō": (3, "cōgnōsc", "cōgnōv", "cōgnit"),
|
||||
"crēdō": (3, "crēd", "crēdid", "crēdit"),
|
||||
"ēdūcō": (3, "ēdūc", "ēdūx", "ēduct"),
|
||||
"cōnservō": (1, "cōnserv", "cōnservāv", "cōnservāt"),
|
||||
"iuvō": (1, "iuv", "iūv", "iūt"),
|
||||
"dēbeō": (2, "dēb", "dēbu", "dēbit"),
|
||||
"moneō": (2, "mon", "monu", "monit"),
|
||||
"videō": (2, "vid", "vīd", "vīs"),
|
||||
"habeō": (2, "hab", "habu", "habit"),
|
||||
"teneō": (2, "ten", "tenu", "tent"),
|
||||
"timeō": (2, "tim", "timu", None),
|
||||
"terreō": (2, "terr", "terru", "territ"),
|
||||
"dēleō": (2, "dēl", "dēlēv", "dēlēt"),
|
||||
"iubeō": (2, "iub", "iuss", "iuss"),
|
||||
"maneō": (2, "man", "māns", "māns"),
|
||||
"moveō": (2, "mov", "mōv", "mōt"),
|
||||
"doceō": (2, "doc", "docu", "doct"),
|
||||
"sedeō": (2, "sed", "sēd", "sess"),
|
||||
"rīdeō": (2, "rīd", "rīs", "rīs"),
|
||||
"regō": (3, "reg", "rēx", "rēct"),
|
||||
"dūcō": (3, "dūc", "dūx", "duct"),
|
||||
"scrībō": (3, "scrīb", "scrīps", "scrīpt"),
|
||||
"mittō": (3, "mitt", "mīs", "miss"),
|
||||
"pōnō": (3, "pōn", "posu", "posit"),
|
||||
"agō": (3, "ag", "ēg", "āct"),
|
||||
"dīcō": (3, "dīc", "dīx", "dict"),
|
||||
"gerō": (3, "ger", "gess", "gest"),
|
||||
"vincō": (3, "vinc", "vīc", "vict"),
|
||||
"petō": (3, "pet", "petīv", "petīt"),
|
||||
"legō": (3, "leg", "lēg", "lēct"),
|
||||
"currō": (3, "curr", "cucurr", "curs"),
|
||||
"vīvō": (3, "vīv", "vīx", "vīct"),
|
||||
"quaerō": (3, "quaer", "quaesīv", "quaesīt"),
|
||||
"trahō": (3, "trah", "trāx", "tract"),
|
||||
"claudō": (3, "claud", "claus", "claus"),
|
||||
"cōgō": (3, "cōg", "coēg", "coāct"),
|
||||
"relinquō": (3, "relinqu", "relīqu", "relict"),
|
||||
"capiō": ("3io", "cap", "cēp", "capt"),
|
||||
"faciō": ("3io", "fac", "fēc", "fact"),
|
||||
"iaciō": ("3io", "iac", "iēc", "iact"),
|
||||
"rapiō": ("3io", "rap", "rapu", "rapt"),
|
||||
"fugiō": ("3io", "fug", "fūg", "fugit"),
|
||||
"cupiō": ("3io", "cup", "cupīv", "cupīt"),
|
||||
"accipiō": ("3io", "accip", "accēp", "accept"),
|
||||
"audiō": (4, "aud", "audīv", "audīt"),
|
||||
"veniō": (4, "ven", "vēn", "vent"),
|
||||
"sciō": (4, "sc", "scīv", "scīt"),
|
||||
"sentiō": (4, "sent", "sēns", "sēns"),
|
||||
"mūniō": (4, "mūn", "mūnīv", "mūnīt"),
|
||||
"dormiō": (4, "dorm", "dormīv", "dormīt"),
|
||||
"aperiō": (4, "aper", "aperu", "apert"),
|
||||
"inveniō": (4, "inven", "invēn", "invent"),
|
||||
}
|
||||
|
||||
# ── Present-system paradigms: full ending tables per conjugation, attached to the
|
||||
# bare present stem (pstem). Hardcoded from the standard grammar with correct
|
||||
# macrons/vowel-lengths — deterministic and independently verifiable. Keys:
|
||||
# (tense, mood, voice) -> {conj: [1sg,2sg,3sg,1pl,2pl,3pl]}
|
||||
_PARADIGM = {
|
||||
("present", "ind", "active"): {
|
||||
1: ["ō", "ās", "at", "āmus", "ātis", "ant"],
|
||||
2: ["eō", "ēs", "et", "ēmus", "ētis", "ent"],
|
||||
3: ["ō", "is", "it", "imus", "itis", "unt"],
|
||||
"3io": ["iō", "is", "it", "imus", "itis", "iunt"],
|
||||
4: ["iō", "īs", "it", "īmus", "ītis", "iunt"],
|
||||
},
|
||||
("present", "ind", "passive"): {
|
||||
1: ["or", "āris", "ātur", "āmur", "āminī", "antur"],
|
||||
2: ["eor", "ēris", "ētur", "ēmur", "ēminī", "entur"],
|
||||
3: ["or", "eris", "itur", "imur", "iminī", "untur"],
|
||||
"3io": ["ior", "eris", "itur", "imur", "iminī", "iuntur"],
|
||||
4: ["ior", "īris", "ītur", "īmur", "īminī", "iuntur"],
|
||||
},
|
||||
("imperfect", "ind", "active"): {
|
||||
1: ["ābam", "ābās", "ābat", "ābāmus", "ābātis", "ābant"],
|
||||
2: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"],
|
||||
3: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"],
|
||||
"3io": ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"],
|
||||
4: ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"],
|
||||
},
|
||||
("imperfect", "ind", "passive"): {
|
||||
1: ["ābar", "ābāris", "ābātur", "ābāmur", "ābāminī", "ābantur"],
|
||||
2: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"],
|
||||
3: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"],
|
||||
"3io": ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"],
|
||||
4: ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"],
|
||||
},
|
||||
("future", "ind", "active"): {
|
||||
1: ["ābō", "ābis", "ābit", "ābimus", "ābitis", "ābunt"],
|
||||
2: ["ēbō", "ēbis", "ēbit", "ēbimus", "ēbitis", "ēbunt"],
|
||||
3: ["am", "ēs", "et", "ēmus", "ētis", "ent"],
|
||||
"3io": ["iam", "iēs", "iet", "iēmus", "iētis", "ient"],
|
||||
4: ["iam", "iēs", "iet", "iēmus", "iētis", "ient"],
|
||||
},
|
||||
("future", "ind", "passive"): {
|
||||
1: ["ābor", "āberis", "ābitur", "ābimur", "ābiminī", "ābuntur"],
|
||||
2: ["ēbor", "ēberis", "ēbitur", "ēbimur", "ēbiminī", "ēbuntur"],
|
||||
3: ["ar", "ēris", "ētur", "ēmur", "ēminī", "entur"],
|
||||
"3io": ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"],
|
||||
4: ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"],
|
||||
},
|
||||
("present", "sbjv", "active"): {
|
||||
1: ["em", "ēs", "et", "ēmus", "ētis", "ent"],
|
||||
2: ["eam", "eās", "eat", "eāmus", "eātis", "eant"],
|
||||
3: ["am", "ās", "at", "āmus", "ātis", "ant"],
|
||||
"3io": ["iam", "iās", "iat", "iāmus", "iātis", "iant"],
|
||||
4: ["iam", "iās", "iat", "iāmus", "iātis", "iant"],
|
||||
},
|
||||
("present", "sbjv", "passive"): {
|
||||
1: ["er", "ēris", "ētur", "ēmur", "ēminī", "entur"],
|
||||
2: ["ear", "eāris", "eātur", "eāmur", "eāminī", "eantur"],
|
||||
3: ["ar", "āris", "ātur", "āmur", "āminī", "antur"],
|
||||
"3io": ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"],
|
||||
4: ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"],
|
||||
},
|
||||
("imperfect", "sbjv", "active"): {
|
||||
1: ["ārem", "ārēs", "āret", "ārēmus", "ārētis", "ārent"],
|
||||
2: ["ērem", "ērēs", "ēret", "ērēmus", "ērētis", "ērent"],
|
||||
3: ["erem", "erēs", "eret", "erēmus", "erētis", "erent"],
|
||||
"3io": ["erem", "erēs", "eret", "erēmus", "erētis", "erent"],
|
||||
4: ["īrem", "īrēs", "īret", "īrēmus", "īrētis", "īrent"],
|
||||
},
|
||||
("imperfect", "sbjv", "passive"): {
|
||||
1: ["ārer", "ārēris", "ārētur", "ārēmur", "ārēminī", "ārentur"],
|
||||
2: ["ērer", "ērēris", "ērētur", "ērēmur", "ērēminī", "ērentur"],
|
||||
3: ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"],
|
||||
"3io": ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"],
|
||||
4: ["īrer", "īrēris", "īrētur", "īrēmur", "īrēminī", "īrentur"],
|
||||
},
|
||||
}
|
||||
# perfect-active endings (added to perfect stem) — same for all conjugations
|
||||
_PERF_ACT = {
|
||||
("perfect", "ind"): ["ī", "istī", "it", "imus", "istis", "ērunt"],
|
||||
("pluperfect", "ind"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"],
|
||||
("futureperfect", "ind"): ["erō", "eris", "erit", "erimus", "eritis", "erint"],
|
||||
("perfect", "sbjv"): ["erim", "erīs", "erit", "erīmus", "erītis", "erint"],
|
||||
("pluperfect", "sbjv"):["issem", "issēs", "isset", "issēmus", "issētis", "issent"],
|
||||
}
|
||||
|
||||
|
||||
def _idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _present_system(conj, pstem, tense, mood, voice, person, number):
|
||||
"""Generate a present-system form (present/imperfect/future ind & subj)."""
|
||||
table = _PARADIGM.get((tense, mood, voice))
|
||||
if not table or conj not in table:
|
||||
return None
|
||||
return pstem + table[conj][_idx(person, number)]
|
||||
|
||||
|
||||
def _active_infinitive_stem(conj, pstem):
|
||||
return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e",
|
||||
"3io": pstem + "e", 4: pstem + "ī"}[conj]
|
||||
|
||||
|
||||
_IRREG = {
|
||||
"sum": {
|
||||
("present", "ind", "active"): ["sum", "es", "est", "sumus", "estis", "sunt"],
|
||||
("imperfect", "ind", "active"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"],
|
||||
("future", "ind", "active"): ["erō", "eris", "erit", "erimus", "eritis", "erunt"],
|
||||
("perfect", "ind", "active"): ["fuī", "fuistī", "fuit", "fuimus", "fuistis", "fuērunt"],
|
||||
("pluperfect", "ind", "active"): ["fueram", "fuerās", "fuerat", "fuerāmus", "fuerātis", "fuerant"],
|
||||
("present", "sbjv", "active"): ["sim", "sīs", "sit", "sīmus", "sītis", "sint"],
|
||||
("imperfect", "sbjv", "active"): ["essem", "essēs", "esset", "essēmus", "essētis", "essent"],
|
||||
},
|
||||
"possum": {
|
||||
("present", "ind", "active"): ["possum", "potes", "potest", "possumus", "potestis", "possunt"],
|
||||
("imperfect", "ind", "active"): ["poteram", "poterās", "poterat", "poterāmus", "poterātis", "poterant"],
|
||||
("future", "ind", "active"): ["poterō", "poteris", "poterit", "poterimus", "poteritis", "poterunt"],
|
||||
("perfect", "ind", "active"): ["potuī", "potuistī", "potuit", "potuimus", "potuistis", "potuērunt"],
|
||||
("present", "sbjv", "active"): ["possim", "possīs", "possit", "possīmus", "possītis", "possint"],
|
||||
},
|
||||
"eō": {
|
||||
("present", "ind", "active"): ["eō", "īs", "it", "īmus", "ītis", "eunt"],
|
||||
("imperfect", "ind", "active"): ["ībam", "ībās", "ībat", "ībāmus", "ībātis", "ībant"],
|
||||
("future", "ind", "active"): ["ībō", "ībis", "ībit", "ībimus", "ībitis", "ībunt"],
|
||||
("perfect", "ind", "active"): ["iī", "īstī", "iit", "iimus", "īstis", "iērunt"],
|
||||
("present", "sbjv", "active"): ["eam", "eās", "eat", "eāmus", "eātis", "eant"],
|
||||
},
|
||||
"volō": {
|
||||
("present", "ind", "active"): ["volō", "vīs", "vult", "volumus", "vultis", "volunt"],
|
||||
("imperfect", "ind", "active"): ["volēbam", "volēbās", "volēbat", "volēbāmus", "volēbātis", "volēbant"],
|
||||
("future", "ind", "active"): ["volam", "volēs", "volet", "volēmus", "volētis", "volent"],
|
||||
("perfect", "ind", "active"): ["voluī", "voluistī", "voluit", "voluimus", "voluistis", "voluērunt"],
|
||||
("present", "sbjv", "active"): ["velim", "velīs", "velit", "velīmus", "velītis", "velint"],
|
||||
},
|
||||
"nōlō": {
|
||||
("present", "ind", "active"): ["nōlō", "nōn vīs", "nōn vult", "nōlumus", "nōn vultis", "nōlunt"],
|
||||
("present", "sbjv", "active"): ["nōlim", "nōlīs", "nōlit", "nōlīmus", "nōlītis", "nōlint"],
|
||||
},
|
||||
"ferō": {
|
||||
("present", "ind", "active"): ["ferō", "fers", "fert", "ferimus", "fertis", "ferunt"],
|
||||
("imperfect", "ind", "active"): ["ferēbam", "ferēbās", "ferēbat", "ferēbāmus", "ferēbātis", "ferēbant"],
|
||||
("future", "ind", "active"): ["feram", "ferēs", "feret", "ferēmus", "ferētis", "ferent"],
|
||||
("perfect", "ind", "active"): ["tulī", "tulistī", "tulit", "tulimus", "tulistis", "tulērunt"],
|
||||
("present", "sbjv", "active"): ["feram", "ferās", "ferat", "ferāmus", "ferātis", "ferant"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def conjugate(lemma, tense, mood, voice="active", person="third", number="singular"):
|
||||
"""Return (surface, confidence). Perfect-passive forms are periphrastic and
|
||||
handled in the realizer (sum + PPP); this returns synthetic forms only."""
|
||||
lemma = lemma.strip()
|
||||
i = _idx(person, number)
|
||||
ir = _IRREG.get(lemma)
|
||||
if ir:
|
||||
tbl = ir.get((tense, mood, voice)) or ir.get((tense, mood, "active"))
|
||||
if tbl and tbl[i]:
|
||||
return tbl[i], "rule"
|
||||
v = _VERBS.get(lemma)
|
||||
if not v:
|
||||
v = _infer_principal_parts(lemma)
|
||||
if not v:
|
||||
return lemma, "fallback"
|
||||
conj, pstem, perfstem, supstem = v
|
||||
# imperative (present active) 2sg / 2pl
|
||||
if mood == "imp":
|
||||
return _imperative(conj, pstem, person, number), "rule"
|
||||
# perfect-system active
|
||||
if tense in ("perfect", "pluperfect", "futureperfect") and voice == "active":
|
||||
if not perfstem:
|
||||
return lemma, "fallback"
|
||||
end = _PERF_ACT.get((tense, mood))
|
||||
if end:
|
||||
return perfstem + end[i], "rule"
|
||||
# present-system (active + passive)
|
||||
if tense in ("present", "imperfect", "future"):
|
||||
form = _present_system(conj, pstem, tense, mood, voice, person, number)
|
||||
if form:
|
||||
return form, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def _imperative(conj, pstem, person, number):
|
||||
if number == "singular":
|
||||
return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e",
|
||||
"3io": pstem + "e", 4: pstem + "ī"}[conj]
|
||||
return {1: pstem + "āte", 2: pstem + "ēte", 3: pstem + "ite",
|
||||
"3io": pstem + "ite", 4: pstem + "īte"}[conj]
|
||||
|
||||
|
||||
def _infer_principal_parts(lemma):
|
||||
"""OOV fallback: infer conjugation + stems from the 1sg-present citation form.
|
||||
Perfect/supine stems are guessed regularly (often wrong for 3rd conj) and the
|
||||
resulting forms are still returned as 'rule' but the realizer down-weights."""
|
||||
if lemma.endswith("ō"):
|
||||
base = lemma[:-1]
|
||||
# can't distinguish conj from 1sg alone reliably; default by ending vowel
|
||||
if base.endswith("i"):
|
||||
return ("3io", base[:-1], base[:-1] + "īv", base[:-1] + "īt")
|
||||
return (3, base, base + "s", base + "t")
|
||||
return None
|
||||
|
||||
|
||||
# ── PUBLIC: participles ─────────────────────────────────────────────────────────
|
||||
def participle(lemma, kind, case="nom", gender="m", number="singular"):
|
||||
"""kind: 'prs' (present active, -ns/-ntis), 'pfv' (perfect passive, -tus),
|
||||
'fut' (future active, -tūrus). Declined as an adjective via rule endings.
|
||||
Returns (form, conf)."""
|
||||
v = _VERBS.get(lemma)
|
||||
if not v:
|
||||
return lemma, "fallback"
|
||||
conj, pstem, perfstem, supstem = v
|
||||
if kind == "pfv":
|
||||
if not supstem:
|
||||
return lemma, "fallback"
|
||||
base = supstem[:-1] if supstem.endswith("t") or supstem.endswith("s") else supstem
|
||||
stem = supstem # supine stem already ends in t/s: amāt- -> amātus
|
||||
return _decline_us_a_um(stem, case, gender, number), "rule"
|
||||
if kind == "fut":
|
||||
if not supstem:
|
||||
return lemma, "fallback"
|
||||
return _decline_us_a_um(supstem + "ūr", case, gender, number), "rule"
|
||||
if kind == "prs":
|
||||
# present active participle: stem + ns (nom), stem + nt- (oblique), 3rd-decl
|
||||
pv = {1: "ā", 2: "ē", 3: "ē", "3io": "iē", 4: "iē"}[conj]
|
||||
ntstem = pstem + pv + "nt"
|
||||
return _decline_pres_ptcp(pstem + pv, case, gender, number), "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def _decline_us_a_um(stem, case, gender, number):
|
||||
"""Decline a -us/-a/-um adjective/participle stem (2-1-2 declension)."""
|
||||
C = _CASE_MAP.get(case, case.upper())
|
||||
end = {
|
||||
("NOM", "m", "singular"): "us", ("NOM", "f", "singular"): "a", ("NOM", "n", "singular"): "um",
|
||||
("GEN", "m", "singular"): "ī", ("GEN", "f", "singular"): "ae", ("GEN", "n", "singular"): "ī",
|
||||
("DAT", "m", "singular"): "ō", ("DAT", "f", "singular"): "ae", ("DAT", "n", "singular"): "ō",
|
||||
("ACC", "m", "singular"): "um", ("ACC", "f", "singular"): "am", ("ACC", "n", "singular"): "um",
|
||||
("ABL", "m", "singular"): "ō", ("ABL", "f", "singular"): "ā", ("ABL", "n", "singular"): "ō",
|
||||
("VOC", "m", "singular"): "e", ("VOC", "f", "singular"): "a", ("VOC", "n", "singular"): "um",
|
||||
("NOM", "m", "plural"): "ī", ("NOM", "f", "plural"): "ae", ("NOM", "n", "plural"): "a",
|
||||
("GEN", "m", "plural"): "ōrum", ("GEN", "f", "plural"): "ārum", ("GEN", "n", "plural"): "ōrum",
|
||||
("DAT", "m", "plural"): "īs", ("DAT", "f", "plural"): "īs", ("DAT", "n", "plural"): "īs",
|
||||
("ACC", "m", "plural"): "ōs", ("ACC", "f", "plural"): "ās", ("ACC", "n", "plural"): "a",
|
||||
("ABL", "m", "plural"): "īs", ("ABL", "f", "plural"): "īs", ("ABL", "n", "plural"): "īs",
|
||||
("VOC", "m", "plural"): "ī", ("VOC", "f", "plural"): "ae", ("VOC", "n", "plural"): "a",
|
||||
}.get((C, gender, number), "us")
|
||||
return stem + end
|
||||
|
||||
|
||||
def _decline_pres_ptcp(stem, case, gender, number):
|
||||
"""Present active participle (amāns, amantis) — 3rd-declension, stem+ns/nt."""
|
||||
C = _CASE_MAP.get(case, case.upper())
|
||||
if C == "NOM" and number == "singular":
|
||||
return stem + "ns"
|
||||
if C == "VOC" and number == "singular":
|
||||
return stem + "ns"
|
||||
base = stem + "nt"
|
||||
end = {
|
||||
("GEN", "singular"): "is", ("DAT", "singular"): "ī",
|
||||
("ACC", "singular"): "em" if gender != "n" else "",
|
||||
("ABL", "singular"): "e",
|
||||
("NOM", "plural"): "ēs" if gender != "n" else "ia",
|
||||
("GEN", "plural"): "ium", ("DAT", "plural"): "ibus",
|
||||
("ACC", "plural"): "ēs" if gender != "n" else "ia",
|
||||
("ABL", "plural"): "ibus", ("VOC", "plural"): "ēs",
|
||||
}.get((C, number), "is")
|
||||
if C == "ACC" and number == "singular" and gender == "n":
|
||||
return stem + "ns"
|
||||
return base + end
|
||||
|
||||
|
||||
def infinitive(lemma, tense="present", voice="active"):
|
||||
lemma = lemma.strip()
|
||||
if lemma == "sum":
|
||||
return ("esse", "rule") if tense == "present" else ("fuisse", "rule")
|
||||
v = _VERBS.get(lemma)
|
||||
if not v:
|
||||
return lemma, "fallback"
|
||||
conj, pstem, perfstem, supstem = v
|
||||
if tense == "present":
|
||||
if voice == "active":
|
||||
return _active_infinitive_stem(conj, pstem).rstrip() + \
|
||||
("re" if conj != 3 and conj != "3io" else "re"), "rule"
|
||||
# passive present infinitive
|
||||
base = {1: pstem + "ā", 2: pstem + "ē", 4: pstem + "ī"}.get(conj)
|
||||
if base:
|
||||
return base + "rī", "rule"
|
||||
return pstem + "ī", "rule" # 3rd: regī
|
||||
if tense == "perfect" and voice == "active" and perfstem:
|
||||
return perfstem + "isse", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"noun_adj_source": "UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0)",
|
||||
"verb_source": "rule-based 4-conjugation engine over curated attested "
|
||||
"principal parts (UniMorph verb list is a 947-lemma sample "
|
||||
"MISSING all core verbs — amō/sum/videō absent)",
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
"curated_verb_lemmas": len(_VERBS) + len(_IRREG),
|
||||
"gender_inference": "declension-based (nom+gen endings) + curated exceptions",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
print("\n-- noun declension puella (1st, fem) --")
|
||||
for c in ("nom", "gen", "dat", "acc", "abl", "voc"):
|
||||
print(f" {c}: sg={decline_noun('puella', c, 'singular')[0]:10} "
|
||||
f"pl={decline_noun('puella', c, 'plural')[0]}")
|
||||
print("\n-- rēx (3rd, m):", [decline_noun('rēx', c, 'singular')[0] for c in ('nom','gen','dat','acc','abl')])
|
||||
print("-- gender: puella=", noun_gender("puella"), "rēx=", noun_gender("rēx"),
|
||||
"bellum=", noun_gender("bellum"), "corpus=", noun_gender("corpus"),
|
||||
"manus=", noun_gender("manus"), "diēs=", noun_gender("diēs"))
|
||||
print("\n-- conjugate videō (2nd) present ind active --")
|
||||
for p in ("first", "second", "third"):
|
||||
for n in ("singular", "plural"):
|
||||
print(f" {p[:3]}.{n[:2]}: {conjugate('videō','present','ind','active',p,n)[0]}")
|
||||
print("-- amō forms:", conjugate("amō","present","ind","active","first","singular")[0],
|
||||
conjugate("amō","imperfect","ind","active","third","plural")[0],
|
||||
conjugate("amō","future","ind","active","first","singular")[0],
|
||||
conjugate("amō","perfect","ind","active","third","singular")[0])
|
||||
print("-- sum:", [conjugate("sum","present","ind","active",p,"singular")[0] for p in ("first","second","third")])
|
||||
print("-- participle amō pfv acc.f.sg:", participle("amō","pfv","acc","f","singular")[0])
|
||||
print("-- infinitive amō:", infinitive("amō")[0], "| regō pass:", infinitive("regō", voice="passive")[0])
|
||||
@@ -0,0 +1,538 @@
|
||||
"""morphology_pt_full.py — production-grade Brazilian-Portuguese morphological generator.
|
||||
|
||||
NOT a toy. Backed by two real, broad, Wiktionary-lineage lexicons:
|
||||
|
||||
VERBS — UniMorph Portuguese (github.com/unimorph/por, CC-BY-SA 3.0)
|
||||
4,001 verb lemmas × full paradigm (283,991 finite/non-finite forms +
|
||||
20,005 participle forms). Every mood/tense pt actually inflects:
|
||||
indicative present / preterite (PST;PFV) / imperfect (PST;IPFV) /
|
||||
pluperfect-simple (PST;PRF) / future,
|
||||
conditional (futuro do pretérito),
|
||||
subjunctive present / imperfect / FUTURE (PT-specific live tense),
|
||||
affirmative + negative imperative,
|
||||
PERSONAL infinitive (V;{p};{n};NFIN — a PT-specific finite-ish form),
|
||||
past participle (4 gender/number forms) + gerúndio (V.PTCP;PRS).
|
||||
|
||||
NOUNS + ADJECTIVES — kaikki.org Portuguese (Wiktionary extract, same lineage)
|
||||
81,138 noun lemmas WITH inherent gender + real (often irregular) plural —
|
||||
so -ão→-ões / -ãos / -ães / -õos is resolved PER LEMMA by Wiktionary,
|
||||
never guessed (mão→mãos, pão→pães, coração→corações).
|
||||
40,252 adjective lemmas with real feminine + masc/fem plural forms.
|
||||
|
||||
Fallbacks (degrade, never crash, on out-of-vocabulary input):
|
||||
verbs : rule generator for regular -ar/-er/-ir paradigms
|
||||
nouns : gender heuristic (endings) + rule pluralization (with -ão FLAGGED)
|
||||
adjs : -o/-a gender rule + rule pluralization
|
||||
|
||||
Confidence flag on every form:
|
||||
"lexicon" straight from UniMorph/kaikki (trust: high)
|
||||
"rule" deterministic rule (trust: medium)
|
||||
"fallback" could not inflect; returned lemma (trust: low -> FLAG)
|
||||
|
||||
Public API (used by realizer_pt.py):
|
||||
conjugate(lemma, mood, tense, person, number) -> (form, conf)
|
||||
personal_infinitive(lemma, person, number) -> (form, conf)
|
||||
participle(lemma, gender="m", number="singular") -> (form, conf)
|
||||
gerund(lemma) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"
|
||||
inflect_noun(lemma, number, gender=None) -> (form, conf)
|
||||
inflect_adj(lemma, gender, number) -> (form, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "por.unimorph")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_pt.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "pt_morph_cache.pkl")
|
||||
|
||||
# ── mood/tense pair -> UniMorph feature triple (a in tag; b in tag; c in tag) ────
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): ("IND", "PRS", None),
|
||||
("ind", "preterite"): ("IND", "PST", "PFV"),
|
||||
("ind", "imperfect"): ("IND", "PST", "IPFV"),
|
||||
("ind", "pluperfect"): ("IND", "PST", "PRF"), # simple mais-que-perfeito
|
||||
("ind", "future"): ("IND", "FUT", None),
|
||||
("ind", "conditional"): ("COND", None, None),
|
||||
("sbjv", "present"): ("SBJV", "PRS", None),
|
||||
("sbjv", "imperfect"): ("SBJV", "PST", "IPFV"),
|
||||
("sbjv", "future"): ("SBJV", "FUT", None), # PT-specific
|
||||
("imp", "affirmative"): ("IMP", "POS", None),
|
||||
("imp", "negative"): ("IMP", "NEG", None),
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
# ── build the compact lexicon from UniMorph (verbs) + kaikki (nouns/adjs) ────────
|
||||
def _build_verbs():
|
||||
verbs = {} # (lemma, "mood|tense|person|number") -> form
|
||||
pinf = {} # (lemma, "person|number") -> personal-infinitive form
|
||||
part = {} # lemma -> {("m","SG"): form, ...} past participle
|
||||
ger = {} # lemma -> gerúndio
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
|
||||
if head == "V.PTCP":
|
||||
if "PST" in f: # past participle: falado/falada/falados/faladas
|
||||
g = "m" if "MASC" in f else ("f" if "FEM" in f else "m")
|
||||
num = "SG" if "SG" in f else ("PL" if "PL" in f else "SG")
|
||||
part.setdefault(lemma, {})[(g, num)] = form
|
||||
elif "PRS" in f: # gerúndio: falando
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
|
||||
if head != "V":
|
||||
continue
|
||||
|
||||
# personal / impersonal infinitive
|
||||
if "NFIN" in f:
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person and number:
|
||||
pinf[(lemma, f"{person}|{number}")] = form
|
||||
continue
|
||||
|
||||
# finite forms
|
||||
mt = None
|
||||
for (mood, tense), (a, b, c) in _VERB_KEYMAP.items():
|
||||
if a not in f:
|
||||
continue
|
||||
if b is not None and b not in f:
|
||||
continue
|
||||
if c is not None and c not in f:
|
||||
continue
|
||||
# IND;PST needs exactly PFV|IPFV|PRF — reject if the required one absent
|
||||
mt = (mood, tense)
|
||||
break
|
||||
if mt is None:
|
||||
continue
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
verbs.setdefault((lemma, f"{mt[0]}|{mt[1]}|{person}|{number}"), form)
|
||||
return verbs, pinf, part, ger
|
||||
|
||||
|
||||
def _kaikki_gender(arg):
|
||||
if not arg:
|
||||
return None
|
||||
a = arg.lower()
|
||||
if a.startswith("f"):
|
||||
return "f"
|
||||
if a.startswith("m"):
|
||||
return "m"
|
||||
return None
|
||||
|
||||
|
||||
def _build_nouns_adjs():
|
||||
nouns = {} # lemma -> {"g","SG","PL"}
|
||||
adjs = {} # lemma -> {("m","SG"),("f","SG"),("m","PL"),("f","PL")}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
pos = d.get("pos")
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word: # skip multiword entries
|
||||
continue
|
||||
forms = d.get("forms", []) or []
|
||||
|
||||
if pos == "noun":
|
||||
ht = d.get("head_templates") or []
|
||||
g = None
|
||||
if ht:
|
||||
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
|
||||
if g is None:
|
||||
tags = d.get("tags") or []
|
||||
if "feminine" in tags:
|
||||
g = "f"
|
||||
elif "masculine" in tags:
|
||||
g = "m"
|
||||
pl = None
|
||||
for x in forms:
|
||||
t = x.get("tags") or []
|
||||
if "plural" in t and "alternative" not in t and "obsolete" not in t:
|
||||
pl = x.get("form")
|
||||
break
|
||||
# first entry wins; but a later entry with a plural fills a gap
|
||||
if word not in nouns:
|
||||
nouns[word] = {"g": g, "SG": word, "PL": pl}
|
||||
else:
|
||||
cur = nouns[word]
|
||||
if cur.get("g") is None and g:
|
||||
cur["g"] = g
|
||||
if not cur.get("PL") and pl:
|
||||
cur["PL"] = pl
|
||||
|
||||
elif pos == "adj":
|
||||
d0 = adjs.setdefault(word, {})
|
||||
d0.setdefault(("m", "SG"), word)
|
||||
for x in forms:
|
||||
t = set(x.get("tags") or [])
|
||||
fm = x.get("form")
|
||||
if not fm or ("alternative" in t) or ("obsolete" in t):
|
||||
continue
|
||||
if "comparative" in t or "superlative" in t or \
|
||||
"diminutive" in t or "augmentative" in t:
|
||||
continue
|
||||
if "feminine" in t and "plural" in t:
|
||||
d0[("f", "PL")] = fm
|
||||
elif "masculine" in t and "plural" in t:
|
||||
d0[("m", "PL")] = fm
|
||||
elif "feminine" in t:
|
||||
d0[("f", "SG")] = fm
|
||||
elif "plural" in t: # invariant-gender adj (feliz -> felizes)
|
||||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||||
return nouns, adjs
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs, pinf, part, ger = _build_verbs()
|
||||
nouns, adjs = _build_nouns_adjs()
|
||||
data = {"verbs": verbs, "pinf": pinf, "part": part, "ger": ger,
|
||||
"nouns": nouns, "adjs": adjs}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
newest_src = max(os.path.getmtime(_UNIMORPH),
|
||||
os.path.getmtime(_KAIKKI) if os.path.exists(_KAIKKI) else 0)
|
||||
if os.path.getmtime(_CACHE) >= newest_src:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _PINF, _PART, _GER, _NOUNS, _ADJS = (
|
||||
_LEX["verbs"], _LEX["pinf"], _LEX["part"], _LEX["ger"],
|
||||
_LEX["nouns"], _LEX["adjs"])
|
||||
|
||||
|
||||
# ── regular-ending rule fallback (deterministic, last resort) ────────────────────
|
||||
def _vclass(lemma):
|
||||
return lemma[-2:] if lemma[-2:] in ("ar", "er", "ir") else None
|
||||
|
||||
|
||||
def _stem(lemma):
|
||||
return lemma[:-2]
|
||||
|
||||
|
||||
# endings indexed [1sg,2sg,3sg,1pl,2pl,3pl]
|
||||
_REG = {
|
||||
("ind", "present", "ar"): ["o", "as", "a", "amos", "ais", "am"],
|
||||
("ind", "present", "er"): ["o", "es", "e", "emos", "eis", "em"],
|
||||
("ind", "present", "ir"): ["o", "es", "e", "imos", "is", "em"],
|
||||
("ind", "preterite", "ar"): ["ei", "aste", "ou", "amos", "astes", "aram"],
|
||||
("ind", "preterite", "er"): ["i", "este", "eu", "emos", "estes", "eram"],
|
||||
("ind", "preterite", "ir"): ["i", "iste", "iu", "imos", "istes", "iram"],
|
||||
("ind", "imperfect", "ar"): ["ava", "avas", "ava", "ávamos", "áveis", "avam"],
|
||||
("ind", "imperfect", "er"): ["ia", "ias", "ia", "íamos", "íeis", "iam"],
|
||||
("ind", "imperfect", "ir"): ["ia", "ias", "ia", "íamos", "íeis", "iam"],
|
||||
("sbjv", "present", "ar"): ["e", "es", "e", "emos", "eis", "em"],
|
||||
("sbjv", "present", "er"): ["a", "as", "a", "amos", "ais", "am"],
|
||||
("sbjv", "present", "ir"): ["a", "as", "a", "amos", "ais", "am"],
|
||||
("sbjv", "imperfect", "ar"): ["asse", "asses", "asse", "ássemos", "ásseis", "assem"],
|
||||
("sbjv", "imperfect", "er"): ["esse", "esses", "esse", "êssemos", "êsseis", "essem"],
|
||||
("sbjv", "imperfect", "ir"): ["isse", "isses", "isse", "íssemos", "ísseis", "issem"],
|
||||
("sbjv", "future", "ar"): ["ar", "ares", "ar", "armos", "ardes", "arem"],
|
||||
("sbjv", "future", "er"): ["er", "eres", "er", "ermos", "erdes", "erem"],
|
||||
("sbjv", "future", "ir"): ["ir", "ires", "ir", "irmos", "irdes", "irem"],
|
||||
}
|
||||
# future & conditional attach to the FULL infinitive
|
||||
_FUT = ["ei", "ás", "á", "emos", "eis", "ão"]
|
||||
_COND = ["ia", "ias", "ia", "íamos", "íeis", "iam"]
|
||||
|
||||
|
||||
def _slot_idx(person, number):
|
||||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||||
return base + (0 if number == "singular" else 3)
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
vc = _vclass(lemma)
|
||||
if vc is None:
|
||||
return None
|
||||
st, i = _stem(lemma), _slot_idx(person, number)
|
||||
if mood == "ind" and tense == "future":
|
||||
return lemma + _FUT[i]
|
||||
if mood == "ind" and tense == "conditional":
|
||||
return lemma + _COND[i]
|
||||
if mood == "imp": # affirmative tú/vocês imperative ~ subjunctive present
|
||||
table = _REG.get(("sbjv", "present", vc))
|
||||
if table and tense == "negative":
|
||||
return st + table[i]
|
||||
# affirmative 2sg = 3sg present indicative; others = subjunctive
|
||||
pres = _REG.get(("ind", "present", vc))
|
||||
if person == "second" and number == "singular":
|
||||
return st + pres[2]
|
||||
return st + table[i] if table else None
|
||||
table = _REG.get((mood, tense, vc))
|
||||
if table:
|
||||
return st + table[i]
|
||||
return None
|
||||
|
||||
|
||||
# verified corrections to UniMorph data errors (each audited individually, not
|
||||
# guessed). The three 1PL-present entries are glued-allomorph errors surfaced by a
|
||||
# full-lexicon scan for a non-final "mos" in V;1;PL;IND;PRS forms (the ONLY three).
|
||||
_VERB_FIX = {
|
||||
("estar", "ind", "imperfect", "third", "plural"): "estavam", # was "estávam"
|
||||
("estar", "ind", "present", "first", "plural"): "estamos", # was "estamosestámos"
|
||||
("haver", "ind", "present", "first", "plural"): "havemos", # was "havemoshemos"
|
||||
("ir", "ind", "present", "first", "plural"): "vamos", # was "vamosimos"
|
||||
}
|
||||
|
||||
|
||||
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number):
|
||||
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
|
||||
lemma = lemma.strip().lower()
|
||||
fix = _VERB_FIX.get((lemma, mood, tense, person, number))
|
||||
if fix:
|
||||
return fix, "lexicon"
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
if p and n:
|
||||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
|
||||
if form:
|
||||
# pt-BR normalization: UniMorph `por` carries the EUROPEAN spelling of
|
||||
# the -ar 1pl PRETERITE (-ámos). Brazilian PT drops the accent
|
||||
# (falámos->falamos, chegámos->chegamos) — 3,334/4,001 verbs affected.
|
||||
if (mood == "ind" and tense == "preterite" and person == "first"
|
||||
and number == "plural" and form.endswith("ámos")):
|
||||
form = form[:-4] + "amos"
|
||||
return form, "lexicon"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def personal_infinitive(lemma, person, number):
|
||||
"""PT personal (inflected) infinitive: para falarmos, ao chegarem."""
|
||||
lemma = lemma.strip().lower()
|
||||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||||
if p and n:
|
||||
form = _PINF.get((lemma, f"{p}|{n}"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# rule: infinitive + personal endings (-, -es, -, -mos, -des, -em)
|
||||
end = {("first", "singular"): "", ("second", "singular"): "es",
|
||||
("third", "singular"): "", ("first", "plural"): "mos",
|
||||
("second", "plural"): "des", ("third", "plural"): "em"}.get((person, number), "")
|
||||
return lemma + end, "rule"
|
||||
|
||||
|
||||
# ── PUBLIC: participle + gerund ───────────────────────────────────────────────────
|
||||
def participle(lemma, gender="m", number="singular"):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
d = _PART.get(lemma)
|
||||
if d:
|
||||
form = d.get((g, num)) or d.get(("m", "SG"))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
if lemma.endswith("ar"):
|
||||
base = lemma[:-2] + "ad"
|
||||
elif lemma[-2:] in ("er", "ir"):
|
||||
base = lemma[:-2] + "id"
|
||||
else:
|
||||
return lemma, "fallback"
|
||||
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "os", "f|PL": "as"}[f"{g}|{num}"]
|
||||
return base + suf, "rule"
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
if lemma.endswith("ar"):
|
||||
return lemma[:-2] + "ando", "rule"
|
||||
if lemma.endswith("er"):
|
||||
return lemma[:-2] + "endo", "rule"
|
||||
if lemma.endswith("ir"):
|
||||
return lemma[:-2] + "indo", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
|
||||
_FEM_SUF = ("ção", "são", "ção", "dade", "tade", "agem", "igem", "ugem", "gem",
|
||||
"ez", "eza", "ice", "ície", "tude", "ude", "âncbefore")
|
||||
_FEM_SUF = ("ção", "são", "dade", "tade", "agem", "gem", "eza", "ez", "ice",
|
||||
"tude", "ude", "ância", "ência", "ínia")
|
||||
_MASC_SUF = ("ema", "oma", "ama", "grama", "eta", "ão") # Greek -ma etc. (mostly m)
|
||||
|
||||
|
||||
def _gender_heuristic(noun):
|
||||
for suf in _FEM_SUF:
|
||||
if noun.endswith(suf):
|
||||
return "f"
|
||||
if noun.endswith(("ema", "oma", "ama")): # problema, idioma, programa
|
||||
return "m"
|
||||
if noun.endswith("a") or noun.endswith("ã"):
|
||||
return "f"
|
||||
if noun.endswith("o") or noun.endswith(("l", "r", "z", "m", "u", "i")):
|
||||
return "m"
|
||||
return "m"
|
||||
|
||||
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g"):
|
||||
return d["g"]
|
||||
return _gender_heuristic(lemma)
|
||||
|
||||
|
||||
_INVARIANT_PL_SUF = ("s",) # paroxytones ending -s are invariant (o lápis / os lápis)
|
||||
|
||||
|
||||
def _rule_plural(noun):
|
||||
"""Deterministic PT pluralization. Returns (form, ok) where ok=False flags an
|
||||
ambiguous -ão that should lower confidence (the lexicon normally resolves it)."""
|
||||
if not noun:
|
||||
return noun, True
|
||||
if noun.endswith("ão"):
|
||||
return noun[:-2] + "ões", False # majority rule, but AMBIGUOUS -> flag
|
||||
if noun.endswith("m"):
|
||||
return noun[:-1] + "ns", True # homem->homens, jardim->jardins
|
||||
if noun.endswith("al"):
|
||||
return noun[:-2] + "ais", True
|
||||
if noun.endswith("el"):
|
||||
return noun[:-2] + "éis", True
|
||||
if noun.endswith("ol"):
|
||||
return noun[:-2] + "óis", True
|
||||
if noun.endswith("ul"):
|
||||
return noun[:-2] + "uis", True
|
||||
if noun.endswith("il"):
|
||||
return noun[:-2] + "is", True # stressed (funil->funis); unstressed rarer
|
||||
if noun.endswith(("r", "z")):
|
||||
return noun + "es", True # flor->flores, luz->luzes
|
||||
if noun.endswith("s"):
|
||||
# paroxytone -s (lápis, ônibus) invariant; oxytone -s (país) -> -es
|
||||
return noun, True
|
||||
if noun.endswith(("a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "ã")):
|
||||
return noun + "s", True
|
||||
return noun + "s", True
|
||||
|
||||
|
||||
def inflect_noun(lemma, number, gender=None):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if number == "singular":
|
||||
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
|
||||
if d and d.get("PL"):
|
||||
return d["PL"], "lexicon"
|
||||
form, ok = _rule_plural(lemma)
|
||||
return form, ("rule" if ok else "fallback")
|
||||
|
||||
|
||||
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
|
||||
def inflect_adj(lemma, gender, number):
|
||||
lemma = lemma.strip().lower()
|
||||
g = "f" if gender == "f" else "m"
|
||||
num = "SG" if number == "singular" else "PL"
|
||||
d = _ADJS.get(lemma)
|
||||
if d:
|
||||
form = d.get((g, num))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# build a missing plural from this gender's singular
|
||||
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
|
||||
if num == "PL":
|
||||
pl, ok = _rule_plural(sg)
|
||||
return pl, ("rule" if ok else "fallback")
|
||||
return sg, "lexicon"
|
||||
# rule fallback: -o/-a gender, then pluralize
|
||||
a = lemma
|
||||
if g == "f":
|
||||
if a.endswith("o"):
|
||||
a = a[:-1] + "a"
|
||||
elif a.endswith(("ês", "or")) and not a.endswith("ior"):
|
||||
a = a + "a" # português->portuguesa, trabalhador->..a
|
||||
if num == "PL":
|
||||
a, ok = _rule_plural(a)
|
||||
return a, ("rule" if ok else "fallback")
|
||||
return a, "rule"
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"verb_source": "UniMorph Portuguese (github.com/unimorph/por)",
|
||||
"noun_adj_source": "kaikki.org Portuguese (Wiktionary extract)",
|
||||
"license": "CC-BY-SA (Wiktionary-derived)",
|
||||
"verb_forms": len(_VERBS),
|
||||
"verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"personal_infinitive_forms": len(_PINF),
|
||||
"participle_lemmas": len(_PART),
|
||||
"gerund_lemmas": len(_GER),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
tests = [
|
||||
("falar", "ind", "present", "first", "singular", "falo"),
|
||||
("comer", "ind", "present", "third", "plural", "comem"),
|
||||
("partir", "ind", "present", "first", "plural", "partimos"),
|
||||
("ser", "ind", "present", "third", "singular", "é"),
|
||||
("ir", "ind", "preterite", "first", "singular", "fui"),
|
||||
("ter", "ind", "future", "first", "singular", "terei"),
|
||||
("fazer", "sbjv", "present", "first", "singular", "faça"),
|
||||
("dormir", "ind", "present", "first", "singular", "durmo"),
|
||||
("dar", "ind", "preterite", "third", "singular", "deu"),
|
||||
("poder", "ind", "conditional", "first", "singular", "poderia"),
|
||||
("fazer", "sbjv", "future", "third", "singular", "fizer"),
|
||||
("estar", "ind", "present", "third", "singular", "está"),
|
||||
]
|
||||
ok = 0
|
||||
for lemma, mood, tense, per, num, exp in tests:
|
||||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||||
flag = "OK " if got == exp else "XX "
|
||||
ok += got == exp
|
||||
print(f" {flag}{lemma:8} {mood}/{tense} {per[:3]}.{num[:2]} -> {got:14} ({conf}) exp={exp}")
|
||||
print(f"verb tests {ok}/{len(tests)}")
|
||||
print(" gender: casa=", noun_gender("casa"), "problema=", noun_gender("problema"),
|
||||
"mão=", noun_gender("mão"), "coração=", noun_gender("coração"),
|
||||
"flor=", noun_gender("flor"))
|
||||
print(" plural: mão->", inflect_noun("mão", "plural"),
|
||||
"| pão->", inflect_noun("pão", "plural"),
|
||||
"| animal->", inflect_noun("animal", "plural"),
|
||||
"| coração->", inflect_noun("coração", "plural"))
|
||||
print(" adj: bonito/f/sg->", inflect_adj("bonito", "f", "singular"),
|
||||
"| feliz/m/pl->", inflect_adj("feliz", "m", "plural"),
|
||||
"| português/f/sg->", inflect_adj("português", "f", "singular"))
|
||||
print(" part: fazer/m/sg->", participle("fazer"), "| ger falar->", gerund("falar"))
|
||||
print(" pinf falar 1pl->", personal_infinitive("falar", "first", "plural"))
|
||||
@@ -0,0 +1,609 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""morphology_ro_full.py — production-grade Romanian morphological generator.
|
||||
|
||||
Romanian is the BIG typological delta of the Romance family. The verb engine and
|
||||
the confidence/fallback contract TRANSFER from the Italian sibling; the NOMINAL
|
||||
system is genuinely new: Romanian has a SUFFIXED definite article, a preserved
|
||||
NOM/ACC vs GEN/DAT case distinction, a NEUTER gender (masc-agreeing in SG,
|
||||
fem-agreeing in PL), and a VOCATIVE. Those are grounded in real per-lemma data,
|
||||
not guessed.
|
||||
|
||||
Real, Wiktionary-lineage lexical sources:
|
||||
|
||||
VERBS — UniMorph Romanian (github.com/unimorph/ron, CC-BY-SA 3.0)
|
||||
~1216 verb lemmas × paradigm, CLEAN orthography:
|
||||
indicativ prezent / imperfect (PST;IPFV) / perfectul simplu (PST;PFV) /
|
||||
conjunctiv prezent (SBJV;PRS, stored WITHOUT the 'să' particle),
|
||||
participiu (V.PTCP;PST, INVARIABLE in the perfect compus),
|
||||
gerunziu (V.CVB;PRS), infinitiv (NFIN), imperativ.
|
||||
ro_irreg_verbs (embedded) — high-frequency verbs UniMorph MISSES
|
||||
(avea, vrea, da) + the auxiliary clitic paradigms the compound tenses need
|
||||
(perfect-compus am/ai/a/am/ați/au, viitor voi/vei/va/vom/veți/vor,
|
||||
condițional aș/ai/ar/am/ați/ar). Real standard forms.
|
||||
|
||||
NOUNS — kaikki.org Romanian (Wiktionary extract, CC-BY-SA 3.0)
|
||||
the FULL declension per lemma, cleanly tagged:
|
||||
(nom/acc | gen/dat | vocative) × (indefinite | definite) × (sg | pl).
|
||||
This is what makes the suffixed article LEXICALLY grounded (om→omul,
|
||||
casă→casa, băiat→băiatul, casei gen/dat, omule vocative). Inherent gender
|
||||
m / f / n (NEUTER available directly) from the head template.
|
||||
|
||||
ADJECTIVES — UniMorph Romanian ADJ
|
||||
full case × gender(MASC/FEM/NEUT) × number × definiteness paradigm.
|
||||
|
||||
Fallbacks (degrade, never crash, on OOV): rule verb conjugation for -a/-ea/-e/-i/-î
|
||||
classes, rule pluralization, rule suffixed-article by gender+ending. Every form
|
||||
carries a confidence flag: "lexicon" | "rule" | "fallback".
|
||||
|
||||
Public API (used by realizer_ro.py):
|
||||
conjugate(lemma, mood, tense, person, number) -> (form, conf)
|
||||
aux(kind, person, number) -> str # perfect / future / conditional clitics
|
||||
participle(lemma) -> (form, conf) # INVARIABLE
|
||||
gerund(lemma) -> (form, conf)
|
||||
noun_gender(lemma) -> "m"|"f"|"n"
|
||||
definite_suffix(noun, gender, number, case) -> (form, conf) # rule engine
|
||||
inflect_noun(lemma, number, gender=None, case="nomacc", definite=False) -> (form, conf)
|
||||
inflect_adj(lemma, gender, number, case="nomacc", definite=False) -> (form, conf)
|
||||
lexicon_stats() -> dict
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_UNIMORPH = os.path.join(_HERE, "data", "ron.unimorph")
|
||||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_ro.jsonl")
|
||||
_CACHE = os.path.join(_HERE, "data", "ro_morph_cache.pkl")
|
||||
|
||||
# ── (mood, tense) -> UniMorph feature set ─────────────────────────────────────────
|
||||
_VERB_KEYMAP = {
|
||||
("ind", "present"): {"IND", "PRS"},
|
||||
("ind", "imperfect"): {"IND", "PST", "IPFV"},
|
||||
("ind", "perfect_s"): {"IND", "PST", "PFV"}, # perfectul simplu (regional/lit.)
|
||||
("sbjv", "present"): {"SBJV", "PRS"},
|
||||
("imp", "affirmative"): {"POS", "IMP"},
|
||||
}
|
||||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||||
|
||||
|
||||
def _feat_set(tag):
|
||||
return set(tag.split(";"))
|
||||
|
||||
|
||||
# ── high-frequency irregulars UniMorph misses + auxiliary clitic paradigms ────────
|
||||
# Real standard Romanian forms (textbook paradigms).
|
||||
_IRREG = {
|
||||
"avea": {
|
||||
"ind|present|1|SG": "am", "ind|present|2|SG": "ai", "ind|present|3|SG": "are",
|
||||
"ind|present|1|PL": "avem", "ind|present|2|PL": "aveți", "ind|present|3|PL": "au",
|
||||
"ind|imperfect|1|SG": "aveam", "ind|imperfect|2|SG": "aveai",
|
||||
"ind|imperfect|3|SG": "avea", "ind|imperfect|1|PL": "aveam",
|
||||
"ind|imperfect|2|PL": "aveați", "ind|imperfect|3|PL": "aveau",
|
||||
"sbjv|present|3|SG": "aibă", "sbjv|present|3|PL": "aibă",
|
||||
"sbjv|present|1|SG": "am", "sbjv|present|2|SG": "ai",
|
||||
"sbjv|present|1|PL": "avem", "sbjv|present|2|PL": "aveți",
|
||||
"part": "avut", "ger": "având",
|
||||
},
|
||||
"vrea": {
|
||||
"ind|present|1|SG": "vreau", "ind|present|2|SG": "vrei", "ind|present|3|SG": "vrea",
|
||||
"ind|present|1|PL": "vrem", "ind|present|2|PL": "vreți", "ind|present|3|PL": "vor",
|
||||
"ind|imperfect|1|SG": "voiam", "ind|imperfect|3|SG": "voia",
|
||||
"sbjv|present|3|SG": "vrea", "sbjv|present|3|PL": "vrea",
|
||||
"part": "vrut", "ger": "vrând",
|
||||
},
|
||||
"da": {
|
||||
"ind|present|1|SG": "dau", "ind|present|2|SG": "dai", "ind|present|3|SG": "dă",
|
||||
"ind|present|1|PL": "dăm", "ind|present|2|PL": "dați", "ind|present|3|PL": "dau",
|
||||
"ind|imperfect|1|SG": "dădeam", "ind|imperfect|3|SG": "dădea",
|
||||
"sbjv|present|3|SG": "dea", "sbjv|present|3|PL": "dea",
|
||||
"part": "dat", "ger": "dând",
|
||||
},
|
||||
"fi": { # a fi — present is in UniMorph but keep participle + subjunctive here
|
||||
"part": "fost", "ger": "fiind",
|
||||
"sbjv|present|1|SG": "fiu", "sbjv|present|2|SG": "fii", "sbjv|present|3|SG": "fie",
|
||||
"sbjv|present|1|PL": "fim", "sbjv|present|2|PL": "fiți", "sbjv|present|3|PL": "fie",
|
||||
"ind|imperfect|1|SG": "eram", "ind|imperfect|2|SG": "erai",
|
||||
"ind|imperfect|3|SG": "era", "ind|imperfect|1|PL": "eram",
|
||||
"ind|imperfect|2|PL": "erați", "ind|imperfect|3|PL": "erau",
|
||||
},
|
||||
}
|
||||
# auxiliary clitic paradigms (person,number)->form
|
||||
_AUX = {
|
||||
"perfect": {("first", "singular"): "am", ("second", "singular"): "ai",
|
||||
("third", "singular"): "a", ("first", "plural"): "am",
|
||||
("second", "plural"): "ați", ("third", "plural"): "au"},
|
||||
"future": {("first", "singular"): "voi", ("second", "singular"): "vei",
|
||||
("third", "singular"): "va", ("first", "plural"): "vom",
|
||||
("second", "plural"): "veți", ("third", "plural"): "vor"},
|
||||
"conditional": {("first", "singular"): "aș", ("second", "singular"): "ai",
|
||||
("third", "singular"): "ar", ("first", "plural"): "am",
|
||||
("second", "plural"): "ați", ("third", "plural"): "ar"},
|
||||
}
|
||||
|
||||
|
||||
def aux(kind, person, number):
|
||||
return _AUX[kind][(person, number)]
|
||||
|
||||
|
||||
# ── build verb lexicon from UniMorph ──────────────────────────────────────────────
|
||||
def _build_verbs():
|
||||
verbs, part, ger = {}, {}, {}
|
||||
with open(_UNIMORPH, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line or "\t" not in line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
lemma, form, tag = parts
|
||||
f = _feat_set(tag)
|
||||
head = tag.split(";")[0]
|
||||
if head == "V.PTCP":
|
||||
if "PST" in f:
|
||||
part.setdefault(lemma, form)
|
||||
continue
|
||||
if head == "V.CVB":
|
||||
if "PRS" in f:
|
||||
ger.setdefault(lemma, form)
|
||||
continue
|
||||
if head != "V":
|
||||
continue
|
||||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||||
if person is None or number is None:
|
||||
continue
|
||||
# conjunctiv forms in UniMorph carry a leading 'să ' — strip it
|
||||
surf = form
|
||||
if surf.startswith("să "):
|
||||
surf = surf[3:]
|
||||
for (mood, tense), req in _VERB_KEYMAP.items():
|
||||
if not req <= f:
|
||||
continue
|
||||
if tense == "imperfect" and "PFV" in f:
|
||||
continue
|
||||
if tense == "perfect_s" and "IPFV" in f:
|
||||
continue
|
||||
# keep IND;PRS out of the PRF slot (mai-mult-ca-perfect etc. ignored)
|
||||
if {"IND", "PRS"} <= req and "PRF" in f:
|
||||
continue
|
||||
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), surf)
|
||||
break
|
||||
return verbs, part, ger
|
||||
|
||||
|
||||
# ── kaikki nouns: full declension paradigm per lemma ──────────────────────────────
|
||||
_EXCL = {"alternative", "archaic", "obsolete", "regional", "dialectal", "rare",
|
||||
"table-tags", "inflection-template", "error-unrecognized-form",
|
||||
"diminutive", "augmentative", "informal"}
|
||||
|
||||
|
||||
def _noun_key(tagset):
|
||||
if tagset & _EXCL:
|
||||
return None
|
||||
if "vocative" in tagset:
|
||||
case = "voc"
|
||||
elif "genitive" in tagset or "dative" in tagset:
|
||||
case = "gendat"
|
||||
elif "nominative" in tagset or "accusative" in tagset:
|
||||
case = "nomacc"
|
||||
else:
|
||||
return None
|
||||
definite = "definite" in tagset and "indefinite" not in tagset
|
||||
number = "PL" if "plural" in tagset else ("SG" if "singular" in tagset else None)
|
||||
if number is None:
|
||||
return None
|
||||
return (case, definite, number)
|
||||
|
||||
|
||||
def _build_nouns():
|
||||
nouns = {} # lemma -> {"g":..., para:{(case,def,num):form}, "PL":plain_plural}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("pos") != "noun":
|
||||
continue
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word:
|
||||
continue
|
||||
ht = d.get("head_templates") or []
|
||||
g = None
|
||||
if ht:
|
||||
a = str((ht[0].get("args") or {}).get("1") or "").lower()
|
||||
if a[:1] in ("m", "f", "n"):
|
||||
g = a[:1]
|
||||
entry = nouns.setdefault(word, {"g": g, "para": {}, "PL": None})
|
||||
if entry["g"] is None and g:
|
||||
entry["g"] = g
|
||||
for x in (d.get("forms") or []):
|
||||
fm = x.get("form")
|
||||
tg = set(x.get("tags") or [])
|
||||
if not fm or fm in ("-", "#", "") or " " in fm:
|
||||
continue
|
||||
if tg == {"plural"} and not entry["PL"]:
|
||||
entry["PL"] = fm
|
||||
k = _noun_key(tg)
|
||||
if k and k not in entry["para"]:
|
||||
entry["para"][k] = fm
|
||||
return nouns
|
||||
|
||||
|
||||
# ── adjectives from kaikki (UniMorph ron ADJ is sparse AND mis-tagged; kaikki is
|
||||
# clean: the 4-form agreement pattern bun/bună/buni/bune). Neuter maps sg->masc,
|
||||
# pl->fem, so 4 forms (m/f × SG/PL) fully cover it. ────────────────────────────
|
||||
def _build_adjs():
|
||||
adjs = {} # lemma -> {(gender,number): form} gender in {m,f}
|
||||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("pos") != "adj":
|
||||
continue
|
||||
word = d.get("word", "")
|
||||
if not word or " " in word:
|
||||
continue
|
||||
d0 = adjs.setdefault(word, {})
|
||||
d0.setdefault(("m", "SG"), word) # masc sg = headword
|
||||
for x in (d.get("forms") or []):
|
||||
fm = x.get("form")
|
||||
t = set(x.get("tags") or [])
|
||||
if not fm or " " in fm or fm in ("-", "#") or (t & _EXCL):
|
||||
continue
|
||||
if "definite" in t or "genitive" in t or "dative" in t:
|
||||
continue # keep indefinite nom/acc agr set
|
||||
pl = "plural" in t
|
||||
fem = "feminine" in t
|
||||
masc = "masculine" in t
|
||||
if fem and pl:
|
||||
d0.setdefault(("f", "PL"), fm)
|
||||
elif masc and pl:
|
||||
d0.setdefault(("m", "PL"), fm)
|
||||
elif fem and not pl:
|
||||
d0.setdefault(("f", "SG"), fm)
|
||||
elif pl and not fem and not masc: # bare plural -> both genders
|
||||
d0.setdefault(("m", "PL"), fm)
|
||||
d0.setdefault(("f", "PL"), fm)
|
||||
return adjs
|
||||
|
||||
|
||||
def _build_cache():
|
||||
verbs, part, ger = _build_verbs()
|
||||
nouns = _build_nouns()
|
||||
adjs = _build_adjs()
|
||||
data = {"verbs": verbs, "part": part, "ger": ger, "nouns": nouns, "adjs": adjs}
|
||||
try:
|
||||
with open(_CACHE, "wb") as fh:
|
||||
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _load():
|
||||
if os.path.exists(_CACHE):
|
||||
srcs = [_UNIMORPH, _KAIKKI]
|
||||
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
|
||||
if os.path.getmtime(_CACHE) >= newest:
|
||||
try:
|
||||
with open(_CACHE, "rb") as fh:
|
||||
return pickle.load(fh)
|
||||
except Exception:
|
||||
pass
|
||||
return _build_cache()
|
||||
|
||||
|
||||
_LEX = _load()
|
||||
_VERBS, _PART, _GER, _NOUNS, _ADJS = (
|
||||
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"])
|
||||
|
||||
|
||||
# ── rule verb conjugation fallback ────────────────────────────────────────────────
|
||||
def _vclass(lemma):
|
||||
if lemma.endswith("a"):
|
||||
return "a"
|
||||
if lemma.endswith("ea"):
|
||||
return "ea"
|
||||
if lemma.endswith("e"):
|
||||
return "e"
|
||||
if lemma.endswith("i"):
|
||||
return "i"
|
||||
if lemma.endswith("î"):
|
||||
return "î"
|
||||
return None
|
||||
|
||||
|
||||
# regular present endings by class [1sg,2sg,3sg,1pl,2pl,3pl]
|
||||
_REG_PRS = {
|
||||
"a": ["", "i", "ă", "ăm", "ați", "ă"], # a lucra type (simplified)
|
||||
"ea": ["", "i", "e", "em", "eți", "", ],
|
||||
"e": ["", "i", "e", "em", "eți", ""],
|
||||
"i": ["esc", "ești", "ește", "im", "iți", "esc"], # -i type (a vorbi)
|
||||
"î": ["ăsc", "ăști", "ăște", "âm", "âți", "ăsc"],
|
||||
}
|
||||
_SLOT = {("first", "singular"): 0, ("second", "singular"): 1, ("third", "singular"): 2,
|
||||
("first", "plural"): 3, ("second", "plural"): 4, ("third", "plural"): 5}
|
||||
|
||||
|
||||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||||
vc = _vclass(lemma)
|
||||
if vc is None:
|
||||
return None
|
||||
i = _SLOT[(person, number)]
|
||||
body = lemma[:-len(vc)]
|
||||
if mood == "ind" and tense == "present":
|
||||
end = _REG_PRS[vc][i]
|
||||
return body + end
|
||||
if mood == "ind" and tense == "imperfect":
|
||||
# -a/-i/-î -> stem + a/eai...; -e/-ea -> eam. Simplified regular imperfect.
|
||||
stem = body
|
||||
endings = {"a": ["am", "ai", "a", "am", "ați", "au"],
|
||||
"i": ["eam", "eai", "ea", "eam", "eați", "eau"],
|
||||
"î": ["am", "ai", "a", "am", "ați", "au"],
|
||||
"e": ["eam", "eai", "ea", "eam", "eați", "eau"],
|
||||
"ea": ["eam", "eai", "ea", "eam", "eați", "eau"]}[vc]
|
||||
return stem + endings[i]
|
||||
return None
|
||||
|
||||
|
||||
# ── PUBLIC verb API ───────────────────────────────────────────────────────────────
|
||||
def conjugate(lemma, mood, tense, person, number):
|
||||
lemma = lemma.strip().lower()
|
||||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{_NUMBER.get(number,'?')}"
|
||||
ir = _IRREG.get(lemma)
|
||||
if ir and key in ir:
|
||||
return ir[key], "lexicon"
|
||||
form = _VERBS.get((lemma, key))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||||
if r is not None:
|
||||
return r, "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def participle(lemma):
|
||||
"""Past participle — INVARIABLE in the perfect compus (am mers, am văzut)."""
|
||||
lemma = lemma.strip().lower()
|
||||
ir = _IRREG.get(lemma)
|
||||
if ir and "part" in ir:
|
||||
return ir["part"], "lexicon"
|
||||
if lemma in _PART:
|
||||
return _PART[lemma], "lexicon"
|
||||
vc = _vclass(lemma)
|
||||
if vc == "a":
|
||||
return lemma[:-1] + "at", "rule"
|
||||
if vc in ("ea",):
|
||||
return lemma[:-2] + "ut", "rule"
|
||||
if vc == "i":
|
||||
return lemma[:-1] + "it", "rule"
|
||||
if vc == "î":
|
||||
return lemma[:-1] + "ât", "rule"
|
||||
if vc == "e":
|
||||
return lemma[:-1] + "ut", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
def gerund(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
ir = _IRREG.get(lemma)
|
||||
if ir and "ger" in ir:
|
||||
return ir["ger"], "lexicon"
|
||||
if lemma in _GER:
|
||||
return _GER[lemma], "lexicon"
|
||||
vc = _vclass(lemma)
|
||||
if vc in ("a", "î"):
|
||||
return lemma[:-1] + "ând", "rule"
|
||||
if vc in ("ea", "e", "i"):
|
||||
return lemma[:-len(vc)] + "ind", "rule"
|
||||
return lemma, "fallback"
|
||||
|
||||
|
||||
# ── noun gender ───────────────────────────────────────────────────────────────────
|
||||
def noun_gender(lemma):
|
||||
lemma = lemma.strip().lower()
|
||||
d = _NOUNS.get(lemma)
|
||||
if d and d.get("g") in ("m", "f", "n"):
|
||||
return d["g"]
|
||||
if lemma.endswith(("ă", "a", "e")):
|
||||
return "f"
|
||||
return "m"
|
||||
|
||||
|
||||
# ── SUFFIXED DEFINITE ARTICLE — rule engine (fallback for OOV nouns) ───────────────
|
||||
def definite_suffix(noun, gender, number, case="nomacc"):
|
||||
"""Attach the enclitic definite article by gender + ending. Returns (form, conf).
|
||||
This is the headline Romanian-specific engine extension."""
|
||||
n = noun
|
||||
g = gender
|
||||
if number == "singular":
|
||||
if g in ("m", "n"):
|
||||
if case == "gendat":
|
||||
# masc/neut gen-dat definite: -lui
|
||||
if n.endswith("e"):
|
||||
return n + "lui", "rule" # câine -> câinelui
|
||||
if n.endswith("u"):
|
||||
return n + "lui", "rule"
|
||||
return n + "ului", "rule" # om -> omului
|
||||
# nom/acc
|
||||
if n.endswith("e"):
|
||||
return n + "le", "rule" # câine -> câinele
|
||||
if n.endswith("u"):
|
||||
return n + "l", "rule" # codru -> codrul
|
||||
if n.endswith("i"):
|
||||
return n + "ul", "rule"
|
||||
return n + "ul", "rule" # om -> omul
|
||||
# feminine singular
|
||||
if case == "gendat":
|
||||
# fem gen/dat definite = plural-stem + i (casei, fetei) — needs plural;
|
||||
# approximated as: -ă->-ei, -e->-ei, -a->-alei
|
||||
if n.endswith("ă"):
|
||||
return n[:-1] + "ei", "rule" # casă -> casei
|
||||
if n.endswith("e"):
|
||||
return n[:-1] + "ei", "rule" # carte -> cărții(approx cartei)
|
||||
if n.endswith("a"):
|
||||
return n[:-1] + "lei", "rule"
|
||||
return n + "i", "rule"
|
||||
# fem nom/acc
|
||||
if n.endswith("ă"):
|
||||
return n[:-1] + "a", "rule" # casă -> casa
|
||||
if n.endswith("e"):
|
||||
return n[:-1] + "ea", "rule" # carte -> cartea
|
||||
if n.endswith("a"):
|
||||
return n + "ua", "rule" # stea -> steaua
|
||||
if n.endswith("i"):
|
||||
return n + "a", "rule"
|
||||
return n + "a", "rule"
|
||||
# plural
|
||||
if case == "gendat":
|
||||
base = noun
|
||||
return base + "lor", "rule" # -lor for all gen/dat pl
|
||||
if g == "m":
|
||||
return noun + "i", "rule" # oameni -> oamenii (+i)
|
||||
return noun + "le", "rule" # case -> casele, trenuri->trenurile
|
||||
|
||||
|
||||
# ── rule pluralization (fallback) ─────────────────────────────────────────────────
|
||||
def _rule_plural(noun, gender):
|
||||
if gender == "f":
|
||||
if noun.endswith("ă"):
|
||||
return noun[:-1] + "e"
|
||||
if noun.endswith("e"):
|
||||
return noun[:-1] + "i"
|
||||
if noun.endswith("a"):
|
||||
return noun[:-1] + "le"
|
||||
return noun + "e"
|
||||
if gender == "n":
|
||||
return noun + "uri"
|
||||
# masculine
|
||||
if noun.endswith(("e",)):
|
||||
return noun[:-1] + "i"
|
||||
return noun + "i"
|
||||
|
||||
|
||||
# ── PUBLIC noun inflection ────────────────────────────────────────────────────────
|
||||
def inflect_noun(lemma, number, gender=None, case="nomacc", definite=False):
|
||||
lemma = lemma.strip().lower()
|
||||
g = gender or noun_gender(lemma)
|
||||
d = _NOUNS.get(lemma)
|
||||
numk = "SG" if number == "singular" else "PL"
|
||||
if d:
|
||||
if case == "voc":
|
||||
form = d["para"].get(("voc", True, numk)) or d["para"].get(("voc", False, numk))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# try the exact paradigm cell from kaikki (lexically grounded)
|
||||
form = d["para"].get((case, definite, numk))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# indefinite fallbacks from the paradigm
|
||||
if not definite:
|
||||
form = d["para"].get(("nomacc", False, numk))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
if numk == "PL" and d.get("PL"):
|
||||
return d["PL"], "lexicon"
|
||||
if numk == "SG":
|
||||
return lemma, "lexicon"
|
||||
# rule path
|
||||
base = lemma if number == "singular" else _rule_plural(lemma, g)
|
||||
if definite:
|
||||
return definite_suffix(base, g, number, case)
|
||||
return base, ("rule" if d is None else "lexicon")
|
||||
|
||||
|
||||
# ── PUBLIC adjective agreement ────────────────────────────────────────────────────
|
||||
def _neuter_map(gender, number):
|
||||
# neuter agrees masculine in SG, feminine in PL
|
||||
if gender == "n":
|
||||
return "m" if number == "singular" else "f"
|
||||
return gender
|
||||
|
||||
|
||||
def inflect_adj(lemma, gender, number, case="nomacc", definite=False):
|
||||
lemma = lemma.strip().lower()
|
||||
numk = "SG" if number == "singular" else "PL"
|
||||
eg = _neuter_map(gender, number) # neuter -> masc(SG)/fem(PL)
|
||||
d = _ADJS.get(lemma)
|
||||
if d:
|
||||
form = d.get((eg, numk))
|
||||
if form:
|
||||
return form, "lexicon"
|
||||
# rule fallback: 4-form pattern bun/bună/buni/bune keyed by effective gender
|
||||
a = lemma
|
||||
if number == "singular":
|
||||
if eg == "f":
|
||||
if a.endswith("e"):
|
||||
return a, "rule" # mare invariant sg
|
||||
if a.endswith("u"):
|
||||
return a[:-1] + "ă", "rule" # nou -> nouă
|
||||
if a.endswith("ă"):
|
||||
return a, "rule"
|
||||
return a + "ă", "rule" # bun -> bună
|
||||
return a, "rule" # masc/neut sg = lemma
|
||||
# plural
|
||||
if eg == "f":
|
||||
if a.endswith("e"):
|
||||
return a[:-1] + "i", "rule" # mare -> mari
|
||||
if a.endswith("u"):
|
||||
return a[:-1] + "e", "rule" # nou -> noue (approx; 'noi' irr)
|
||||
if a.endswith("ă"):
|
||||
return a[:-1] + "e", "rule"
|
||||
return a + "e", "rule" # bun -> bune
|
||||
# masc/neut(SG-only)->here masc pl -> -i
|
||||
if a.endswith("e"):
|
||||
return a[:-1] + "i", "rule" # mare -> mari
|
||||
if a.endswith("u"):
|
||||
return a[:-1] + "i", "rule"
|
||||
return a + "i", "rule" # bun -> buni
|
||||
|
||||
|
||||
def lexicon_stats():
|
||||
return {
|
||||
"verb_source": "UniMorph Romanian (github.com/unimorph/ron) + curated "
|
||||
"irregulars (avea/vrea/da + aux clitic paradigms)",
|
||||
"noun_source": "kaikki.org Romanian — full case/definite/vocative declension",
|
||||
"adj_source": "UniMorph Romanian ADJ (case×gender×number×definiteness)",
|
||||
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
|
||||
"unimorph_verb_forms": len(_VERBS),
|
||||
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
|
||||
"irregular_verb_lemmas": len(_IRREG),
|
||||
"participle_lemmas": len(_PART),
|
||||
"noun_lemmas": len(_NOUNS),
|
||||
"adj_lemmas": len(_ADJS),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||||
print("\n── SUFFIXED DEFINITE ARTICLE (the headline delta) ──")
|
||||
for n, g in [("om", "m"), ("băiat", "m"), ("casă", "f"), ("carte", "f"),
|
||||
("tren", "n"), ("student", "m"), ("floare", "f")]:
|
||||
sg = inflect_noun(n, "singular", g, "nomacc", True)
|
||||
pl = inflect_noun(n, "plural", g, "nomacc", True)
|
||||
gd = inflect_noun(n, "singular", g, "gendat", True)
|
||||
vo = inflect_noun(n, "singular", g, "voc", False)
|
||||
print(f" {n:8}({g}) def.sg={sg[0]:12} def.pl={pl[0]:14} "
|
||||
f"gen/dat.sg={gd[0]:12} voc={vo[0]}")
|
||||
print("\n── NEUTER split agreement (tren: masc SG / fem PL) ──")
|
||||
print(" tren nou ->", inflect_noun("tren", "singular", "n")[0],
|
||||
inflect_adj("nou", "n", "singular")[0])
|
||||
print(" trenuri noi->", inflect_noun("tren", "plural", "n")[0],
|
||||
inflect_adj("nou", "n", "plural")[0])
|
||||
print("\n── verbs ──")
|
||||
for l, m, t, p, n, in [("merge", "ind", "present", "third", "singular"),
|
||||
("avea", "ind", "present", "first", "singular"),
|
||||
("fi", "ind", "present", "third", "singular"),
|
||||
("vorbi", "ind", "present", "third", "plural"),
|
||||
("face", "sbjv", "present", "third", "singular"),
|
||||
("lucra", "ind", "imperfect", "third", "singular")]:
|
||||
print(f" {l:8}{m}/{t:10}{p[:3]}.{n[:2]} -> {conjugate(l,m,t,p,n)}")
|
||||
print(" perfect-aux(3sg):", aux("perfect", "third", "singular"),
|
||||
"| future(1sg):", aux("future", "first", "singular"),
|
||||
"| cond(3sg):", aux("conditional", "third", "singular"))
|
||||
print(" participle merge/vedea:", participle("merge"), participle("vedea"))
|
||||
+1
-1
@@ -22,7 +22,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)/el}"
|
||||
ELC="${ELC:-${EL_HOME}/dist/platform/elc}"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
SRC_DIR="$(cd .. && pwd)/src"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
|
||||
+2
-5
@@ -1,6 +1,3 @@
|
||||
.DS_Store
|
||||
*.db
|
||||
*.elc
|
||||
*.elh
|
||||
dist/
|
||||
target/
|
||||
*.db
|
||||
.DS_Store
|
||||
|
||||
+17
-108
@@ -132,7 +132,7 @@ fn route_text_health(method: String, path: String, body: String) -> String {
|
||||
// any durable write that follows persists the pruning too).
|
||||
fn persist_canonical() -> Int {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
// (2026-08-10 self-review) This returned a hardcoded 1, which made every
|
||||
// caller's `let saved: Int = persist_canonical()` a dead variable — six
|
||||
// durable write paths each believed they had confirmation of a successful
|
||||
@@ -140,57 +140,6 @@ fn persist_canonical() -> Int {
|
||||
return engram_save(dir + "/snapshot.json")
|
||||
}
|
||||
|
||||
// ── WAL persistence (design doc §§3-14; gated behind ENGRAM_WAL=on) ──────────
|
||||
// Default OFF → every persist path below is byte-identical to the historical
|
||||
// per-write full-snapshot behavior. When ON, structural mutations append O(1)
|
||||
// WAL records instead of rewriting the whole graph, with threshold compaction.
|
||||
fn wal_on() -> Bool {
|
||||
str_eq(env("ENGRAM_WAL"), "on")
|
||||
}
|
||||
|
||||
// Persist a single-node mutation (create / content-evolve / strengthen).
|
||||
fn persist_node(id: String) -> Int {
|
||||
if wal_on() {
|
||||
let d: String = engram_resolve_data_dir()
|
||||
let a: Int = engram_wal_node_put(d, id)
|
||||
let c: Int = engram_wal_maybe_compact(d)
|
||||
return a
|
||||
}
|
||||
return persist_canonical()
|
||||
}
|
||||
|
||||
// Persist edges appended at index >= start (covers single-edge and batch).
|
||||
fn persist_edges_since(start: Int) -> Int {
|
||||
if wal_on() {
|
||||
let d: String = engram_resolve_data_dir()
|
||||
let a: Int = engram_wal_edges_since(d, start)
|
||||
let c: Int = engram_wal_maybe_compact(d)
|
||||
return a
|
||||
}
|
||||
return persist_canonical()
|
||||
}
|
||||
|
||||
// Persist a Hebbian consolidation batch as ONE WAL record (single fsync, §5-B).
|
||||
fn persist_hebb_batch(start: Int) -> Int {
|
||||
if wal_on() {
|
||||
let d: String = engram_resolve_data_dir()
|
||||
let a: Int = engram_wal_hebb_batch(d, start)
|
||||
let c: Int = engram_wal_maybe_compact(d)
|
||||
return a
|
||||
}
|
||||
return persist_canonical()
|
||||
}
|
||||
|
||||
// Bulk mutation (embedding backfill, load-merge): write a fresh compaction base
|
||||
// so the many-node change is durable in one atomic snapshot; WAL is truncated.
|
||||
fn persist_bulk() -> Int {
|
||||
if wal_on() {
|
||||
let d: String = engram_resolve_data_dir()
|
||||
return engram_wal_compact(d)
|
||||
}
|
||||
return persist_canonical()
|
||||
}
|
||||
|
||||
// INCOMPLETE-ROUTE FIX (2026-07-24 self-review): this route silently dropped
|
||||
// label, importance, tier, and tags — engram_node() defaults label to content
|
||||
// and importance to 0.5, so every node created over HTTP lost its metadata.
|
||||
@@ -232,7 +181,7 @@ fn route_create_node(method: String, path: String, body: String) -> String {
|
||||
salience, importance, confidence,
|
||||
tier, tags
|
||||
)
|
||||
let saved: Int = persist_node(id)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||
}
|
||||
|
||||
@@ -259,7 +208,7 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||||
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let snap_path: String = dir + "/.scan-export.json"
|
||||
engram_save(snap_path)
|
||||
let snap: String = fs_read(snap_path)
|
||||
@@ -301,9 +250,8 @@ fn route_create_edge(method: String, path: String, body: String) -> String {
|
||||
// (dormant association); only default when the key is absent.
|
||||
let w_present: String = json_get_raw(body, "weight")
|
||||
let weight: Float = if str_eq(w_present, "") { 0.5 } else { json_get_float(body, "weight") }
|
||||
let ec0: Int = engram_edge_count()
|
||||
engram_connect(from_id, to_id, weight, relation)
|
||||
let saved: Int = persist_edges_since(ec0)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||
}
|
||||
|
||||
@@ -328,7 +276,6 @@ fn route_create_edges_batch(method: String, path: String, body: String) -> Strin
|
||||
if str_eq(arr, "") { return err_json("missing edges array") }
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 { return "{\"ok\":true,\"accepted\":0,\"skipped\":0}" }
|
||||
let ec0: Int = engram_edge_count()
|
||||
let i: Int = 0
|
||||
let accepted: Int = 0
|
||||
let skipped: Int = 0
|
||||
@@ -352,7 +299,7 @@ fn route_create_edges_batch(method: String, path: String, body: String) -> Strin
|
||||
// Skip it when nothing was accepted: an all-malformed payload must not
|
||||
// trigger a 60MB write.
|
||||
if accepted > 0 {
|
||||
let saved: Int = persist_hebb_batch(ec0)
|
||||
let saved: Int = persist_canonical()
|
||||
}
|
||||
return "{\"ok\":true,\"accepted\":" + int_to_str(accepted) + ",\"skipped\":" + int_to_str(skipped) + "}"
|
||||
}
|
||||
@@ -368,50 +315,22 @@ fn route_strengthen(method: String, path: String, body: String) -> String {
|
||||
let id: String = json_get_string(body, "node_id")
|
||||
if str_eq(id, "") { return err_json("missing node_id") }
|
||||
engram_strengthen(id)
|
||||
let saved: Int = persist_node(id)
|
||||
let saved: Int = persist_canonical()
|
||||
ok_json()
|
||||
}
|
||||
|
||||
// route_forget — DELETE /api/nodes/:id — INTEGRITY HARDENED (design doc §18.1).
|
||||
//
|
||||
// Two invariants now enforced AT THE STORE (not one layer up in neuron-api.el,
|
||||
// which a direct HTTP client could bypass):
|
||||
// 1. Write-protection: protected identity/value nodes (derived from the self
|
||||
// graph — self root + values hub + their neighbors, §18.3) cannot be
|
||||
// deleted over HTTP. Returns 403, node untouched.
|
||||
// 2. No hard delete over the wire, ever: an ordinary delete creates a
|
||||
// Tombstone marker node + `tombstones` edge and KEEPS the original node
|
||||
// and its edges (recoverable), instead of the old destructive
|
||||
// engram_forget() shift-delete. Raw engram_forget is now internal-GC only
|
||||
// and no longer reachable from any HTTP route.
|
||||
fn route_forget(method: String, path: String, body: String) -> String {
|
||||
let id: String = extract_id(path, "/api/nodes/")
|
||||
if str_eq(id, "") { return err_json("missing id") }
|
||||
if engram_is_protected(id) == 1 {
|
||||
return "{\"__status__\":403,\"error\":\"protected node; deletion refused\",\"id\":\"" + id + "\"}"
|
||||
}
|
||||
let tomb_id: String = engram_node_full(
|
||||
"tombstone:" + id, "Tombstone", "tombstone:" + id,
|
||||
0.1, 0.1, 1.0, "Episodic", "[\"tombstone\"]"
|
||||
)
|
||||
let ec0: Int = engram_edge_count()
|
||||
engram_connect(tomb_id, id, 1.0, "tombstones")
|
||||
let saved: Int = if wal_on() {
|
||||
let d: String = engram_resolve_data_dir()
|
||||
let a: Int = engram_wal_node_put(d, tomb_id)
|
||||
let b: Int = engram_wal_edges_since(d, ec0)
|
||||
let c: Int = engram_wal_maybe_compact(d)
|
||||
a
|
||||
} else {
|
||||
persist_canonical()
|
||||
}
|
||||
"{\"ok\":true,\"tombstoned\":\"" + id + "\",\"tombstone_id\":\"" + tomb_id + "\"}"
|
||||
engram_forget(id)
|
||||
let saved: Int = persist_canonical()
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) engram_save returns 0 on an empty path and the
|
||||
// route discarded it, so the response was a literal "ok":true regardless
|
||||
@@ -427,7 +346,7 @@ fn route_save(method: String, path: String, body: String) -> String {
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) This was a stub response over the single most
|
||||
// destructive operation in the server. engram_load returns 0 on an empty
|
||||
@@ -479,7 +398,7 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String {
|
||||
let result: String = engram_embed_backfill(n)
|
||||
let done: Float = json_get_float(result, "embedded")
|
||||
if done > 0.0 {
|
||||
let saved: Int = persist_bulk()
|
||||
let saved: Int = persist_canonical()
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -498,7 +417,7 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String {
|
||||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||||
fn route_sync(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||
let snap_path: String = dir + "/.sync-export.json"
|
||||
@@ -532,7 +451,7 @@ fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||
engram_load_merge(p)
|
||||
let added_n: Int = engram_node_count() - before_n
|
||||
let added_e: Int = engram_edge_count() - before_e
|
||||
let saved: Int = persist_bulk()
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||||
}
|
||||
|
||||
@@ -631,7 +550,7 @@ fn route_capture_knowledge(method: String, path: String, body: String) -> String
|
||||
sal, imp, conf,
|
||||
"Semantic", tags
|
||||
)
|
||||
let saved: Int = persist_node(id)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
@@ -794,21 +713,11 @@ let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
// §18.2: resolve the data dir safely — unset ENGRAM_DATA_DIR → $HOME/.neuron/engram,
|
||||
// never /tmp; fail loud if HOME is unresolvable (engram_resolve_data_dir exits).
|
||||
let data_dir: String = engram_resolve_data_dir()
|
||||
let data_dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data_dir_raw }
|
||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||
engram_load(snapshot_path)
|
||||
|
||||
// WAL replay (design doc §6). Gated: default OFF is byte-identical to legacy
|
||||
// snapshot-only boot. When ON, the snapshot above is the compaction BASE and
|
||||
// the WAL carries every mutation since; replay reconstructs state to the last
|
||||
// CRC-valid record, then opens the WAL for appending.
|
||||
if wal_on() {
|
||||
let replayed: Int = engram_wal_boot(data_dir)
|
||||
println("[engram] WAL enabled — replayed " + int_to_str(replayed) + " records")
|
||||
}
|
||||
|
||||
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
|
||||
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
|
||||
// Preserve the evidence and warn loudly — and since read routes no longer write
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# WAL unit + integration + crash-fuzz gate. Throwaway HOME/dirs only.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
REL="$HERE/../../lang/runtime"
|
||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" \
|
||||
"$HERE/test_wal.c" -lcurl -lpthread -o /tmp/test_wal
|
||||
HOME=/tmp/engram-throwaway-home /tmp/test_wal
|
||||
# Fail-loud data-dir check (must exit 1 with a FATAL line):
|
||||
cat > /tmp/test_failloud.c <<'C'
|
||||
#include "el_runtime.c"
|
||||
int main(void){ unsetenv("ENGRAM_DATA_DIR"); unsetenv("HOME");
|
||||
engram_resolve_data_dir(); printf("REACHED\n"); return 0; }
|
||||
C
|
||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" /tmp/test_failloud.c -lcurl -lpthread -o /tmp/test_failloud
|
||||
if env -u HOME -u ENGRAM_DATA_DIR /tmp/test_failloud; then echo "FAIL: should have exited"; exit 1; else echo "[PASS] fail-loud exit on unresolvable HOME"; fi
|
||||
@@ -1,473 +0,0 @@
|
||||
/* test_wal.c — unit + integration + crash-fuzz harness for the engram WAL.
|
||||
*
|
||||
* Includes el_runtime.c directly so it can exercise the static internals
|
||||
* (eg_crc32, eg_wal_*, eg_apply_*) in genuine isolation. Build:
|
||||
* cc -O2 -fbracket-depth=1024 -I<release-dir> test_wal.c -lcurl -lpthread -o test_wal
|
||||
* Runtime testing only — writes exclusively under a throwaway /tmp dir.
|
||||
*/
|
||||
#define ENGRAM_TEST_BUILD 1
|
||||
#include "el_runtime.c"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond) {
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_tmpdir[512];
|
||||
static void mk_tmpdir(void) {
|
||||
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/engram-wal-test-%d", (int)getpid());
|
||||
mkdir(g_tmpdir, 0700);
|
||||
}
|
||||
static void path_in(char* out, size_t cap, const char* name) {
|
||||
snprintf(out, cap, "%s/%s", g_tmpdir, name);
|
||||
}
|
||||
static void write_file(const char* path, const void* data, size_t n) {
|
||||
FILE* f = fopen(path, "wb"); if (!f) { perror("write_file"); exit(2); }
|
||||
fwrite(data, 1, n, f); fclose(f);
|
||||
}
|
||||
static long file_size(const char* path) {
|
||||
struct stat st; if (stat(path, &st) != 0) return -1; return (long)st.st_size;
|
||||
}
|
||||
static void reset_store(void) {
|
||||
char p[600]; path_in(p, sizeof(p), "_reset.json");
|
||||
const char* empty = "{\"nodes\":[],\"edges\":[],\"layers\":[]}";
|
||||
write_file(p, empty, strlen(empty));
|
||||
engram_load((el_val_t)(uintptr_t)p);
|
||||
}
|
||||
/* Close any open WAL handle so a fresh dir test starts clean. */
|
||||
static void wal_close(void) {
|
||||
if (eg_wal.fp) { fclose(eg_wal.fp); eg_wal.fp = NULL; }
|
||||
eg_wal.path[0] = 0; eg_wal.lsn = 0; eg_wal.bytes = 0; eg_wal.uncommitted = 0;
|
||||
}
|
||||
|
||||
/* ── Snapshot fingerprint: serialize store to a string for A==B comparisons ── */
|
||||
static char* store_fingerprint(void) {
|
||||
char p[600]; path_in(p, sizeof(p), "_fp.json");
|
||||
engram_save((el_val_t)(uintptr_t)p);
|
||||
long sz = file_size(p);
|
||||
if (sz < 0) return strdup("");
|
||||
FILE* f = fopen(p, "rb"); char* buf = malloc(sz + 1);
|
||||
size_t got = fread(buf, 1, sz, f); fclose(f); buf[got] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── crc32 known-answer vectors ─────────────────────────────────────────── */
|
||||
static void test_crc32(void) {
|
||||
printf("\n== crc32 known-answer ==\n");
|
||||
ok("crc32(\"\") == 0x00000000", eg_crc32("", 0) == 0x00000000u);
|
||||
ok("crc32(\"123456789\") == 0xCBF43926", eg_crc32("123456789", 9) == 0xCBF43926u);
|
||||
ok("crc32(\"a\") == 0xE8B7BE43", eg_crc32("a", 1) == 0xE8B7BE43u);
|
||||
/* builtin wrapper agrees */
|
||||
ok("engram_crc32 builtin matches",
|
||||
(uint32_t)(int64_t)engram_crc32(EL_STR("123456789")) == 0xCBF43926u);
|
||||
}
|
||||
|
||||
/* ── WAL record encode↔decode + framing + corruption rejection ──────────── */
|
||||
static void test_framing(void) {
|
||||
printf("\n== record framing / encode-decode / corruption ==\n");
|
||||
char wal[600]; path_in(wal, sizeof(wal), "engram.wal");
|
||||
unlink(wal); wal_close();
|
||||
eg_wal_open(g_tmpdir);
|
||||
const char* pl = "{\"id\":\"n1\",\"content\":\"x\"}";
|
||||
int w = eg_wal_write(EG_OP_NODE_PUT, 0, pl, strlen(pl));
|
||||
eg_wal_commit(1);
|
||||
ok("append returns success", w == 1);
|
||||
|
||||
/* Read raw bytes and verify header fields. */
|
||||
long sz = file_size(wal);
|
||||
FILE* f = fopen(wal, "rb"); unsigned char* buf = malloc(sz); fread(buf, 1, sz, f); fclose(f);
|
||||
uint32_t magic, len32, crc; uint64_t lsn;
|
||||
memcpy(&magic, buf + 0, 4); memcpy(&len32, buf + 4, 4);
|
||||
uint8_t op = buf[8], flags = buf[9]; memcpy(&lsn, buf + 10, 8); memcpy(&crc, buf + 18, 4);
|
||||
ok("magic == 'EWL1'", magic == EG_WAL_MAGIC);
|
||||
ok("payload_len correct", len32 == strlen(pl));
|
||||
ok("op == NODE_PUT", op == EG_OP_NODE_PUT);
|
||||
ok("flags == 0", flags == 0);
|
||||
ok("lsn == 1", lsn == 1);
|
||||
ok("crc matches recompute", crc == eg_wal_record_crc(op, flags, lsn, pl, strlen(pl)));
|
||||
ok("total size == hdr+payload", sz == (long)(EG_WAL_HDR_LEN + strlen(pl)));
|
||||
|
||||
/* Corrupt CRC → replay rejects (0 records). */
|
||||
{ char bad[600]; path_in(bad, sizeof(bad), "bad_crc.wal");
|
||||
unsigned char* c = malloc(sz); memcpy(c, buf, sz); c[18] ^= 0xFF; write_file(bad, c, sz);
|
||||
reset_store(); uint64_t ll = 99; int64_t n = eg_wal_replay_file(bad, &ll);
|
||||
ok("corrupt crc → 0 applied", n == 0 && ll == 0); free(c); }
|
||||
/* Corrupt length (claim longer than file) → replay rejects. */
|
||||
{ char bad[600]; path_in(bad, sizeof(bad), "bad_len.wal");
|
||||
unsigned char* c = malloc(sz); memcpy(c, buf, sz);
|
||||
uint32_t big = 0xFFFF; memcpy(c + 4, &big, 4); write_file(bad, c, sz);
|
||||
reset_store(); int64_t n = eg_wal_replay_file(bad, NULL);
|
||||
ok("corrupt length → 0 applied", n == 0); free(c); }
|
||||
/* Intact file → replay applies exactly 1. */
|
||||
{ reset_store(); uint64_t ll = 0; int64_t n = eg_wal_replay_file(wal, &ll);
|
||||
ok("intact → 1 applied, last_lsn=1", n == 1 && ll == 1); }
|
||||
free(buf); wal_close();
|
||||
}
|
||||
|
||||
/* ── Single-op apply on an (empty) store ────────────────────────────────── */
|
||||
static void test_single_ops(void) {
|
||||
printf("\n== single-op apply ==\n");
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"n1\",\"content\":\"hello\",\"salience\":0.7,\"layer_id\":2}");
|
||||
EngramNode* n = engram_find_node("n1");
|
||||
ok("NODE_PUT creates node", n != NULL);
|
||||
ok("NODE_PUT content", n && strcmp(n->content, "hello") == 0);
|
||||
ok("NODE_PUT salience", n && n->salience > 0.69 && n->salience < 0.71);
|
||||
ok("NODE_PUT layer_id", n && n->layer_id == 2);
|
||||
ok("NODE_PUT count == 1", engram_get()->node_count == 1);
|
||||
|
||||
/* NODE_PUT upsert idempotency: same id overwrites, no dup. */
|
||||
eg_apply_node_put("{\"id\":\"n1\",\"content\":\"changed\"}");
|
||||
n = engram_find_node("n1");
|
||||
ok("NODE_PUT upsert (no dup)", engram_get()->node_count == 1);
|
||||
ok("NODE_PUT upsert content", n && strcmp(n->content, "changed") == 0);
|
||||
|
||||
eg_apply_node_put("{\"id\":\"n2\",\"content\":\"b\"}");
|
||||
eg_apply_edge_put("{\"id\":\"e1\",\"from_id\":\"n1\",\"to_id\":\"n2\",\"relation\":\"r\",\"weight\":0.4,\"hebb\":0.25}");
|
||||
EngramStore* g = engram_get();
|
||||
int64_t ei = eg_find_edge_index(g, "e1");
|
||||
ok("EDGE_PUT creates edge", ei >= 0);
|
||||
ok("EDGE_PUT weight", ei >= 0 && g->edges[ei].weight > 0.39 && g->edges[ei].weight < 0.41);
|
||||
ok("EDGE_PUT hebb", ei >= 0 && g->edges[ei].hebb > 0.24 && g->edges[ei].hebb < 0.26);
|
||||
/* EDGE_PUT upsert idempotency */
|
||||
eg_apply_edge_put("{\"id\":\"e1\",\"from_id\":\"n1\",\"to_id\":\"n2\",\"relation\":\"r\",\"weight\":0.9}");
|
||||
ok("EDGE_PUT upsert (no dup)", g->edge_count == 1);
|
||||
|
||||
/* TOMBSTONE marks metadata, keeps node */
|
||||
eg_wal_apply(EG_OP_TOMBSTONE, "{\"id\":\"n1\"}", strlen("{\"id\":\"n1\"}"));
|
||||
n = engram_find_node("n1");
|
||||
ok("TOMBSTONE keeps node", n != NULL);
|
||||
ok("TOMBSTONE marks metadata", n && strstr(n->metadata, "tombstoned") != NULL);
|
||||
|
||||
/* SUPERSEDE marks metadata with by-id */
|
||||
{ const char* s = "{\"id\":\"n2\",\"by\":\"n1\"}";
|
||||
eg_wal_apply(EG_OP_SUPERSEDE, s, strlen(s));
|
||||
n = engram_find_node("n2");
|
||||
ok("SUPERSEDE marks superseded_by", n && strstr(n->metadata, "superseded_by") != NULL);
|
||||
ok("SUPERSEDE records by-id", n && strstr(n->metadata, "n1") != NULL); }
|
||||
|
||||
/* LAYER_PUT / LAYER_DEL */
|
||||
{ const char* lp = "{\"layer_id\":42,\"name\":\"testlayer\",\"activation_priority\":7}";
|
||||
eg_wal_apply(EG_OP_LAYER_PUT, lp, strlen(lp));
|
||||
int found = 0; for (size_t i = 0; i < g->layer_count; i++)
|
||||
if (g->layers[i].layer_id == 42 && g->layers[i].name && strcmp(g->layers[i].name, "testlayer") == 0) found = 1;
|
||||
ok("LAYER_PUT adds layer", found);
|
||||
const char* ld = "{\"layer_id\":42}";
|
||||
eg_wal_apply(EG_OP_LAYER_DEL, ld, strlen(ld));
|
||||
int gone = 1; for (size_t i = 0; i < g->layer_count; i++)
|
||||
if (g->layers[i].layer_id == 42 && g->layers[i].name) gone = 0;
|
||||
ok("LAYER_DEL removes layer name", gone); }
|
||||
|
||||
/* HEBB_BATCH upserts multiple edges in one record */
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"a\"}"); eg_apply_node_put("{\"id\":\"b\"}"); eg_apply_node_put("{\"id\":\"c\"}");
|
||||
{ const char* hb = "{\"edges\":["
|
||||
"{\"id\":\"he1\",\"from_id\":\"a\",\"to_id\":\"b\",\"hebb\":0.1},"
|
||||
"{\"id\":\"he2\",\"from_id\":\"b\",\"to_id\":\"c\",\"hebb\":0.2}]}";
|
||||
eg_wal_apply(EG_OP_HEBB_BATCH, hb, strlen(hb));
|
||||
ok("HEBB_BATCH upserts 2 edges", engram_get()->edge_count == 2); }
|
||||
|
||||
/* FORGET hard-removes node + incident edges */
|
||||
{ const char* fg = "{\"id\":\"b\"}";
|
||||
eg_wal_apply(EG_OP_FORGET, fg, strlen(fg));
|
||||
ok("FORGET removes node", engram_find_node("b") == NULL);
|
||||
ok("FORGET removes incident edges", engram_get()->edge_count == 0); }
|
||||
}
|
||||
|
||||
/* ── Replay idempotency: apply file twice == once ───────────────────────── */
|
||||
static void test_replay_idempotent(void) {
|
||||
printf("\n== replay idempotency ==\n");
|
||||
reset_store(); wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "engram.wal"); unlink(wal);
|
||||
eg_wal_open(g_tmpdir);
|
||||
eg_apply_node_put("{\"id\":\"x\"}");
|
||||
engram_wal_node_put(EL_STR(g_tmpdir), EL_STR("x"));
|
||||
eg_apply_node_put("{\"id\":\"y\"}");
|
||||
engram_wal_node_put(EL_STR(g_tmpdir), EL_STR("y"));
|
||||
eg_wal_commit(1);
|
||||
reset_store();
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
int64_t after1 = engram_get()->node_count;
|
||||
eg_wal_replay_file(wal, NULL); /* replay AGAIN */
|
||||
int64_t after2 = engram_get()->node_count;
|
||||
ok("replay once == 2 nodes", after1 == 2);
|
||||
ok("replay twice == replay once (idempotent)", after2 == after1);
|
||||
wal_close();
|
||||
}
|
||||
|
||||
/* ── hebb + emb serialize round-trip ────────────────────────────────────── */
|
||||
static void test_hebb_emb_roundtrip(void) {
|
||||
printf("\n== hebb + emb serialize round-trip ==\n");
|
||||
reset_store();
|
||||
/* hebb via edge emit→parse */
|
||||
eg_apply_node_put("{\"id\":\"p\"}"); eg_apply_node_put("{\"id\":\"q\"}");
|
||||
eg_apply_edge_put("{\"id\":\"eh\",\"from_id\":\"p\",\"to_id\":\"q\",\"hebb\":0.123456}");
|
||||
EngramStore* g = engram_get();
|
||||
int64_t ei = eg_find_edge_index(g, "eh");
|
||||
JsonBuf b; jb_init(&b); engram_emit_edge_json(&b, &g->edges[ei]);
|
||||
char* ej = strndup(b.buf, b.len); free(b.buf);
|
||||
ok("emit edge carries hebb", strstr(ej, "\"hebb\"") != NULL);
|
||||
eg_apply_edge_put(ej); /* re-parse */
|
||||
ei = eg_find_edge_index(g, "eh");
|
||||
ok("hebb survives emit→parse (%.6g)", g->edges[ei].hebb > 0.1234 && g->edges[ei].hebb < 0.1235);
|
||||
free(ej);
|
||||
|
||||
/* emb via node emit(include_emb=1)→parse, bit-exact at %.4g. The runtime
|
||||
* requires dim>=8 (garbage guard), so use 8 dyadic-rational values that
|
||||
* survive %.4g round-trip exactly. */
|
||||
eg_apply_node_put("{\"id\":\"ez\",\"emb\":\"0.5,-0.25,0.125,1,-0.0625,0.75,-1,0.375\"}");
|
||||
EngramNode* n = engram_find_node("ez");
|
||||
ok("emb parsed dim==8", n && n->emb_dim == 8);
|
||||
float e0 = n->emb[0], e1 = n->emb[1], e2 = n->emb[2], e3 = n->emb[3];
|
||||
JsonBuf nb; jb_init(&nb); engram_emit_node_json(&nb, n, 1);
|
||||
char* nj = strndup(nb.buf, nb.len); free(nb.buf);
|
||||
ok("emit node carries emb", strstr(nj, "\"emb\"") != NULL);
|
||||
eg_apply_node_put(nj); free(nj);
|
||||
n = engram_find_node("ez");
|
||||
ok("emb[0]==0.5 exact", n->emb[0] == e0 && e0 == 0.5f);
|
||||
ok("emb[1]==-0.25 exact", n->emb[1] == e1 && e1 == -0.25f);
|
||||
ok("emb[2]==0.125 exact", n->emb[2] == e2 && e2 == 0.125f);
|
||||
ok("emb[3]==1 exact", n->emb[3] == e3 && e3 == 1.0f);
|
||||
}
|
||||
|
||||
/* ── data-dir resolution (§18.2) ────────────────────────────────────────── */
|
||||
static void test_data_dir(void) {
|
||||
printf("\n== data-dir resolution ==\n");
|
||||
setenv("ENGRAM_DATA_DIR", "/data/explicit", 1);
|
||||
ok("explicit ENGRAM_DATA_DIR honored",
|
||||
strcmp(EL_CSTR(engram_resolve_data_dir()), "/data/explicit") == 0);
|
||||
unsetenv("ENGRAM_DATA_DIR");
|
||||
char fakehome[600]; snprintf(fakehome, sizeof(fakehome), "%s/home", g_tmpdir);
|
||||
mkdir(fakehome, 0700);
|
||||
setenv("HOME", fakehome, 1);
|
||||
char expect[700]; snprintf(expect, sizeof(expect), "%s/.neuron/engram", fakehome);
|
||||
const char* got = EL_CSTR(engram_resolve_data_dir());
|
||||
ok("unset → $HOME/.neuron/engram", strcmp(got, expect) == 0);
|
||||
ok("resolved dir is NOT /tmp/engram", strcmp(got, "/tmp/engram") != 0);
|
||||
ok("resolved dir was created", file_size(expect) >= 0 || 1); /* mkdir ran */
|
||||
/* HOME-unresolvable fail-loud path is verified out-of-process (calls exit). */
|
||||
printf(" [NOTE] HOME-unresolvable → exit(1) verified via subprocess (see run script)\n");
|
||||
}
|
||||
|
||||
/* ── protected-set derivation (§18.1/18.3) ──────────────────────────────── */
|
||||
static void build_self_graph(int n_identity, int n_values) {
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"" EG_SELF_ROOT "\",\"content\":\"self\"}");
|
||||
eg_apply_node_put("{\"id\":\"" EG_VALUES_HUB "\",\"content\":\"values-hub\"}");
|
||||
char buf[256];
|
||||
for (int i = 0; i < n_identity; i++) {
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"id-%d\"}", i); eg_apply_node_put(buf);
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"eid-%d\",\"from_id\":\"" EG_SELF_ROOT "\",\"to_id\":\"id-%d\"}", i, i);
|
||||
eg_apply_edge_put(buf);
|
||||
}
|
||||
for (int i = 0; i < n_values; i++) {
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"val-%d\"}", i); eg_apply_node_put(buf);
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"eval-%d\",\"from_id\":\"" EG_VALUES_HUB "\",\"to_id\":\"val-%d\"}", i, i);
|
||||
eg_apply_edge_put(buf);
|
||||
}
|
||||
/* an ordinary, unconnected node */
|
||||
eg_apply_node_put("{\"id\":\"ordinary-1\"}");
|
||||
}
|
||||
static int count_occurrences(const char* hay, const char* needle) {
|
||||
int c = 0; const char* p = hay;
|
||||
while ((p = strstr(p, needle))) { c++; p += strlen(needle); }
|
||||
return c;
|
||||
}
|
||||
static void test_protected(void) {
|
||||
printf("\n== protected-set derivation ==\n");
|
||||
build_self_graph(7, 13);
|
||||
const char* pj = EL_CSTR(engram_protected_json());
|
||||
ok("self root protected", eg_is_protected(EG_SELF_ROOT));
|
||||
ok("values hub protected", eg_is_protected(EG_VALUES_HUB));
|
||||
ok("a value node protected", eg_is_protected("val-5"));
|
||||
ok("an identity node protected", eg_is_protected("id-3"));
|
||||
ok("ordinary node NOT protected", !eg_is_protected("ordinary-1"));
|
||||
ok("missing node NOT protected", !eg_is_protected("nope-xyz"));
|
||||
ok("derived set has 13 values", count_occurrences(pj, "\"val-") == 13);
|
||||
ok("derived set has 7 identity", count_occurrences(pj, "\"id-") == 7);
|
||||
ok("ordinary not in derived set", strstr(pj, "ordinary-1") == NULL);
|
||||
}
|
||||
|
||||
/* ── Replay parity: WAL round-trip == direct apply ──────────────────────── */
|
||||
static void rand_node_json(char* out, size_t cap, int id) {
|
||||
snprintf(out, cap, "{\"id\":\"pn-%d\",\"content\":\"c%d\",\"salience\":%.3f,\"importance\":%.3f}",
|
||||
id, id, (rand() % 1000) / 1000.0, (rand() % 1000) / 1000.0);
|
||||
}
|
||||
static void test_replay_parity(void) {
|
||||
printf("\n== replay parity (WAL round-trip vs direct apply) ==\n");
|
||||
srand(1234);
|
||||
/* Build a random op stream. */
|
||||
#define NOPS 200
|
||||
char ops[NOPS][256]; uint8_t opcode[NOPS]; int nops = 0;
|
||||
int nodes_created = 0;
|
||||
for (int i = 0; i < NOPS; i++) {
|
||||
int r = rand() % 10;
|
||||
if (r < 6 || nodes_created < 3) {
|
||||
rand_node_json(ops[nops], sizeof(ops[0]), nodes_created);
|
||||
opcode[nops] = EG_OP_NODE_PUT; nodes_created++; nops++;
|
||||
} else if (r < 8) { /* edge between two existing nodes */
|
||||
int a = rand() % nodes_created, b = rand() % nodes_created;
|
||||
snprintf(ops[nops], sizeof(ops[0]),
|
||||
"{\"id\":\"pe-%d\",\"from_id\":\"pn-%d\",\"to_id\":\"pn-%d\",\"weight\":0.5}", i, a, b);
|
||||
opcode[nops] = EG_OP_EDGE_PUT; nops++;
|
||||
} else { /* upsert (overwrite) an existing node */
|
||||
int a = rand() % nodes_created;
|
||||
snprintf(ops[nops], sizeof(ops[0]), "{\"id\":\"pn-%d\",\"content\":\"upd%d\"}", a, i);
|
||||
opcode[nops] = EG_OP_NODE_PUT; nops++;
|
||||
}
|
||||
}
|
||||
/* Oracle: apply directly. */
|
||||
reset_store();
|
||||
for (int i = 0; i < nops; i++) eg_wal_apply(opcode[i], ops[i], strlen(ops[i]));
|
||||
char* oracle = store_fingerprint();
|
||||
|
||||
/* WAL path: write each op to a fresh WAL, then replay into a reset store. */
|
||||
wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "parity.wal"); unlink(wal);
|
||||
/* point eg_wal at the parity file by opening a dir handle then overriding */
|
||||
reset_store();
|
||||
{ FILE* f = fopen(wal, "wb"); fclose(f); }
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal);
|
||||
eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
for (int i = 0; i < nops; i++) eg_wal_write(opcode[i], 0, ops[i], strlen(ops[i]));
|
||||
eg_wal_commit(1); wal_close();
|
||||
reset_store();
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
char* replayed = store_fingerprint();
|
||||
|
||||
ok("WAL replay fingerprint == direct-apply oracle", strcmp(oracle, replayed) == 0);
|
||||
if (strcmp(oracle, replayed) != 0) {
|
||||
printf(" oracle len=%zu\n replay len=%zu\n", strlen(oracle), strlen(replayed));
|
||||
}
|
||||
free(oracle); free(replayed);
|
||||
}
|
||||
|
||||
/* ── Torn-tail fuzz: truncate at EVERY offset; never crash, recover to last
|
||||
* intact record ─────────────────────────────────────────────────────── */
|
||||
static int count_full_records(const unsigned char* buf, long len) {
|
||||
long off = 0; int n = 0;
|
||||
while (off + EG_WAL_HDR_LEN <= len) {
|
||||
uint32_t magic, len32; memcpy(&magic, buf + off, 4);
|
||||
if (magic != EG_WAL_MAGIC) break;
|
||||
memcpy(&len32, buf + off + 4, 4);
|
||||
if (off + EG_WAL_HDR_LEN + len32 > len) break;
|
||||
n++; off += EG_WAL_HDR_LEN + len32;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static void test_torn_tail(void) {
|
||||
printf("\n== torn-tail fuzz (truncate at every byte offset) ==\n");
|
||||
wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "torn.wal"); unlink(wal);
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal);
|
||||
eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
char pl[128]; snprintf(pl, sizeof(pl), "{\"id\":\"t-%d\",\"content\":\"payload-%d\"}", i, i);
|
||||
eg_wal_write(EG_OP_NODE_PUT, 0, pl, strlen(pl));
|
||||
}
|
||||
eg_wal_commit(1); wal_close();
|
||||
long sz = file_size(wal);
|
||||
FILE* f = fopen(wal, "rb"); unsigned char* full = malloc(sz); fread(full, 1, sz, f); fclose(f);
|
||||
|
||||
int all_ok = 1, mismatches = 0;
|
||||
char trunc[600]; path_in(trunc, sizeof(trunc), "torn_trunc.wal");
|
||||
for (long L = 0; L <= sz; L++) {
|
||||
write_file(trunc, full, L);
|
||||
reset_store();
|
||||
uint64_t last = 12345;
|
||||
int64_t applied = eg_wal_replay_file(trunc, &last); /* must not crash */
|
||||
int expect = count_full_records(full, L);
|
||||
if (applied != expect) { all_ok = 0; if (mismatches++ < 3)
|
||||
printf(" L=%ld applied=%lld expect=%d\n", L, (long long)applied, expect); }
|
||||
}
|
||||
ok("no crash across all truncation offsets", 1); /* reached here => survived */
|
||||
ok("recovered record count == #intact records at every offset", all_ok);
|
||||
free(full);
|
||||
}
|
||||
|
||||
/* ── Compaction crash-window convergence (§7) ───────────────────────────── */
|
||||
static void test_compaction_crash(void) {
|
||||
printf("\n== compaction crash-window convergence ==\n");
|
||||
/* Build state: base snapshot has n1; WAL adds n2,n3. */
|
||||
char dir[600]; snprintf(dir, sizeof(dir), "%s/comp", g_tmpdir); mkdir(dir, 0700);
|
||||
char base[700], wal[700], waltmp[700];
|
||||
snprintf(base, sizeof(base), "%s/snapshot.json", dir);
|
||||
snprintf(wal, sizeof(wal), "%s/engram.wal", dir);
|
||||
snprintf(waltmp, sizeof(waltmp), "%s/engram.wal.tmp", dir);
|
||||
|
||||
/* Reference full state = n1,n2,n3. */
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"n1\"}");
|
||||
eg_apply_node_put("{\"id\":\"n2\"}");
|
||||
eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
char* full = store_fingerprint();
|
||||
|
||||
/* Prepare OLD base (n1 only) + OLD wal (n2,n3). */
|
||||
reset_store(); eg_apply_node_put("{\"id\":\"n1\"}");
|
||||
engram_save((el_val_t)(uintptr_t)base);
|
||||
wal_close(); unlink(wal);
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal); eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
reset_store(); eg_apply_node_put("{\"id\":\"n1\"}"); eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_wal_node_put(EL_STR(dir), EL_STR("n2"));
|
||||
engram_wal_node_put(EL_STR(dir), EL_STR("n3"));
|
||||
eg_wal_commit(1); wal_close();
|
||||
|
||||
/* Boot helper: load base then replay wal (mirrors server boot order). */
|
||||
#define BOOT_FP(fp) do { \
|
||||
engram_load((el_val_t)(uintptr_t)base); \
|
||||
eg_wal_replay_file(wal, NULL); \
|
||||
fp = store_fingerprint(); } while (0)
|
||||
|
||||
/* Crash BEFORE compaction (steady state). */
|
||||
char* c0; BOOT_FP(c0);
|
||||
ok("pre-compaction boot converges to full", strcmp(c0, full) == 0); free(c0);
|
||||
|
||||
/* Crash AFTER step 1 (new base written) but BEFORE wal swap:
|
||||
* base now = full (n1,n2,n3), wal still = old (n2,n3). Idempotent replay. */
|
||||
engram_load((el_val_t)(uintptr_t)base); /* reload old base into store */
|
||||
eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_save((el_val_t)(uintptr_t)base); /* == compaction step 1: new base */
|
||||
char* c1; BOOT_FP(c1);
|
||||
ok("crash after new-base, before wal-swap → converges", strcmp(c1, full) == 0); free(c1);
|
||||
|
||||
/* Crash AFTER wal.tmp written but BEFORE rename: stray tmp ignored,
|
||||
* old wal still authoritative over (new) base. */
|
||||
{ FILE* tf = fopen(waltmp, "wb"); const char* junk = "PARTIAL"; fwrite(junk,1,7,tf); fclose(tf); }
|
||||
char* c2; BOOT_FP(c2);
|
||||
ok("crash after wal.tmp, before rename → converges", strcmp(c2, full) == 0);
|
||||
unlink(waltmp); free(c2);
|
||||
|
||||
/* Crash AFTER rename (compaction complete): base=full, wal=only COMPACT_MARK. */
|
||||
reset_store();
|
||||
engram_load((el_val_t)(uintptr_t)base);
|
||||
eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_wal_compact(EL_STR(dir)); /* full compaction */
|
||||
wal_close();
|
||||
char* c3;
|
||||
engram_load((el_val_t)(uintptr_t)base);
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
c3 = store_fingerprint();
|
||||
ok("post-compaction boot converges to full", strcmp(c3, full) == 0);
|
||||
long wsz = file_size(wal);
|
||||
ok("post-compaction WAL truncated (only COMPACT_MARK)",
|
||||
wsz > 0 && wsz < 64); /* just the marker record */
|
||||
free(c3); free(full);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mk_tmpdir();
|
||||
printf("engram WAL test harness — tmpdir=%s\n", g_tmpdir);
|
||||
test_crc32();
|
||||
test_framing();
|
||||
test_single_ops();
|
||||
test_replay_idempotent();
|
||||
test_hebb_emb_roundtrip();
|
||||
test_data_dir();
|
||||
test_protected();
|
||||
test_replay_parity();
|
||||
test_torn_tail();
|
||||
test_compaction_crash();
|
||||
printf("\n================= %d passed, %d failed =================\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
+6
-6
@@ -27,11 +27,11 @@ This is where almost all work belongs. El programs are source files that get com
|
||||
|
||||
**Do not add C code when El can express it.** If functionality can be built from existing El primitives (string ops, `exec`, `fs_read/write`, `http_post`, etc.), write it in El.
|
||||
|
||||
### Layer 2: The C seed (`runtime/el_seed.c`)
|
||||
### Layer 2: The C seed (`el-compiler/runtime/el_seed.c`)
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The old `el_runtime.c` has been archived to `runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
The old `el_runtime.c` has been archived to `el-compiler/runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
|
||||
**Only edit `el_seed.c` when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features). For everything else, write El.
|
||||
|
||||
@@ -50,9 +50,9 @@ After changing any `.el` source in `el-compiler/src/`:
|
||||
```bash
|
||||
cd /Users/will/Development/neuron-technologies/foundation/el
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c runtime/el_seed.c
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
# Verify self-hosting:
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
@@ -104,8 +104,8 @@ Use `exec()` (blocking) or `exec_bg()` (fire-and-forget) with shell scripts to r
|
||||
| `el-compiler/src/codegen.el` | Code generator — builtin arity table lives here |
|
||||
| `el-compiler/src/lexer.el` | Lexer |
|
||||
| `el-compiler/src/parser.el` | Parser |
|
||||
| `runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `el-compiler/runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `el-compiler/runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `spec/language.md` | Language specification |
|
||||
| `BOOTSTRAP.md` | How to recover the compiler from scratch |
|
||||
| `elc-cli.el` | Compiler entry point |
|
||||
|
||||
+12
-12
@@ -50,9 +50,9 @@ To rebuild the current binary from source using the current binary:
|
||||
```bash
|
||||
cd /path/to/el
|
||||
./dist/platform/elc elc-cli.el elc-new.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c runtime/el_runtime.c
|
||||
elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
Verify self-hosting by using `elc-new` to recompile itself and diffing the outputs.
|
||||
@@ -288,14 +288,14 @@ The codegen tracks declared names per C scope. When `count` is already in `decla
|
||||
|
||||
## 3. The Runtime API
|
||||
|
||||
All runtime functions are declared in `runtime/el_runtime.h`. Every compiled El program links against `runtime/el_runtime.c`.
|
||||
All runtime functions are declared in `el-compiler/runtime/el_runtime.h`. Every compiled El program links against `el-compiler/runtime/el_runtime.c`.
|
||||
|
||||
All values are `el_val_t` (`int64_t`). Strings are pointers cast through `int64_t` using `EL_STR(s)` / `EL_CSTR(v)` macros.
|
||||
|
||||
Canonical compile command:
|
||||
```bash
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o <out> <prog>.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### I/O
|
||||
@@ -794,8 +794,8 @@ Using your minimal implementation, compile `elc-cli.el` (which imports the entir
|
||||
python3 minimal_elc.py elc-cli.el > elc-new.c
|
||||
|
||||
# Build with the runtime
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o elc-new elc-new.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-new elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### Step 5: Verify Self-Hosting
|
||||
@@ -803,8 +803,8 @@ cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
```bash
|
||||
# Compile elc-cli.el with the new compiler
|
||||
./elc-new elc-cli.el elc-v2.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o elc-v2 elc-v2.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-v2 elc-v2.c el-compiler/runtime/el_runtime.c
|
||||
|
||||
# Compile again with the second-generation compiler
|
||||
./elc-v2 elc-cli.el elc-v3.c
|
||||
@@ -880,9 +880,9 @@ This is the planned path. It does not exist yet.
|
||||
| `el-compiler/src/parser.el` | Recursive descent parser. `parse(tokens)` → AST. All statement and expression forms | 1071 |
|
||||
| `el-compiler/src/codegen.el` | C code emitter. `codegen(stmts, source)` → (streams to stdout). Expression codegen, statement codegen, function codegen, type tracking, capability enforcement, temporal type dispatch | 2721 |
|
||||
| `el-compiler/src/codegen-js.el` | JavaScript backend. `codegen_js(stmts, source)` → JS source | ~500 |
|
||||
| `runtime/el_runtime.h` | Full runtime API declaration | 755 |
|
||||
| `runtime/el_runtime.c` | Full runtime implementation | large |
|
||||
| `runtime/el_runtime.js` | JS runtime | — |
|
||||
| `el-compiler/runtime/el_runtime.h` | Full runtime API declaration | 755 |
|
||||
| `el-compiler/runtime/el_runtime.c` | Full runtime implementation | large |
|
||||
| `el-compiler/runtime/el_runtime.js` | JS runtime | — |
|
||||
| `elb.el` | Build coordinator. Reads `manifest.el`, walks import graph, compiles modules, links binary. The `.NET`-style incremental build model | 367 |
|
||||
| `elc-combined.el` | Pre-merged single-file bootstrap edition (for early bootstrap iterations) | large |
|
||||
| `spec/language.md` | Language specification v1.2.0 | — |
|
||||
|
||||
@@ -132,7 +132,7 @@ if [[ $LVGL_OK -eq 1 ]]; then
|
||||
else
|
||||
_miss "LVGL/MCU" "-DEL_TARGET_LVGL (lvgl.h not found)"
|
||||
echo " Install: git clone https://github.com/lvgl/lvgl"
|
||||
echo " (place lvgl/ next to runtime/)"
|
||||
echo " (place lvgl/ next to el-compiler/runtime/)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,897 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
* EL_NULL null / zero value
|
||||
* EL_FALSE boolean false (0)
|
||||
* EL_TRUE boolean true (1)
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
|
||||
* register handlers across translation units; previously defined only inside el_runtime.c, which
|
||||
* made cross-module references (and the Windows build) fail. Home in the shared header. */
|
||||
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
|
||||
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
#define EL_FALSE ((el_val_t)0)
|
||||
#define EL_TRUE ((el_val_t)1)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t println(el_val_t s);
|
||||
el_val_t print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t native_str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */
|
||||
el_val_t el_arena_push(void);
|
||||
el_val_t el_arena_pop(el_val_t mark);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_serve_async(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
el_val_t html_escape(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_list_json(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_escape_string(el_val_t sv);
|
||||
el_val_t json_build_object(el_val_t kvs);
|
||||
el_val_t json_build_array(el_val_t items);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
el_val_t now_ns(void);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
el_val_t state_has(el_val_t key);
|
||||
el_val_t state_get_or(el_val_t key, el_val_t default_val);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||
* during long compilation loops (e.g. after each function is compiled).
|
||||
* Returns 0 when memory is within bounds. */
|
||||
el_val_t el_mem_check(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_get_node_by_label(el_val_t label);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── Working memory ──────────────────────────────────────────────────────────*/
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void);
|
||||
el_val_t engram_wm_top_json(el_val_t n);
|
||||
el_val_t engram_load_merge(el_val_t path);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */
|
||||
el_val_t stdout_restore(void); /* restore process stdout to terminal */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
|
||||
* The El self-hosting compiler emits calls to __-prefixed names. These are
|
||||
* forwarding wrappers around the existing el_runtime functions above. */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s);
|
||||
el_val_t __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f);
|
||||
el_val_t __str_to_float(el_val_t s);
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b);
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t __str_alloc(el_val_t n);
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code);
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
el_val_t __fs_list_raw(el_val_t path);
|
||||
|
||||
/* HTTP server */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
|
||||
|
||||
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
|
||||
el_val_t __http_conn_fd(void);
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path);
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_len(el_val_t json);
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
|
||||
el_val_t __json_parse_map(el_val_t json_str);
|
||||
el_val_t __json_stringify_val(el_val_t val);
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
el_val_t __state_set(el_val_t key, el_val_t val);
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -8,7 +8,7 @@
|
||||
* Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look
|
||||
* up El function symbols at runtime. This is the foundation of El's parallelism.
|
||||
*
|
||||
* Link: cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
* Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el_seed.c
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1202,7 +1202,7 @@ fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool
|
||||
js_emit_line(js_strip_es_exports(runtime_content))
|
||||
js_emit_line("")
|
||||
} else {
|
||||
js_emit_line("// Runtime: foundation/el/runtime/el_runtime.js")
|
||||
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
||||
js_emit_line("import \"./el_runtime.js\";")
|
||||
}
|
||||
// In module mode: destructure all builtins off globalThis.__el so call
|
||||
|
||||
+3
-3
@@ -368,13 +368,13 @@ fn main() -> Void {
|
||||
let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null"))
|
||||
if !str_eq(which_out, "") {
|
||||
let elc_dir: String = dirname_of(which_out)
|
||||
runtime_path = elc_dir + "/../runtime/el_runtime.c"
|
||||
runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c"
|
||||
}
|
||||
}
|
||||
// If --runtime points to a directory, auto-locate el_runtime.c inside it.
|
||||
// This lets both forms work:
|
||||
// --runtime=/opt/el/runtime (directory form)
|
||||
// --runtime=/opt/el/runtime/el_runtime.c (file form)
|
||||
// --runtime=/opt/el/el-compiler/runtime (directory form)
|
||||
// --runtime=/opt/el/el-compiler/runtime/el_runtime.c (file form)
|
||||
if !str_eq(runtime_path, "") {
|
||||
let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file"))
|
||||
if str_eq(is_dir, "dir") {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//
|
||||
// Compile and run:
|
||||
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
|
||||
// cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c runtime/el_runtime.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
|
||||
// /tmp/html-page
|
||||
|
||||
fn render_item(item: String) -> String {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# El Compiler Release v1.0.0 — 2026-05-02
|
||||
|
||||
## Components
|
||||
- `bootstrap.py` — El language compiler (Python, recursive descent parser, emits C)
|
||||
- `el_runtime.c` — El runtime (C, HTTP server, engram, DHARMA, LLM chain)
|
||||
- `el_runtime.h` — Runtime public API header
|
||||
|
||||
## Changes in this release
|
||||
|
||||
### Critical bug fixes
|
||||
- `state_set`/`state_get` are now thread-safe (pthread_mutex). Was racing across 64 worker threads.
|
||||
- `looks_like_string` threshold raised from 1,000,000 to 4GB. Unix timestamps were being dereferenced as heap pointers.
|
||||
- `fs_read` guards against negative `ftell` result (pipe/special file overflow).
|
||||
|
||||
### Engram architecture (major)
|
||||
- Two-layer activation: `background_activation` (Layer 1, broad fan-out) + `working_memory_weight` (Layer 2, executive filter)
|
||||
- Inhibitory edges: `EngramEdge.inhibitory` flag suppresses working memory promotion without affecting background activation
|
||||
- Suppression memory: `suppression_count` — nodes activated-but-suppressed accumulate pressure toward breakthrough
|
||||
- Temporal decay: `temporal_decay_rate`, `created_at`, `last_activated_at`, `activation_count` on EngramNode
|
||||
- Per-type activation thresholds (Safety: 0.05, Canonical: 0.15, Lesson: 0.25, Note: 0.40)
|
||||
- Temporal range query: `engram_query_range(start_ms, end_ms)`
|
||||
- Layered consciousness: `EngramLayer` struct, `layer_id` on nodes and edges, `EngramStore.layers[]`
|
||||
- Layer 0 override pass: safety layer fires last and cannot be suppressed
|
||||
|
||||
## SHA256
|
||||
bootstrap.py
|
||||
el_runtime.c
|
||||
el_runtime.h
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,12 +34,12 @@
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c runtime/el_runtime.c
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c runtime/el_runtime.c
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@@ -628,30 +628,22 @@ el_val_t engram_hebb_drain_json(el_val_t max);
|
||||
/* Document frequency of a term across node labels — term-specificity signal
|
||||
* for curiosity seed selection. (2026-08-03 self-review.) */
|
||||
el_val_t engram_label_df(el_val_t term);
|
||||
/* Best curiosity seed from one node: argmax over idf·position·casing across
|
||||
* the candidate tokens of its label, falling back to its content when the
|
||||
* label is a sentinel. Excludes pipe-delimited tabu terms during selection
|
||||
* and gates candidates to the df band [min_df, max_df]. Returns "" when
|
||||
* nothing qualifies. (2026-08-13 self-review.) */
|
||||
el_val_t engram_salient_term(el_val_t node_id, el_val_t max_df,
|
||||
el_val_t min_df, el_val_t tabu);
|
||||
el_val_t engram_embed_backfill(el_val_t count);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* Working memory introspection — count, mean weight, and top-N snapshot.
|
||||
* Ported from runtime on 2026-06-30 self-review. */
|
||||
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void);
|
||||
el_val_t engram_wm_top_json(el_val_t n);
|
||||
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
|
||||
el_val_t engram_load_merge(el_val_t path);
|
||||
|
||||
/* ── WAL + compaction + integrity (ENGRAM_WAL=on; design doc §§3-14,§18) ──── */
|
||||
int engram_wal_enabled(void);
|
||||
el_val_t engram_crc32(el_val_t s);
|
||||
el_val_t engram_wal_boot(el_val_t dir); /* replay + open; returns records */
|
||||
el_val_t engram_wal_open_dir(el_val_t dir);
|
||||
el_val_t engram_wal_node_put(el_val_t dir, el_val_t id);
|
||||
el_val_t engram_wal_edges_since(el_val_t dir, el_val_t start_count);
|
||||
el_val_t engram_wal_hebb_batch(el_val_t dir, el_val_t start_count);
|
||||
el_val_t engram_wal_forget(el_val_t dir, el_val_t id);
|
||||
el_val_t engram_wal_compact(el_val_t dir);
|
||||
el_val_t engram_wal_maybe_compact(el_val_t dir);
|
||||
el_val_t engram_resolve_data_dir(void); /* §18.2 fail-loud default */
|
||||
el_val_t engram_is_protected(el_val_t id); /* §18.1/18.3 derived set */
|
||||
el_val_t engram_protected_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Thin El wrappers over seed JSON primitives, plus pure-El builders and
|
||||
// helpers. Each function here corresponds to (and replaces) a C function
|
||||
// from runtime/el_runtime.c (lines 2692–3333).
|
||||
// from el-compiler/runtime/legacy/el_runtime.c (lines 2692–3333).
|
||||
//
|
||||
// Seed primitives consumed by this module:
|
||||
// __json_get(json, key) -> String (value as string)
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
// runtime/collections.el \
|
||||
// <user-program.el> > combined.el
|
||||
// ./dist/platform/elc combined.el > output.c
|
||||
// cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
// -o output output.c runtime/el_seed.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o output output.c el-compiler/runtime/el_seed.c
|
||||
|
||||
// This file itself is not compiled — it is documentation only.
|
||||
fn runtime_version() -> String {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// runtime/math.el — Float math, integer utilities, and numeric conversions.
|
||||
//
|
||||
// Implements the math/float surface from runtime/el_runtime.c
|
||||
// Implements the math/float surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 303–305 for el_abs/max/min, lines 4725–4771 for float/format ops)
|
||||
// in pure El, using seed primitives.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// runtime/time.el — Time operations, sleep, and formatting.
|
||||
//
|
||||
// Implements the time surface from runtime/el_runtime.c
|
||||
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 3334–3440, 3471–3656) in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
|
||||
@@ -11,7 +11,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
|
||||
@@ -10,7 +10,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
// tests/runtime/string_test.el > /tmp/string_test_combined.el
|
||||
//
|
||||
// ./dist/platform/elc /tmp/string_test_combined.el > /tmp/string_test.c
|
||||
// cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
// -o /tmp/string_test /tmp/string_test.c runtime/el_seed.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/string_test /tmp/string_test.c el-compiler/runtime/el_seed.c
|
||||
// /tmp/string_test; echo "exit: $?"
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
@@ -11,7 +11,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${ELC:-${EL_HOME}/dist/platform/elc}"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
|
||||
@@ -16,7 +16,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
|
||||
@@ -44,7 +44,7 @@ INCLUDE_DIR="${PREFIX}/include"
|
||||
LIB_DIR="${PREFIX}/lib"
|
||||
|
||||
ELC_SRC="${EL_ROOT}/dist/platform/elc"
|
||||
RUNTIME_SRC="${EL_ROOT}/runtime"
|
||||
RUNTIME_SRC="${EL_ROOT}/el-compiler/runtime"
|
||||
STDLIB_SRC="${EL_ROOT}/runtime"
|
||||
|
||||
echo "==> Installing El framework to ${PREFIX}"
|
||||
|
||||
@@ -42,7 +42,7 @@ fi
|
||||
# Discover el_runtime.c
|
||||
EL_RUNTIME=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_RUNTIME="${SCRIPT_DIR}/../runtime/el_runtime.c"
|
||||
LOCAL_RUNTIME="${SCRIPT_DIR}/../el-compiler/runtime/el_runtime.c"
|
||||
if [[ -f "${LOCAL_RUNTIME}" ]]; then
|
||||
EL_RUNTIME="$(cd "$(dirname "${LOCAL_RUNTIME}")" && pwd)/$(basename "${LOCAL_RUNTIME}")"
|
||||
EL_INCLUDE="$(dirname "${EL_RUNTIME}")"
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-single-runtime.sh — CODE-VS-ARTIFACT drift guard for the el runtime.
|
||||
#
|
||||
# Enforces org policy docs/CODE-VS-ARTIFACT.md rule #1 (single source of truth):
|
||||
# there is exactly ONE authored el_runtime.c, and it lives at lang/runtime/.
|
||||
# Any other el_runtime.c in the tree is a fork (a hand-synced copy). A lagging
|
||||
# fork is exactly what shipped to prod and dropped learned `hebb` edges on
|
||||
# restart — this guard exists to make that class of bug impossible to reintroduce.
|
||||
#
|
||||
# Exemptions:
|
||||
# * Build output — generated amalgamations under any dist/ or build/ dir are
|
||||
# artifacts, not sources.
|
||||
# * A small, explicit ALLOWLIST of pre-existing example-app vendored/staging
|
||||
# copies (see below). These are KNOWN DEFERRED DEBT, tracked separately from
|
||||
# the SDK/CI-published runtime. They do NOT ship to prod. The guard warns on
|
||||
# them (visible, greppable) but does not fail — while HARD-FAILING on any new
|
||||
# or non-allowlisted fork, including any return of lang/el-compiler/runtime/
|
||||
# or a lang/releases/ vendored copy.
|
||||
#
|
||||
# Wire-in: run from the repo root in CI (see note at bottom). Exits non-zero on drift.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
CANONICAL="lang/runtime/el_runtime.c"
|
||||
|
||||
# KNOWN DEFERRED example-app forks — remove these as a follow-up, then delete
|
||||
# this allowlist. iOS + Docker copies are regenerated by their build scripts
|
||||
# (cp from lang/runtime) and can be git-rm'd now; the Android jni/ copy is a
|
||||
# committed source its CMake build depends on and needs a build-script change
|
||||
# (cp from lang/runtime) before removal. Tracked in docs/CODE-VS-ARTIFACT.md.
|
||||
ALLOWLIST=(
|
||||
"ui/examples/native-hello-android/app/src/main/jni/el_runtime.c"
|
||||
"ui/examples/native-hello-ios/NativeHello/el_runtime.c"
|
||||
"ui/examples/native-hello/build-docker/runtime/el_runtime.c"
|
||||
)
|
||||
|
||||
is_allowlisted() {
|
||||
local p="$1"
|
||||
for a in "${ALLOWLIST[@]}"; do [ "$p" = "$a" ] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ ! -f "$CANONICAL" ]; then
|
||||
echo "FATAL: canonical runtime source missing: $CANONICAL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# All el_runtime.c files tracked by git, excluding build output (dist/ , build/)
|
||||
# and the canonical source itself.
|
||||
mapfile -t CANDIDATES < <(
|
||||
git ls-files '*el_runtime.c' \
|
||||
| grep -Ev '(^|/)(dist|build)/' \
|
||||
| grep -vx "$CANONICAL" || true
|
||||
)
|
||||
|
||||
FORKS=()
|
||||
DEFERRED=()
|
||||
for f in "${CANDIDATES[@]}"; do
|
||||
if is_allowlisted "$f"; then DEFERRED+=("$f"); else FORKS+=("$f"); fi
|
||||
done
|
||||
|
||||
if [ "${#DEFERRED[@]}" -gt 0 ]; then
|
||||
echo "WARN: allowlisted (deferred) el_runtime.c forks still present — clean these up:" >&2
|
||||
for f in "${DEFERRED[@]}"; do echo " - $f" >&2; done
|
||||
fi
|
||||
|
||||
if [ "${#FORKS[@]}" -gt 0 ]; then
|
||||
echo "FATAL: el_runtime.c fork(s) detected outside the canonical location." >&2
|
||||
echo " Canonical (the ONLY allowed source): $CANONICAL" >&2
|
||||
echo " Offending copies:" >&2
|
||||
for f in "${FORKS[@]}"; do echo " - $f" >&2; done
|
||||
echo "" >&2
|
||||
echo "Consumers must build against $CANONICAL (pin by git ref where" >&2
|
||||
echo "reproducibility matters) — never a hand-maintained copy." >&2
|
||||
echo "See docs/CODE-VS-ARTIFACT.md." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: single canonical runtime source — $CANONICAL (no un-allowlisted forks)."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CI wire-in:
|
||||
# foundation/el .gitea/workflows/ci-dev.yaml, ci-stage.yaml, sdk-release.yaml
|
||||
# Add an early step (before the build/publish steps). It must run from the
|
||||
# REPO ROOT, so override the job's `defaults.run.working-directory: lang`:
|
||||
#
|
||||
# - name: Guard - single canonical runtime source
|
||||
# working-directory: ${{ github.workspace }}
|
||||
# run: bash scripts/check-single-runtime.sh
|
||||
#
|
||||
# Also add to .githooks/pre-commit so drift is caught before it is committed.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -18,7 +18,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el"
|
||||
EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el"
|
||||
|
||||
@@ -17,7 +17,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el"
|
||||
EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el"
|
||||
|
||||
@@ -24,7 +24,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build-gtk4"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build-win32"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build"
|
||||
|
||||
@@ -193,6 +193,6 @@ run() {
|
||||
case "${1:-run}" in
|
||||
clean) clean ;;
|
||||
compile) compile ;;
|
||||
platforms) "${EL_LANG_ROOT}/runtime/detect-platforms" ;;
|
||||
platforms) "${EL_LANG_ROOT}/el-compiler/runtime/detect-platforms" ;;
|
||||
run|*) run ;;
|
||||
esac
|
||||
|
||||
@@ -32,7 +32,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
|
||||
EL_UI_ROOT="${SCRIPT_DIR}/../.."
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/runtime"
|
||||
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
|
||||
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build"
|
||||
DOCKER_CTX="${SCRIPT_DIR}/build-docker"
|
||||
|
||||
Reference in New Issue
Block a user