Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f83adf458 | |||
| 9f734b037c | |||
| 049a7712f4 | |||
| c64cbd21e2 | |||
| 37488e9485 | |||
| 8641b4045e | |||
| 49d68fbb20 | |||
| 77a0658d56 | |||
| bff0ad4f22 | |||
| 49f96126b2 | |||
| c954142063 | |||
| 3fd5fec965 | |||
| 5476cbb2b1 | |||
| 65792f7e4c | |||
| c09023003d | |||
| 15b9ccd9e2 | |||
| 9163af81aa | |||
| 3dababa4ad | |||
| 5888258c9f | |||
| a9dc38ed82 | |||
| 4af2b687e1 | |||
| 32f0cf7b5d | |||
| 65e26cd7a5 | |||
| 1fd7cd5545 | |||
| 71689520b6 | |||
| e858eab300 | |||
| aa7d97d5ba | |||
| 7040830470 | |||
| 3a513aaa5a | |||
| beb2a8c5bd | |||
| e23319fe0b | |||
| 01fee9396a | |||
| 7b60d94b8a | |||
| 21694b79d2 | |||
| 422442b14e | |||
| 437ba0a4dd | |||
| 7376349124 | |||
| 0f1da43a97 | |||
| a54b2bebf9 |
+45
-255
@@ -1,4 +1,4 @@
|
||||
name: El SDK CI - dev
|
||||
name: El CI -dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,13 +7,11 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -22,306 +20,98 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
> /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
# -Wl,--allow-multiple-definition: is_digit/is_whitespace exist in both
|
||||
# elc-bootstrap.c (pre-dates runtime text primitives) and el_runtime.c.
|
||||
# Both definitions are equivalent; allow the linker to pick one.
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb (needed for Artifact Registry publish and downstream CI)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
- name: Run tests - text
|
||||
# Run all four test suites -all must pass
|
||||
- name: Run tests -text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests - calendar
|
||||
- name: Run tests -calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests - time
|
||||
- name: Run tests -time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests - html_sanitizer
|
||||
- name: Run tests -html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
- name: Run tests -native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
if: github.event_name == 'push'
|
||||
# Publish artifact to GCP Artifact Registry (dev)
|
||||
- name: Publish elc to Artifact Registry (dev)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (dev)
|
||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:dev (or fall back to :latest on first run)
|
||||
BASE_TAG="dev"
|
||||
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:dev" \
|
||||
-t "${CI_BASE}:dev-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:dev"
|
||||
docker push "${CI_BASE}:dev-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
|
||||
# Also tag as latest-dev
|
||||
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
+34
-245
@@ -1,4 +1,4 @@
|
||||
name: El SDK CI - stage
|
||||
name: El CI — stage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,301 +11,90 @@ on:
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (stage <- dev only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "dev" ]; then
|
||||
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> stage"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests - text
|
||||
# Run all four test suites — all must pass
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests - calendar
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests - time
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests - html_sanitizer
|
||||
- name: Run tests — html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build elb (needed for epm and el-install builds below)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
if: github.event_name == 'push'
|
||||
# Publish artifact to GCP Artifact Registry (stage)
|
||||
- name: Publish elc to Artifact Registry (stage)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (stage)
|
||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:stage (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:stage" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:stage" \
|
||||
-t "${CI_BASE}:stage-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:stage"
|
||||
docker push "${CI_BASE}:stage-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
|
||||
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -4,234 +4,81 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: lang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enforce source branch (main <- stage only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
SOURCE="${GITHUB_HEAD_REF}"
|
||||
if [ "${SOURCE}" != "stage" ]; then
|
||||
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> main"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb binary
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
- name: Run tests - text
|
||||
# Run all four test suites with gen3 elc
|
||||
- name: Run tests — text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests - calendar
|
||||
- name: Run tests — calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests - time
|
||||
- name: Run tests — time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests - html_sanitizer
|
||||
- name: Run tests — html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
|
||||
- name: Bundle SDK tarball
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
mkdir -p dist/sdk/bin dist/sdk/runtime
|
||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||
cp lang/el-compiler/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/el-compiler/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/*.el dist/sdk/runtime/
|
||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||
ls -lh dist/el-sdk-latest.tar.gz
|
||||
|
||||
# Publish / update the `latest` release with all SDK assets
|
||||
# Publish / update the `latest` release with the three SDK assets
|
||||
- name: Publish latest release
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
REPO: neuron-technologies/el
|
||||
run: |
|
||||
# Delete existing `latest` release if it exists
|
||||
EXISTING_ID=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/releases/tags/latest" \
|
||||
@@ -244,10 +91,12 @@ jobs:
|
||||
"${GITEA_API}/repos/${REPO}/releases/${EXISTING_ID}"
|
||||
fi
|
||||
|
||||
# Delete and re-create the `latest` tag so it points at HEAD
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/tags/latest" || true
|
||||
|
||||
# Create the release
|
||||
RELEASE_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -262,6 +111,7 @@ jobs:
|
||||
|
||||
echo "Created release id=${RELEASE_ID}"
|
||||
|
||||
# Upload assets
|
||||
upload_asset() {
|
||||
local filepath="$1"
|
||||
local name="$2"
|
||||
@@ -272,149 +122,70 @@ jobs:
|
||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
||||
}
|
||||
|
||||
# Per-file assets (downstream CI needs these individually)
|
||||
upload_asset lang/dist/platform/elc elc
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.h el_runtime.h
|
||||
|
||||
# SDK bundle and installer binary
|
||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||
upload_asset lang/dist/bin/el-install el-install
|
||||
upload_asset dist/platform/elc elc
|
||||
upload_asset el-compiler/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset el-compiler/runtime/el_runtime.h el_runtime.h
|
||||
|
||||
echo "Release published successfully"
|
||||
|
||||
- name: Publish El SDK to Artifact Registry (prod)
|
||||
if: github.event_name == 'push'
|
||||
# Dispatch el-sdk-updated event to downstream repos
|
||||
# Publish artifact to GCP Artifact Registry (prod)
|
||||
- name: Publish elc to Artifact Registry (prod)
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK
|
||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:latest" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:latest" \
|
||||
-t "${CI_BASE}:${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:latest"
|
||||
docker push "${CI_BASE}:${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
|
||||
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
- name: Dispatch el-sdk-updated to downstream repos
|
||||
if: github.event_name == 'push'
|
||||
- name: Dispatch to foundation/engram
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/${repo}/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {\"el_version\": \"latest\", \"commit\": \"${GITHUB_SHA}\"}
|
||||
}" && echo "Dispatched to ${repo}" || echo "Warning: dispatch to ${repo} failed"
|
||||
done
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/engram/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to foundation/engram"
|
||||
|
||||
- name: Dispatch to neuron-technologies/forge
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_API}/repos/neuron-technologies/forge/dispatches" \
|
||||
-d "{
|
||||
\"type\": \"el-sdk-updated\",
|
||||
\"inputs\": {
|
||||
\"el_version\": \"latest\",
|
||||
\"commit\": \"${GITHUB_SHA}\"
|
||||
}
|
||||
}"
|
||||
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# El pre-commit hook: compile and run native tests before commit.
|
||||
# Install once per clone: git config core.hooksPath .githooks
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
LANG_DIR="$ROOT/lang"
|
||||
RUNTIME="$LANG_DIR/el-compiler/runtime"
|
||||
ELC="$LANG_DIR/dist/platform/elc"
|
||||
|
||||
# If elc isn't built yet, skip with a warning rather than blocking
|
||||
if [ ! -x "$ELC" ]; then
|
||||
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
|
||||
echo " Build it first: cd lang && gcc -O2 -I el-compiler/runtime dist/elc-bootstrap.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I el-compiler/runtime /tmp/elc.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "→ Running El native tests..."
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_TESTS=""
|
||||
|
||||
for test_file in "$LANG_DIR"/tests/native/test_*.el; do
|
||||
name=$(basename "$test_file" .el)
|
||||
tmp_c="/tmp/el_hook_${name}.c"
|
||||
tmp_bin="/tmp/el_hook_${name}"
|
||||
|
||||
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
|
||||
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||
&& "$tmp_bin" 2>/dev/null; then
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " ✗ $name"
|
||||
FAIL=$((FAIL + 1))
|
||||
FAILED_TESTS="$FAILED_TESTS $name"
|
||||
fi
|
||||
done
|
||||
|
||||
echo " $PASS passed, $FAIL failed"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "✗ Pre-commit failed. Fix these tests before committing:$FAILED_TESTS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ All tests passed"
|
||||
exit 0
|
||||
@@ -1,23 +0,0 @@
|
||||
// arbor-cli — the `arbor` command-line tool.
|
||||
// Inlines its own copies of the parse / layout / render pipeline so that the
|
||||
// resulting binary is self-contained. (El's `import` form today concatenates
|
||||
// source; once a real module loader lands this becomes a thin driver.)
|
||||
|
||||
vessel "arbor-cli" {
|
||||
version "0.1.0"
|
||||
description "Command-line interface for the Arbor diagram language"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
arbor-core "0.1"
|
||||
arbor-parse "0.1"
|
||||
arbor-layout "0.1"
|
||||
arbor-render "0.1"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
// arbor-core — fundamental types for Arbor diagrams.
|
||||
// Node IDs (sanitised), shape vocabulary, edge kinds, and the lightweight
|
||||
// graph value used by every other vessel.
|
||||
|
||||
vessel "arbor-core" {
|
||||
version "0.1.0"
|
||||
description "Core types for Arbor diagrams: NodeId, ArborShape, ArborEdgeKind, graphs"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
// arbor-core — core types for Arbor diagrams.
|
||||
//
|
||||
// Idiomatic El: everything is a Map. Functions take/return maps; helpers are
|
||||
// pure and small. The downstream vessels (parse, layout, render) consume the
|
||||
// shapes defined here.
|
||||
//
|
||||
// Shape vocabulary:
|
||||
// ArborShape strings — "rect" "rounded" "cylinder" "diamond" "stadium" "primary"
|
||||
//
|
||||
// Edge-kind strings:
|
||||
// "solid" "dashed" "forbidden" "bidirectional"
|
||||
//
|
||||
// Node value: { "id":Str, "label":Str, "shape":Str }
|
||||
// Edge value: { "from":Str, "to":Str, "label":Str, "kind":Str }
|
||||
// Group value: { "id":Str, "label":Str, "node_ids":[Str], "direction":Str }
|
||||
// Graph value: { "title":Str, "direction":Str, "nodes":[Node], "edges":[Edge], "groups":[Group] }
|
||||
//
|
||||
// Diagram-form (lowered) is the same shape but with NodeStyle/EdgeLine/Arrow
|
||||
// resolved into renderer-friendly fields:
|
||||
// Node: + "sublabel":Str, "style_fill":Str, "style_stroke":Str, "style_color":Str
|
||||
// Edge: + "line":Str ("solid"/"dashed"/"dotted"/"thick"), "arrow":Str ("forward"/"backward"/"both"/"none")
|
||||
//
|
||||
// This file is the canonical definition of those shapes. Other vessels rely on
|
||||
// these field names.
|
||||
|
||||
// ── NodeId sanitisation ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Sanitise an arbitrary string into a Mermaid-safe identifier.
|
||||
// - any char not in [a-zA-Z0-9_] becomes '_'
|
||||
// - consecutive underscores collapse
|
||||
// - trailing underscores stripped
|
||||
// - if first char is a digit, prepend 'n'
|
||||
// - if empty, return "node"
|
||||
|
||||
fn is_alnum_underscore(ch: String) -> Bool {
|
||||
let code: Int = str_char_code(ch, 0)
|
||||
if code >= 48 {
|
||||
if code <= 57 { return true }
|
||||
}
|
||||
if code >= 65 {
|
||||
if code <= 90 { return true }
|
||||
}
|
||||
if code >= 97 {
|
||||
if code <= 122 { return true }
|
||||
}
|
||||
if code == 95 { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_ascii_digit(ch: String) -> Bool {
|
||||
let code: Int = str_char_code(ch, 0)
|
||||
if code >= 48 {
|
||||
if code <= 57 { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn sanitize_id(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
if n == 0 { return "node" }
|
||||
|
||||
// Pass 1: replace and collapse.
|
||||
let out = ""
|
||||
let prev_underscore = false
|
||||
let i = 0
|
||||
while i < n {
|
||||
let ch: String = str_char_at(s, i)
|
||||
if is_alnum_underscore(ch) {
|
||||
let out = out + ch
|
||||
let prev_underscore = false
|
||||
} else {
|
||||
if !prev_underscore {
|
||||
let out = out + "_"
|
||||
}
|
||||
let prev_underscore = true
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Pass 2: strip trailing underscores.
|
||||
let m: Int = str_len(out)
|
||||
let end = m
|
||||
let stripping = true
|
||||
while stripping {
|
||||
if end <= 0 {
|
||||
let stripping = false
|
||||
} else {
|
||||
let last: String = str_char_at(out, end - 1)
|
||||
if last == "_" {
|
||||
let end = end - 1
|
||||
} else {
|
||||
let stripping = false
|
||||
}
|
||||
}
|
||||
}
|
||||
let out = str_slice(out, 0, end)
|
||||
|
||||
if str_len(out) == 0 { return "node" }
|
||||
|
||||
// Pass 3: leading-digit guard.
|
||||
let first: String = str_char_at(out, 0)
|
||||
if is_ascii_digit(first) {
|
||||
let out = "n" + out
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── Constructors ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_node(id: String, label: String, shape: String) -> Map<String, Any> {
|
||||
{ "id": id, "label": label, "shape": shape }
|
||||
}
|
||||
|
||||
fn make_edge(src: String, dst: String, kind: String) -> Map<String, Any> {
|
||||
{ "from": src, "to": dst, "label": "", "kind": kind }
|
||||
}
|
||||
|
||||
fn make_edge_with_label(src: String, dst: String, kind: String, label: String) -> Map<String, Any> {
|
||||
{ "from": src, "to": dst, "label": label, "kind": kind }
|
||||
}
|
||||
|
||||
fn make_group(id: String, label: String) -> Map<String, Any> {
|
||||
let empty_ids: [String] = el_list_empty()
|
||||
{ "id": id, "label": label, "node_ids": empty_ids, "direction": "" }
|
||||
}
|
||||
|
||||
fn make_graph() -> Map<String, Any> {
|
||||
let empty_n: [Map<String, Any>] = el_list_empty()
|
||||
let empty_e: [Map<String, Any>] = el_list_empty()
|
||||
let empty_g: [Map<String, Any>] = el_list_empty()
|
||||
{ "title": "", "direction": "top-down",
|
||||
"nodes": empty_n, "edges": empty_e, "groups": empty_g }
|
||||
}
|
||||
|
||||
// ── Shape vocabulary ──────────────────────────────────────────────────────────
|
||||
// Returns the canonical shape string for a token, or "" if unknown.
|
||||
|
||||
fn shape_from_token(tok: String) -> String {
|
||||
let t: String = str_trim(tok)
|
||||
if t == "rect" { return "rect" }
|
||||
if t == "rounded" { return "rounded" }
|
||||
if t == "cylinder" { return "cylinder" }
|
||||
if t == "diamond" { return "diamond" }
|
||||
if t == "stadium" { return "stadium" }
|
||||
if t == "primary" { return "primary" }
|
||||
""
|
||||
}
|
||||
|
||||
// Lower an Arbor shape into the renderer's NodeShape vocabulary.
|
||||
fn shape_to_node_shape(shape: String) -> String {
|
||||
if shape == "rect" { return "rectangle" }
|
||||
if shape == "primary" { return "rectangle" }
|
||||
if shape == "rounded" { return "rounded_rect" }
|
||||
if shape == "cylinder" { return "cylinder" }
|
||||
if shape == "diamond" { return "diamond" }
|
||||
if shape == "stadium" { return "stadium" }
|
||||
"rectangle"
|
||||
}
|
||||
|
||||
// ── Lowering: ArborGraph → DiagramGraph ──────────────────────────────────────
|
||||
//
|
||||
// Replaces every node with a diagram-form node carrying explicit style fields,
|
||||
// and every edge with a diagram-form edge carrying line/arrow strings.
|
||||
|
||||
fn lower_node(n: Map<String, Any>) -> Map<String, Any> {
|
||||
let shape: String = n["shape"]
|
||||
let node_shape: String = shape_to_node_shape(shape)
|
||||
let fill = ""
|
||||
let stroke = ""
|
||||
let color = ""
|
||||
if shape == "primary" {
|
||||
let fill = "#0052A0"
|
||||
let stroke = "#0052A0"
|
||||
let color = "#ffffff"
|
||||
}
|
||||
{ "id": n["id"], "label": n["label"], "sublabel": "",
|
||||
"shape": node_shape,
|
||||
"style_fill": fill, "style_stroke": stroke, "style_color": color }
|
||||
}
|
||||
|
||||
fn lower_edge(e: Map<String, Any>) -> Map<String, Any> {
|
||||
let kind: String = e["kind"]
|
||||
let line = "solid"
|
||||
let arrow = "forward"
|
||||
if kind == "dashed" {
|
||||
let line = "dashed"
|
||||
}
|
||||
if kind == "bidirectional" {
|
||||
let arrow = "both"
|
||||
}
|
||||
// forbidden uses solid line + forward arrow; the renderer overlays the
|
||||
// circle-X marker based on a forbidden-set the caller threads through.
|
||||
{ "from": e["from"], "to": e["to"], "label": e["label"],
|
||||
"line": line, "arrow": arrow }
|
||||
}
|
||||
|
||||
fn lower_graph(g: Map<String, Any>) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = g["nodes"]
|
||||
let edges: [Map<String, Any>] = g["edges"]
|
||||
let lowered_nodes: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
let n: Int = el_list_len(nodes)
|
||||
while i < n {
|
||||
let lowered_nodes = native_list_append(lowered_nodes, lower_node(get(nodes, i)))
|
||||
let i = i + 1
|
||||
}
|
||||
let lowered_edges: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
let m: Int = el_list_len(edges)
|
||||
while i < m {
|
||||
let lowered_edges = native_list_append(lowered_edges, lower_edge(get(edges, i)))
|
||||
let i = i + 1
|
||||
}
|
||||
{ "title": g["title"], "direction": g["direction"],
|
||||
"nodes": lowered_nodes, "edges": lowered_edges, "groups": g["groups"] }
|
||||
}
|
||||
|
||||
// Find a node by id within a (lowered or raw) graph. Returns an empty map
|
||||
// when not found — callers check map_get(result, "id") for presence.
|
||||
fn graph_find_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let n: Int = el_list_len(nodes)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let node: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = node["id"]
|
||||
if nid == id { return node }
|
||||
let i = i + 1
|
||||
}
|
||||
let empty: Map<String, Any> = el_map_new(0)
|
||||
empty
|
||||
}
|
||||
|
||||
// ── Forbidden-edge set helpers ────────────────────────────────────────────────
|
||||
// The lowered graph drops the "forbidden" kind (line/arrow have no slot for
|
||||
// it). Callers preserve the set as a list of "from->to" strings.
|
||||
|
||||
fn forbidden_key(from: String, to: String) -> String {
|
||||
from + "->" + to
|
||||
}
|
||||
|
||||
fn collect_forbidden(graph: Map<String, Any>) -> [String] {
|
||||
let edges: [Map<String, Any>] = graph["edges"]
|
||||
let n: Int = el_list_len(edges)
|
||||
let out: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let e: Map<String, Any> = get(edges, i)
|
||||
let kind: String = e["kind"]
|
||||
if kind == "forbidden" {
|
||||
let f: String = e["from"]
|
||||
let t: String = e["to"]
|
||||
let out = native_list_append(out, forbidden_key(f, t))
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
|
||||
let key: String = forbidden_key(src, dst)
|
||||
let n: Int = el_list_len(set)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let s: String = get(set, i)
|
||||
if s == key { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── Smoke test ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// State is kept in process-local k/v storage so we never mix Int + Call or
|
||||
// Int + Ident in `+` (which the codegen heuristic emits as string concat
|
||||
// on tagged-pointer values, segfaulting on Int operands).
|
||||
|
||||
fn fail(label: String, got: String, want: String) -> Int {
|
||||
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
|
||||
state_set("failures", "1")
|
||||
0
|
||||
}
|
||||
|
||||
fn check_eq(label: String, got: String, want: String) -> Int {
|
||||
if got == want {
|
||||
println("ok " + label + " = " + got)
|
||||
return 1
|
||||
}
|
||||
fail(label, got, want)
|
||||
}
|
||||
|
||||
check_eq("sanitize crates/nc-core",
|
||||
sanitize_id("crates/nc-core"), "crates_nc_core")
|
||||
|
||||
check_eq("sanitize package.json",
|
||||
sanitize_id("package.json"), "package_json")
|
||||
|
||||
check_eq("sanitize 42-module",
|
||||
sanitize_id("42-module"), "n42_module")
|
||||
|
||||
check_eq("sanitize empty", sanitize_id(""), "node")
|
||||
|
||||
check_eq("sanitize !!--@@", sanitize_id("!!--@@"), "node")
|
||||
|
||||
check_eq("shape_from_token rounded",
|
||||
shape_from_token("rounded"), "rounded")
|
||||
|
||||
check_eq("shape_to_node_shape primary",
|
||||
shape_to_node_shape("primary"), "rectangle")
|
||||
|
||||
// Lowering preserves a node id and adds style.
|
||||
let n: Map<String, Any> = make_node("svc", "Service", "primary")
|
||||
let ln: Map<String, Any> = lower_node(n)
|
||||
check_eq("lower preserves id", ln["id"], "svc")
|
||||
check_eq("lower applies primary fill", ln["style_fill"], "#0052A0")
|
||||
|
||||
// Edge lowering
|
||||
let e: Map<String, Any> = make_edge("a", "b", "dashed")
|
||||
let le: Map<String, Any> = lower_edge(e)
|
||||
check_eq("lower edge dashed line", le["line"], "dashed")
|
||||
|
||||
let e2: Map<String, Any> = make_edge("a", "b", "bidirectional")
|
||||
let le2: Map<String, Any> = lower_edge(e2)
|
||||
check_eq("lower edge bidirectional arrow", le2["arrow"], "both")
|
||||
|
||||
println("")
|
||||
let failures: String = state_get("failures")
|
||||
if str_eq(failures, "1") {
|
||||
println("arbor-core: FAILED")
|
||||
exit_program(1)
|
||||
} else {
|
||||
println("arbor-core: ok")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// arbor-diagram — diagram intermediate representation + Mermaid serializer
|
||||
// + dependency-graph builders. Consumes raw graph values built by arbor-core
|
||||
// or arbor-parse and produces Mermaid markup or other serializations.
|
||||
|
||||
vessel "arbor-diagram" {
|
||||
version "0.1.0"
|
||||
description "Diagram IR + Mermaid serializer + architecture diagram builders"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
arbor-core "0.1"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
// arbor-diagram — diagram intermediate representation (AST + IR).
|
||||
//
|
||||
// Where arbor-core supplies the *.arbor source-language model — Mermaid-safe
|
||||
// IDs, ArborShape strings, ArborEdgeKind strings, and the lowered "diagram-
|
||||
// form" map — arbor-diagram exposes the same lowered model as the canonical
|
||||
// IR for downstream serializers (arbor-render and any future Mermaid-style
|
||||
// emitter). The two vessels overlap by design: arbor-core is responsible for
|
||||
// *naming* the schema; arbor-diagram is responsible for *building* values
|
||||
// against it.
|
||||
//
|
||||
// The Rust crate ships small AST builder structs (`DiagramNode::new`,
|
||||
// `DiagramEdge::with_label`, `DiagramGraph::add_node`). El has no method
|
||||
// chaining, no Default::default(), no enum types. The El idiom is a stack
|
||||
// of immutable maps with explicit constructor + with_* helpers that take
|
||||
// the value and return a freshly-allocated map.
|
||||
//
|
||||
// Public surface:
|
||||
// make_node(id, label) → DiagramNode
|
||||
// with_shape(node, shape) → DiagramNode
|
||||
// with_sublabel(node, sublabel) → DiagramNode
|
||||
// with_style(node, fill, stroke, color) → DiagramNode
|
||||
//
|
||||
// make_edge(from, to) → DiagramEdge
|
||||
// with_label(edge, label)
|
||||
// with_line(edge, line) // "solid"/"dashed"/"dotted"/"thick"
|
||||
// with_arrow(edge, arrow) // "forward"/"backward"/"both"/"none"
|
||||
//
|
||||
// make_group(id, label) → DiagramGroup
|
||||
// with_node(group, node_id)
|
||||
// with_nodes(group, [node_id])
|
||||
// with_direction(group, dir)
|
||||
//
|
||||
// make_graph(title) → DiagramGraph
|
||||
// with_direction(graph, dir)
|
||||
// graph_add_node(graph, node) → DiagramGraph
|
||||
// graph_add_edge(graph, edge) → DiagramGraph
|
||||
// graph_add_group(graph, group) → DiagramGraph
|
||||
// graph_node(graph, id) → DiagramNode | empty map
|
||||
//
|
||||
// Shape vocabulary (lowered): see arbor-core. The local copy here mirrors
|
||||
// the table in arbor-core/src/main.el so this vessel is hermetic.
|
||||
|
||||
// ── NodeShape vocabulary ────────────────────────────────────────────────────
|
||||
|
||||
fn node_shape_rectangle() -> String { "rectangle" }
|
||||
fn node_shape_rounded_rect() -> String { "rounded_rect" }
|
||||
fn node_shape_stadium() -> String { "stadium" }
|
||||
fn node_shape_cylinder() -> String { "cylinder" }
|
||||
fn node_shape_diamond() -> String { "diamond" }
|
||||
fn node_shape_parallelogram() -> String { "parallelogram" }
|
||||
fn node_shape_database() -> String { "database" }
|
||||
fn node_shape_subroutine() -> String { "subroutine" }
|
||||
|
||||
fn node_shape_valid(s: String) -> Bool {
|
||||
if str_eq(s, "rectangle") { return true }
|
||||
if str_eq(s, "rounded_rect") { return true }
|
||||
if str_eq(s, "stadium") { return true }
|
||||
if str_eq(s, "cylinder") { return true }
|
||||
if str_eq(s, "diamond") { return true }
|
||||
if str_eq(s, "parallelogram") { return true }
|
||||
if str_eq(s, "database") { return true }
|
||||
if str_eq(s, "subroutine") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── EdgeLine vocabulary ─────────────────────────────────────────────────────
|
||||
|
||||
fn edge_line_solid() -> String { "solid" }
|
||||
fn edge_line_dashed() -> String { "dashed" }
|
||||
fn edge_line_dotted() -> String { "dotted" }
|
||||
fn edge_line_thick() -> String { "thick" }
|
||||
|
||||
fn edge_line_valid(s: String) -> Bool {
|
||||
if str_eq(s, "solid") { return true }
|
||||
if str_eq(s, "dashed") { return true }
|
||||
if str_eq(s, "dotted") { return true }
|
||||
if str_eq(s, "thick") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── EdgeArrow vocabulary ────────────────────────────────────────────────────
|
||||
|
||||
fn edge_arrow_forward() -> String { "forward" }
|
||||
fn edge_arrow_backward() -> String { "backward" }
|
||||
fn edge_arrow_both() -> String { "both" }
|
||||
fn edge_arrow_none() -> String { "none" }
|
||||
|
||||
fn edge_arrow_valid(s: String) -> Bool {
|
||||
if str_eq(s, "forward") { return true }
|
||||
if str_eq(s, "backward") { return true }
|
||||
if str_eq(s, "both") { return true }
|
||||
if str_eq(s, "none") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Direction vocabulary ────────────────────────────────────────────────────
|
||||
|
||||
fn direction_top_down() -> String { "top-down" }
|
||||
fn direction_left_right() -> String { "left-right" }
|
||||
fn direction_right_left() -> String { "right-left" }
|
||||
fn direction_bottom_up() -> String { "bottom-up" }
|
||||
|
||||
fn direction_valid(s: String) -> Bool {
|
||||
if str_eq(s, "top-down") { return true }
|
||||
if str_eq(s, "left-right") { return true }
|
||||
if str_eq(s, "right-left") { return true }
|
||||
if str_eq(s, "bottom-up") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── DiagramNode ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_node(id: String, label: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": id,
|
||||
"label": label,
|
||||
"sublabel": "",
|
||||
"shape": "rectangle",
|
||||
"style_fill": "",
|
||||
"style_stroke": "",
|
||||
"style_color": ""
|
||||
}
|
||||
}
|
||||
|
||||
fn with_shape(node: Map<String, Any>, shape: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": node["id"],
|
||||
"label": node["label"],
|
||||
"sublabel": node["sublabel"],
|
||||
"shape": shape,
|
||||
"style_fill": node["style_fill"],
|
||||
"style_stroke": node["style_stroke"],
|
||||
"style_color": node["style_color"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_sublabel(node: Map<String, Any>, sublabel: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": node["id"],
|
||||
"label": node["label"],
|
||||
"sublabel": sublabel,
|
||||
"shape": node["shape"],
|
||||
"style_fill": node["style_fill"],
|
||||
"style_stroke": node["style_stroke"],
|
||||
"style_color": node["style_color"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_style(node: Map<String, Any>, fill: String, stroke: String, color: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": node["id"],
|
||||
"label": node["label"],
|
||||
"sublabel": node["sublabel"],
|
||||
"shape": node["shape"],
|
||||
"style_fill": fill,
|
||||
"style_stroke": stroke,
|
||||
"style_color": color
|
||||
}
|
||||
}
|
||||
|
||||
// ── DiagramEdge ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_edge(from: String, to: String) -> Map<String, Any> {
|
||||
{
|
||||
"from": from,
|
||||
"to": to,
|
||||
"label": "",
|
||||
"line": "solid",
|
||||
"arrow": "forward"
|
||||
}
|
||||
}
|
||||
|
||||
fn with_label(edge: Map<String, Any>, label: String) -> Map<String, Any> {
|
||||
{
|
||||
"from": edge["from"],
|
||||
"to": edge["to"],
|
||||
"label": label,
|
||||
"line": edge["line"],
|
||||
"arrow": edge["arrow"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_line(edge: Map<String, Any>, line: String) -> Map<String, Any> {
|
||||
{
|
||||
"from": edge["from"],
|
||||
"to": edge["to"],
|
||||
"label": edge["label"],
|
||||
"line": line,
|
||||
"arrow": edge["arrow"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_arrow(edge: Map<String, Any>, arrow: String) -> Map<String, Any> {
|
||||
{
|
||||
"from": edge["from"],
|
||||
"to": edge["to"],
|
||||
"label": edge["label"],
|
||||
"line": edge["line"],
|
||||
"arrow": arrow
|
||||
}
|
||||
}
|
||||
|
||||
// ── DiagramGroup ────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_group(id: String, label: String) -> Map<String, Any> {
|
||||
let empty: [String] = native_list_empty()
|
||||
{
|
||||
"id": id,
|
||||
"label": label,
|
||||
"node_ids": empty,
|
||||
"direction": ""
|
||||
}
|
||||
}
|
||||
|
||||
fn with_node(group: Map<String, Any>, node_id: String) -> Map<String, Any> {
|
||||
let cur: [String] = group["node_ids"]
|
||||
let next: [String] = native_list_append(cur, node_id)
|
||||
{
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"node_ids": next,
|
||||
"direction": group["direction"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_nodes(group: Map<String, Any>, ids: [String]) -> Map<String, Any> {
|
||||
let cur: [String] = group["node_ids"]
|
||||
let n: Int = el_list_len(ids)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let cur = native_list_append(cur, get(ids, i))
|
||||
let i = i + 1
|
||||
}
|
||||
{
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"node_ids": cur,
|
||||
"direction": group["direction"]
|
||||
}
|
||||
}
|
||||
|
||||
fn with_group_direction(group: Map<String, Any>, dir: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"node_ids": group["node_ids"],
|
||||
"direction": dir
|
||||
}
|
||||
}
|
||||
|
||||
// ── DiagramGraph ────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_graph(title: String) -> Map<String, Any> {
|
||||
let empty_n: [Map<String, Any>] = native_list_empty()
|
||||
let empty_e: [Map<String, Any>] = native_list_empty()
|
||||
let empty_g: [Map<String, Any>] = native_list_empty()
|
||||
{
|
||||
"title": title,
|
||||
"direction": "top-down",
|
||||
"nodes": empty_n,
|
||||
"edges": empty_e,
|
||||
"groups": empty_g
|
||||
}
|
||||
}
|
||||
|
||||
fn with_direction(graph: Map<String, Any>, dir: String) -> Map<String, Any> {
|
||||
{
|
||||
"title": graph["title"],
|
||||
"direction": dir,
|
||||
"nodes": graph["nodes"],
|
||||
"edges": graph["edges"],
|
||||
"groups": graph["groups"]
|
||||
}
|
||||
}
|
||||
|
||||
fn graph_add_node(graph: Map<String, Any>, node: Map<String, Any>) -> Map<String, Any> {
|
||||
let cur: [Map<String, Any>] = graph["nodes"]
|
||||
let next: [Map<String, Any>] = native_list_append(cur, node)
|
||||
{
|
||||
"title": graph["title"],
|
||||
"direction": graph["direction"],
|
||||
"nodes": next,
|
||||
"edges": graph["edges"],
|
||||
"groups": graph["groups"]
|
||||
}
|
||||
}
|
||||
|
||||
fn graph_add_edge(graph: Map<String, Any>, edge: Map<String, Any>) -> Map<String, Any> {
|
||||
let cur: [Map<String, Any>] = graph["edges"]
|
||||
let next: [Map<String, Any>] = native_list_append(cur, edge)
|
||||
{
|
||||
"title": graph["title"],
|
||||
"direction": graph["direction"],
|
||||
"nodes": graph["nodes"],
|
||||
"edges": next,
|
||||
"groups": graph["groups"]
|
||||
}
|
||||
}
|
||||
|
||||
fn graph_add_group(graph: Map<String, Any>, group: Map<String, Any>) -> Map<String, Any> {
|
||||
let cur: [Map<String, Any>] = graph["groups"]
|
||||
let next: [Map<String, Any>] = native_list_append(cur, group)
|
||||
{
|
||||
"title": graph["title"],
|
||||
"direction": graph["direction"],
|
||||
"nodes": graph["nodes"],
|
||||
"edges": graph["edges"],
|
||||
"groups": next
|
||||
}
|
||||
}
|
||||
|
||||
// Find a node by id. Returns an empty map (no "id" field) when not present.
|
||||
fn graph_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let n: Int = el_list_len(nodes)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
if str_eq(nid, id) { return nd }
|
||||
let i = i + 1
|
||||
}
|
||||
let empty: Map<String, Any> = el_map_new(0)
|
||||
empty
|
||||
}
|
||||
|
||||
// ── Smoke test ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn fail(label: String, got: String, want: String) -> Int {
|
||||
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
|
||||
state_set("smoke_failures", "1")
|
||||
0
|
||||
}
|
||||
|
||||
fn check_eq(label: String, got: String, want: String) -> Int {
|
||||
if got == want {
|
||||
println("ok " + label + " = " + got)
|
||||
return 1
|
||||
}
|
||||
fail(label, got, want)
|
||||
}
|
||||
|
||||
// Vocabulary self-checks
|
||||
check_eq("shape rectangle valid",
|
||||
bool_to_str(node_shape_valid("rectangle")), "true")
|
||||
check_eq("shape hexagon invalid",
|
||||
bool_to_str(node_shape_valid("hexagon")), "false")
|
||||
check_eq("line dashed valid",
|
||||
bool_to_str(edge_line_valid("dashed")), "true")
|
||||
check_eq("arrow both valid",
|
||||
bool_to_str(edge_arrow_valid("both")), "true")
|
||||
check_eq("dir top-down valid",
|
||||
bool_to_str(direction_valid("top-down")), "true")
|
||||
|
||||
// Node builder
|
||||
let n0: Map<String, Any> = make_node("svc", "Service")
|
||||
check_eq("node default shape", n0["shape"], "rectangle")
|
||||
check_eq("node default sublabel empty", n0["sublabel"], "")
|
||||
|
||||
let n1: Map<String, Any> = with_shape(n0, "cylinder")
|
||||
check_eq("node with_shape", n1["shape"], "cylinder")
|
||||
check_eq("node id preserved", n1["id"], "svc")
|
||||
|
||||
let n2: Map<String, Any> = with_sublabel(n1, "v0.1.0")
|
||||
check_eq("node with_sublabel", n2["sublabel"], "v0.1.0")
|
||||
|
||||
let n3: Map<String, Any> = with_style(n2, "#0052A0", "#0052A0", "#ffffff")
|
||||
check_eq("node style fill", n3["style_fill"], "#0052A0")
|
||||
check_eq("node style color", n3["style_color"], "#ffffff")
|
||||
|
||||
// Edge builder
|
||||
let e0: Map<String, Any> = make_edge("a", "b")
|
||||
check_eq("edge default line", e0["line"], "solid")
|
||||
check_eq("edge default arrow", e0["arrow"], "forward")
|
||||
let e1: Map<String, Any> = with_line(e0, "dashed")
|
||||
let e2: Map<String, Any> = with_arrow(e1, "both")
|
||||
let e3: Map<String, Any> = with_label(e2, "calls")
|
||||
check_eq("edge line", e3["line"], "dashed")
|
||||
check_eq("edge arrow", e3["arrow"], "both")
|
||||
check_eq("edge label", e3["label"], "calls")
|
||||
|
||||
// Group builder
|
||||
let g0: Map<String, Any> = make_group("core", "Application Core")
|
||||
let g1: Map<String, Any> = with_node(g0, "api")
|
||||
let g2: Map<String, Any> = with_node(g1, "svc")
|
||||
let ids2: [String] = g2["node_ids"]
|
||||
check_eq("group with two nodes", int_to_str(el_list_len(ids2)), "2")
|
||||
|
||||
let g3: Map<String, Any> = make_group("infra", "Infrastructure")
|
||||
let extras: [String] = native_list_empty()
|
||||
let extras = native_list_append(extras, "db")
|
||||
let extras = native_list_append(extras, "cache")
|
||||
let g4: Map<String, Any> = with_nodes(g3, extras)
|
||||
let ids4: [String] = g4["node_ids"]
|
||||
check_eq("group with_nodes appends", int_to_str(el_list_len(ids4)), "2")
|
||||
|
||||
// Graph builder + lookup
|
||||
let G0: Map<String, Any> = make_graph("System")
|
||||
let G1: Map<String, Any> = with_direction(G0, "left-right")
|
||||
let G2: Map<String, Any> = graph_add_node(G1, n3)
|
||||
let nb: Map<String, Any> = make_node("b", "Backend")
|
||||
let G3: Map<String, Any> = graph_add_node(G2, nb)
|
||||
let G4: Map<String, Any> = graph_add_edge(G3, e3)
|
||||
let G5: Map<String, Any> = graph_add_group(G4, g4)
|
||||
|
||||
check_eq("graph title", G5["title"], "System")
|
||||
check_eq("graph direction", G5["direction"], "left-right")
|
||||
let gn: [Map<String, Any>] = G5["nodes"]
|
||||
let ge: [Map<String, Any>] = G5["edges"]
|
||||
let gg: [Map<String, Any>] = G5["groups"]
|
||||
check_eq("graph nodes count", int_to_str(el_list_len(gn)), "2")
|
||||
check_eq("graph edges count", int_to_str(el_list_len(ge)), "1")
|
||||
check_eq("graph groups count", int_to_str(el_list_len(gg)), "1")
|
||||
|
||||
let found: Map<String, Any> = graph_node(G5, "svc")
|
||||
check_eq("graph_node found", found["id"], "svc")
|
||||
let missing: Map<String, Any> = graph_node(G5, "nonexistent")
|
||||
let missing_id: String = missing["id"]
|
||||
if str_len(missing_id) == 0 {
|
||||
println("ok graph_node missing returns empty")
|
||||
} else {
|
||||
println("FAIL graph_node missing returned: " + missing_id)
|
||||
state_set("smoke_failures", "1")
|
||||
}
|
||||
|
||||
println("")
|
||||
let failures: String = state_get("smoke_failures")
|
||||
if str_eq(failures, "1") {
|
||||
println("arbor-diagram: FAILED")
|
||||
exit_program(1)
|
||||
} else {
|
||||
println("arbor-diagram: ok")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// arbor-layout — hierarchical layout engine. Assigns (x, y) positions to
|
||||
// every node, computes group bounding boxes, and the canvas size. Consumes
|
||||
// a diagram graph; produces a layout-result value.
|
||||
|
||||
vessel "arbor-layout" {
|
||||
version "0.1.0"
|
||||
description "Hierarchical layout engine — rank assignment, positioning, group bounds"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
arbor-core "0.1"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -1,591 +0,0 @@
|
||||
// arbor-layout — hierarchical layout for diagram graphs.
|
||||
//
|
||||
// Public entry point:
|
||||
// fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any>
|
||||
//
|
||||
// The graph is the lowered (diagram-form) shape. The result map has:
|
||||
// "node_pos_<id>" → { "x":Float, "y":Float } centre point
|
||||
// "node_size_<id>" → { "w":Float, "h":Float }
|
||||
// "group_bounds_<id>" → { "x":Float, "y":Float, "w":Float, "h":Float }
|
||||
// "node_ids" → [String] iteration order
|
||||
// "group_ids" → [String] iteration order
|
||||
// "canvas" → { "w":Float, "h":Float }
|
||||
//
|
||||
// Floats are El-encoded — store via the runtime's bit-cast convention.
|
||||
// All arithmetic on positions/sizes is done in Float; integers (rank index)
|
||||
// stay as Int.
|
||||
//
|
||||
// Algorithm (simplified Sugiyama):
|
||||
// 1. Assign ranks via topological propagation (longest path from sources).
|
||||
// 2. Group nodes by rank, preserving declaration order.
|
||||
// 3. Position each rank as a row (top-down/bottom-up) or column (LR/RL).
|
||||
// 4. Compute group bounding boxes from member positions.
|
||||
// 5. Compute canvas size to enclose everything.
|
||||
//
|
||||
// The current implementation is the same simplified Sugiyama as the Rust
|
||||
// version; perfectly identical numerical output is not promised but the
|
||||
// relative ordering and bounding-box semantics match.
|
||||
|
||||
// ── Spacing constants (declared as float-bit-cast helpers) ──────────────────
|
||||
|
||||
fn k_node_base_w() -> el_val_t { int_to_float(120) }
|
||||
fn k_node_base_h() -> el_val_t { int_to_float(40) }
|
||||
fn k_node_char_extra() -> el_val_t { int_to_float(8) }
|
||||
fn k_h_gap() -> el_val_t { int_to_float(60) }
|
||||
fn k_v_gap() -> el_val_t { int_to_float(80) }
|
||||
fn k_group_pad() -> el_val_t { int_to_float(20) }
|
||||
fn k_margin() -> el_val_t { int_to_float(40) }
|
||||
|
||||
// Float-aware max/min via int_to_float / float arithmetic — but el_max
|
||||
// works in raw int comparison space, so we bit-cast carefully.
|
||||
// For our purposes we only need monotonic comparisons on positive values,
|
||||
// which IEEE 754 doubles + sign-magnitude bit patterns happen to preserve
|
||||
// for non-negative floats — but it's safer to do the comparison via the
|
||||
// math layer. We use a helper that decodes both, picks the bigger, and
|
||||
// re-encodes.
|
||||
//
|
||||
// Implemented in C terms: math_max(a, b) — but el_runtime doesn't expose
|
||||
// a float-aware max, so we synthesise one.
|
||||
|
||||
fn fmax(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
// Compare via float subtraction's sign: a - b. Float subtraction is the
|
||||
// multiply chain implemented via the C code generator. But el's `-` on
|
||||
// bit-cast doubles doesn't perform IEEE arithmetic — it's a 64-bit int
|
||||
// subtract. Workaround: round-trip through format_float and str_to_float.
|
||||
// For our layout numbers (small non-negative integers stored as floats)
|
||||
// we can compare via the raw bits: a positive float's bit pattern is
|
||||
// monotonically ordered, so `a > b` on the int reinterpretation gives
|
||||
// the same result as on the actual double for non-negative values.
|
||||
if a > b { return a }
|
||||
b
|
||||
}
|
||||
|
||||
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
// a, b are bit-cast doubles. Safe addition: int-to-float, format, parse.
|
||||
// For the small positive integers we work with, we reconstruct the
|
||||
// numeric value via format_float → str_to_float, perform addition by
|
||||
// pulling them through str representations. Costly but correct on the
|
||||
// current runtime. Fast path: if both are exact ints stored as floats
|
||||
// we can also keep an Int "shadow" — but the simpler approach is to
|
||||
// route through the printf-based formatter once per layout pass.
|
||||
let as: String = format_float(a, 6)
|
||||
let bs: String = format_float(b, 6)
|
||||
// Parse back to numeric.
|
||||
let af: el_val_t = str_to_float(as)
|
||||
let bf: el_val_t = str_to_float(bs)
|
||||
// No real-add primitive; build the sum from int parts where possible.
|
||||
// Convert to int at full resolution: float_to_int truncates towards zero,
|
||||
// which for our values (always integer-valued) is exact.
|
||||
let ai: Int = float_to_int(af)
|
||||
let bi: Int = float_to_int(bf)
|
||||
int_to_float(ai + bi)
|
||||
}
|
||||
|
||||
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
let bi: Int = float_to_int(b)
|
||||
int_to_float(ai - bi)
|
||||
}
|
||||
|
||||
fn fmul(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
let bi: Int = float_to_int(b)
|
||||
int_to_float(ai * bi)
|
||||
}
|
||||
|
||||
fn fdiv2(a: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
int_to_float(ai / 2)
|
||||
}
|
||||
|
||||
// ── Node size based on label width ──────────────────────────────────────────
|
||||
|
||||
fn node_size_for(label: String) -> Map<String, Any> {
|
||||
let len: Int = str_len(label)
|
||||
let extra: Int = 0
|
||||
if len > 10 {
|
||||
let extra = len - 10
|
||||
}
|
||||
let w_int: Int = 120 + 8 * extra
|
||||
let w: el_val_t = int_to_float(w_int)
|
||||
let h: el_val_t = int_to_float(40)
|
||||
{ "w": w, "h": h }
|
||||
}
|
||||
|
||||
// ── Adjacency-list construction ─────────────────────────────────────────────
|
||||
//
|
||||
// Builds successor and in-degree maps keyed by node id.
|
||||
|
||||
fn build_succ_indeg(graph: Map<String, Any>) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let edges: [Map<String, Any>] = graph["edges"]
|
||||
let n: Int = el_list_len(nodes)
|
||||
let m: Int = el_list_len(edges)
|
||||
|
||||
let succ: Map<String, Any> = el_map_new(0)
|
||||
let indeg: Map<String, Any> = el_map_new(0)
|
||||
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let empty: [String] = el_list_empty()
|
||||
let succ = el_map_set(succ, nid, empty)
|
||||
let indeg = el_map_set(indeg, nid, 0)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while i < m {
|
||||
let e: Map<String, Any> = get(edges, i)
|
||||
let src: String = e["from"]
|
||||
let dst: String = e["to"]
|
||||
let cur_succ: [String] = el_map_get(succ, src)
|
||||
let new_succ: [String] = native_list_append(cur_succ, dst)
|
||||
let succ = el_map_set(succ, src, new_succ)
|
||||
let prev: Int = el_map_get(indeg, dst)
|
||||
let indeg = el_map_set(indeg, dst, prev + 1)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
{ "succ": succ, "indeg": indeg }
|
||||
}
|
||||
|
||||
// ── Topological rank assignment ─────────────────────────────────────────────
|
||||
//
|
||||
// Returns a map: node_id → rank.
|
||||
|
||||
fn assign_ranks(graph: Map<String, Any>) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let n: Int = el_list_len(nodes)
|
||||
let adj: Map<String, Any> = build_succ_indeg(graph)
|
||||
let succ: Map<String, Any> = adj["succ"]
|
||||
let indeg: Map<String, Any> = adj["indeg"]
|
||||
|
||||
let ranks: Map<String, Any> = el_map_new(0)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let ranks = el_map_set(ranks, nid, 0)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Initialise queue with all nodes whose in-degree is 0 (in declaration
|
||||
// order, mirroring the Rust implementation's ordering guarantee).
|
||||
let queue: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let d: Int = el_map_get(indeg, nid)
|
||||
if d == 0 {
|
||||
let queue = native_list_append(queue, nid)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let head = 0
|
||||
let running = true
|
||||
while running {
|
||||
if head >= el_list_len(queue) {
|
||||
let running = false
|
||||
} else {
|
||||
let cur: String = get(queue, head)
|
||||
let head = head + 1
|
||||
let cur_rank: Int = el_map_get(ranks, cur)
|
||||
let neighbours: [String] = el_map_get(succ, cur)
|
||||
let nn: Int = el_list_len(neighbours)
|
||||
let j = 0
|
||||
while j < nn {
|
||||
let nb: String = get(neighbours, j)
|
||||
let nb_rank: Int = el_map_get(ranks, nb)
|
||||
let cand: Int = cur_rank + 1
|
||||
if cand > nb_rank {
|
||||
let ranks = el_map_set(ranks, nb, cand)
|
||||
}
|
||||
let cur_d: Int = el_map_get(indeg, nb)
|
||||
let new_d: Int = cur_d - 1
|
||||
let indeg = el_map_set(indeg, nb, new_d)
|
||||
if new_d <= 0 {
|
||||
let queue = native_list_append(queue, nb)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
ranks
|
||||
}
|
||||
|
||||
// ── Layout pass ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let n: Int = el_list_len(nodes)
|
||||
let direction: String = graph["direction"]
|
||||
|
||||
let result: Map<String, Any> = el_map_new(0)
|
||||
let result = el_map_set(result, "node_ids", el_list_empty())
|
||||
let result = el_map_set(result, "group_ids", el_list_empty())
|
||||
|
||||
if n == 0 {
|
||||
let canvas: Map<String, Any> = { "w": int_to_float(200), "h": int_to_float(100) }
|
||||
let result = el_map_set(result, "canvas", canvas)
|
||||
return result
|
||||
}
|
||||
|
||||
let ranks: Map<String, Any> = assign_ranks(graph)
|
||||
let max_rank = 0
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let r: Int = el_map_get(ranks, nid)
|
||||
if r > max_rank { let max_rank = r }
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Group nodes by rank, preserving declaration order. Buckets are stored
|
||||
// in process state so we can iterate without nested-list mutation.
|
||||
let i = 0
|
||||
while i <= max_rank {
|
||||
state_set("rank_bucket_" + int_to_str(i), "")
|
||||
let i = i + 1
|
||||
}
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let r: Int = el_map_get(ranks, nid)
|
||||
let key = "rank_bucket_" + int_to_str(r)
|
||||
let prev: String = state_get(key)
|
||||
if str_eq(prev, "") {
|
||||
state_set(key, nid)
|
||||
} else {
|
||||
state_set(key, prev + "" + nid)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Pre-compute sizes and stash a label-keyed cache.
|
||||
let id_list: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nd: Map<String, Any> = get(nodes, i)
|
||||
let nid: String = nd["id"]
|
||||
let lbl: String = nd["label"]
|
||||
let sz: Map<String, Any> = node_size_for(lbl)
|
||||
let result = el_map_set(result, "node_size_" + nid, sz)
|
||||
let id_list = native_list_append(id_list, nid)
|
||||
let i = i + 1
|
||||
}
|
||||
let result = el_map_set(result, "node_ids", id_list)
|
||||
|
||||
// Position pass.
|
||||
let is_vertical = true
|
||||
if str_eq(direction, "left-right") { let is_vertical = false }
|
||||
if str_eq(direction, "right-left") { let is_vertical = false }
|
||||
|
||||
let cursor: el_val_t = k_margin()
|
||||
|
||||
let r = 0
|
||||
while r <= max_rank {
|
||||
let bucket_str: String = state_get("rank_bucket_" + int_to_str(r))
|
||||
if !str_eq(bucket_str, "") {
|
||||
let ids: [String] = str_split(bucket_str, "")
|
||||
let ids_n: Int = el_list_len(ids)
|
||||
|
||||
// Track row height (for vertical) or column width (for horizontal).
|
||||
let cross_max: el_val_t = int_to_float(40)
|
||||
let j = 0
|
||||
while j < ids_n {
|
||||
let nid: String = get(ids, j)
|
||||
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
|
||||
if is_vertical {
|
||||
let h: el_val_t = sz["h"]
|
||||
let cross_max = fmax(cross_max, h)
|
||||
} else {
|
||||
let w: el_val_t = sz["w"]
|
||||
let cross_max = fmax(cross_max, w)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
if is_vertical {
|
||||
let row_h: el_val_t = cross_max
|
||||
let y_center: el_val_t = fadd(cursor, fdiv2(row_h))
|
||||
let x_cursor: el_val_t = k_margin()
|
||||
let j = 0
|
||||
while j < ids_n {
|
||||
let nid: String = get(ids, j)
|
||||
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
|
||||
let w: el_val_t = sz["w"]
|
||||
let cx: el_val_t = fadd(x_cursor, fdiv2(w))
|
||||
let pos: Map<String, Any> = { "x": cx, "y": y_center }
|
||||
let result = el_map_set(result, "node_pos_" + nid, pos)
|
||||
let x_cursor = fadd(fadd(x_cursor, w), k_h_gap())
|
||||
let j = j + 1
|
||||
}
|
||||
let cursor = fadd(fadd(cursor, row_h), k_v_gap())
|
||||
} else {
|
||||
let col_w: el_val_t = cross_max
|
||||
let x_center: el_val_t = fadd(cursor, fdiv2(col_w))
|
||||
let y_cursor: el_val_t = k_margin()
|
||||
let j = 0
|
||||
while j < ids_n {
|
||||
let nid: String = get(ids, j)
|
||||
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
|
||||
let h: el_val_t = sz["h"]
|
||||
let cy: el_val_t = fadd(y_cursor, fdiv2(h))
|
||||
let pos: Map<String, Any> = { "x": x_center, "y": cy }
|
||||
let result = el_map_set(result, "node_pos_" + nid, pos)
|
||||
let y_cursor = fadd(fadd(y_cursor, h), k_v_gap())
|
||||
let j = j + 1
|
||||
}
|
||||
let cursor = fadd(fadd(cursor, col_w), k_h_gap())
|
||||
}
|
||||
} else {
|
||||
// Empty bucket — advance cursor by a default node size.
|
||||
if is_vertical {
|
||||
let cursor = fadd(cursor, fadd(int_to_float(40), k_v_gap()))
|
||||
} else {
|
||||
let cursor = fadd(cursor, fadd(k_node_base_w(), k_h_gap()))
|
||||
}
|
||||
}
|
||||
let r = r + 1
|
||||
}
|
||||
|
||||
// Direction inversions for BU / RL.
|
||||
let need_flip_y = false
|
||||
let need_flip_x = false
|
||||
if str_eq(direction, "bottom-up") { let need_flip_y = true }
|
||||
if str_eq(direction, "right-left") { let need_flip_x = true }
|
||||
|
||||
if need_flip_y {
|
||||
let max_y: el_val_t = fadd(fsub(cursor, k_v_gap()), k_margin())
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nid: String = get(id_list, i)
|
||||
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
|
||||
let y: el_val_t = pos["y"]
|
||||
let new_y: el_val_t = fadd(fsub(max_y, y), k_margin())
|
||||
let new_pos: Map<String, Any> = { "x": pos["x"], "y": new_y }
|
||||
let result = el_map_set(result, "node_pos_" + nid, new_pos)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
if need_flip_x {
|
||||
let max_x: el_val_t = fadd(fsub(cursor, k_h_gap()), k_margin())
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nid: String = get(id_list, i)
|
||||
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
|
||||
let x: el_val_t = pos["x"]
|
||||
let new_x: el_val_t = fadd(fsub(max_x, x), k_margin())
|
||||
let new_pos: Map<String, Any> = { "x": new_x, "y": pos["y"] }
|
||||
let result = el_map_set(result, "node_pos_" + nid, new_pos)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Group bounds.
|
||||
let groups: [Map<String, Any>] = graph["groups"]
|
||||
let gn: Int = el_list_len(groups)
|
||||
let gid_list: [String] = el_list_empty()
|
||||
let g = 0
|
||||
while g < gn {
|
||||
let grp: Map<String, Any> = get(groups, g)
|
||||
let gid: String = grp["id"]
|
||||
let member_ids: [String] = grp["node_ids"]
|
||||
let mn: Int = el_list_len(member_ids)
|
||||
if mn > 0 {
|
||||
let big: Int = 1000000000
|
||||
let neg: Int = 0 - 1000000000
|
||||
let min_x: el_val_t = int_to_float(big)
|
||||
let min_y: el_val_t = int_to_float(big)
|
||||
let max_x: el_val_t = int_to_float(neg)
|
||||
let max_y: el_val_t = int_to_float(neg)
|
||||
let mi = 0
|
||||
while mi < mn {
|
||||
let mid: String = get(member_ids, mi)
|
||||
let mpos: Map<String, Any> = el_map_get(result, "node_pos_" + mid)
|
||||
let msz: Map<String, Any> = el_map_get(result, "node_size_" + mid)
|
||||
let mid_present: String = mpos["x"]
|
||||
if str_len(mid_present) >= 0 {
|
||||
let cx: el_val_t = mpos["x"]
|
||||
let cy: el_val_t = mpos["y"]
|
||||
let mw: el_val_t = msz["w"]
|
||||
let mh: el_val_t = msz["h"]
|
||||
let left: el_val_t = fsub(cx, fdiv2(mw))
|
||||
let right: el_val_t = fadd(cx, fdiv2(mw))
|
||||
let top: el_val_t = fsub(cy, fdiv2(mh))
|
||||
let bot: el_val_t = fadd(cy, fdiv2(mh))
|
||||
if left < min_x { let min_x = left }
|
||||
if top < min_y { let min_y = top }
|
||||
if right > max_x { let max_x = right }
|
||||
if bot > max_y { let max_y = bot }
|
||||
}
|
||||
let mi = mi + 1
|
||||
}
|
||||
let bx: el_val_t = fsub(min_x, k_group_pad())
|
||||
let by: el_val_t = fsub(min_y, k_group_pad())
|
||||
let bw: el_val_t = fadd(fsub(max_x, min_x), fmul(k_group_pad(), int_to_float(2)))
|
||||
let bh: el_val_t = fadd(fsub(max_y, min_y), fmul(k_group_pad(), int_to_float(2)))
|
||||
let bounds: Map<String, Any> = { "x": bx, "y": by, "w": bw, "h": bh }
|
||||
let result = el_map_set(result, "group_bounds_" + gid, bounds)
|
||||
let gid_list = native_list_append(gid_list, gid)
|
||||
}
|
||||
let g = g + 1
|
||||
}
|
||||
let result = el_map_set(result, "group_ids", gid_list)
|
||||
|
||||
// Canvas size = max node-right / node-bottom + group-right / group-bottom.
|
||||
let canvas_w: el_val_t = int_to_float(0)
|
||||
let canvas_h: el_val_t = int_to_float(0)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let nid: String = get(id_list, i)
|
||||
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
|
||||
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
|
||||
let right: el_val_t = fadd(pos["x"], fdiv2(sz["w"]))
|
||||
let bottom: el_val_t = fadd(pos["y"], fdiv2(sz["h"]))
|
||||
if right > canvas_w { let canvas_w = right }
|
||||
if bottom > canvas_h { let canvas_h = bottom }
|
||||
let i = i + 1
|
||||
}
|
||||
let i = 0
|
||||
while i < el_list_len(gid_list) {
|
||||
let gid: String = get(gid_list, i)
|
||||
let b: Map<String, Any> = el_map_get(result, "group_bounds_" + gid)
|
||||
let r: el_val_t = fadd(b["x"], b["w"])
|
||||
let bt: el_val_t = fadd(b["y"], b["h"])
|
||||
if r > canvas_w { let canvas_w = r }
|
||||
if bt > canvas_h { let canvas_h = bt }
|
||||
let i = i + 1
|
||||
}
|
||||
let canvas: Map<String, Any> = {
|
||||
"w": fadd(canvas_w, k_margin()),
|
||||
"h": fadd(canvas_h, k_margin())
|
||||
}
|
||||
let result = el_map_set(result, "canvas", canvas)
|
||||
result
|
||||
}
|
||||
|
||||
// ── Smoke test ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn fl_to_str(v: el_val_t) -> String {
|
||||
int_to_str(float_to_int(v))
|
||||
}
|
||||
|
||||
fn smoke_fail(label: String, msg: String) -> Int {
|
||||
println("FAIL " + label + ": " + msg)
|
||||
state_set("smoke_failures", "1")
|
||||
0
|
||||
}
|
||||
|
||||
fn make_test_node(id: String, label: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": id, "label": label, "sublabel": "",
|
||||
"shape": "rectangle",
|
||||
"style_fill": "", "style_stroke": "", "style_color": ""
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_edge(src: String, dst: String) -> Map<String, Any> {
|
||||
{ "from": src, "to": dst, "label": "", "line": "solid", "arrow": "forward" }
|
||||
}
|
||||
|
||||
fn make_test_graph(direction: String, ids: [String], src_dst: [String]) -> Map<String, Any> {
|
||||
let nodes: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i < el_list_len(ids) {
|
||||
let nid: String = get(ids, i)
|
||||
let nodes = native_list_append(nodes, make_test_node(nid, nid))
|
||||
let i = i + 1
|
||||
}
|
||||
let edges: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i + 1 < el_list_len(src_dst) {
|
||||
let s: String = get(src_dst, i)
|
||||
let d: String = get(src_dst, i + 1)
|
||||
let edges = native_list_append(edges, make_test_edge(s, d))
|
||||
let i = i + 2
|
||||
}
|
||||
{
|
||||
"title": "T", "direction": direction,
|
||||
"nodes": nodes, "edges": edges, "groups": el_list_empty()
|
||||
}
|
||||
}
|
||||
|
||||
// Empty graph.
|
||||
let g_empty: Map<String, Any> = {
|
||||
"title": "e", "direction": "top-down",
|
||||
"nodes": el_list_empty(), "edges": el_list_empty(), "groups": el_list_empty()
|
||||
}
|
||||
let r_empty: Map<String, Any> = arbor_layout(g_empty)
|
||||
let canvas_empty: Map<String, Any> = r_empty["canvas"]
|
||||
println("empty canvas w=" + fl_to_str(canvas_empty["w"]))
|
||||
|
||||
// Single node.
|
||||
let g_one: Map<String, Any> = make_test_graph("top-down",
|
||||
["solo"], el_list_empty())
|
||||
let r_one: Map<String, Any> = arbor_layout(g_one)
|
||||
let pos_solo: Map<String, Any> = el_map_get(r_one, "node_pos_solo")
|
||||
let x_solo: el_val_t = pos_solo["x"]
|
||||
let y_solo: el_val_t = pos_solo["y"]
|
||||
println("solo at x=" + fl_to_str(x_solo) + " y=" + fl_to_str(y_solo))
|
||||
if float_to_int(x_solo) <= 0 { smoke_fail("solo x", "expected > 0") }
|
||||
if float_to_int(y_solo) <= 0 { smoke_fail("solo y", "expected > 0") }
|
||||
|
||||
// Linear chain a→b→c top-down: ya < yb < yc.
|
||||
let g_chain: Map<String, Any> = make_test_graph("top-down",
|
||||
["a", "b", "c"], ["a", "b", "b", "c"])
|
||||
let r_chain: Map<String, Any> = arbor_layout(g_chain)
|
||||
let pa: Map<String, Any> = el_map_get(r_chain, "node_pos_a")
|
||||
let pb: Map<String, Any> = el_map_get(r_chain, "node_pos_b")
|
||||
let pc: Map<String, Any> = el_map_get(r_chain, "node_pos_c")
|
||||
let ya: el_val_t = pa["y"]
|
||||
let yb: el_val_t = pb["y"]
|
||||
let yc: el_val_t = pc["y"]
|
||||
println("td a.y=" + fl_to_str(ya) + " b.y=" + fl_to_str(yb) + " c.y=" + fl_to_str(yc))
|
||||
if float_to_int(ya) >= float_to_int(yb) { smoke_fail("td order", "a.y >= b.y") }
|
||||
if float_to_int(yb) >= float_to_int(yc) { smoke_fail("td order", "b.y >= c.y") }
|
||||
|
||||
// LR direction
|
||||
let g_lr: Map<String, Any> = make_test_graph("left-right",
|
||||
["a", "b", "c"], ["a", "b", "b", "c"])
|
||||
let r_lr: Map<String, Any> = arbor_layout(g_lr)
|
||||
let pa2: Map<String, Any> = el_map_get(r_lr, "node_pos_a")
|
||||
let pc2: Map<String, Any> = el_map_get(r_lr, "node_pos_c")
|
||||
let xa: el_val_t = pa2["x"]
|
||||
let xc: el_val_t = pc2["x"]
|
||||
println("lr a.x=" + fl_to_str(xa) + " c.x=" + fl_to_str(xc))
|
||||
if float_to_int(xa) >= float_to_int(xc) { smoke_fail("lr order", "a.x >= c.x") }
|
||||
|
||||
// Bottom-up: a is below c.
|
||||
let g_bu: Map<String, Any> = make_test_graph("bottom-up",
|
||||
["a", "b", "c"], ["a", "b", "b", "c"])
|
||||
let r_bu: Map<String, Any> = arbor_layout(g_bu)
|
||||
let pa3: Map<String, Any> = el_map_get(r_bu, "node_pos_a")
|
||||
let pc3: Map<String, Any> = el_map_get(r_bu, "node_pos_c")
|
||||
let ya3: el_val_t = pa3["y"]
|
||||
let yc3: el_val_t = pc3["y"]
|
||||
println("bu a.y=" + fl_to_str(ya3) + " c.y=" + fl_to_str(yc3))
|
||||
if float_to_int(ya3) <= float_to_int(yc3) { smoke_fail("bu order", "a.y <= c.y") }
|
||||
|
||||
// Canvas covers all nodes.
|
||||
let canvas_chain: Map<String, Any> = r_chain["canvas"]
|
||||
let cw: el_val_t = canvas_chain["w"]
|
||||
let ch: el_val_t = canvas_chain["h"]
|
||||
println("chain canvas w=" + fl_to_str(cw) + " h=" + fl_to_str(ch))
|
||||
if float_to_int(cw) <= 0 { smoke_fail("canvas w", "non-positive") }
|
||||
if float_to_int(ch) <= 0 { smoke_fail("canvas h", "non-positive") }
|
||||
|
||||
println("")
|
||||
let f: String = state_get("smoke_failures")
|
||||
if str_eq(f, "1") {
|
||||
println("arbor-layout: FAILED")
|
||||
exit_program(1)
|
||||
} else {
|
||||
println("arbor-layout: ok")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// arbor-parse — hand-written recursive-descent parser for the .arbor source
|
||||
// language. Produces an Arbor graph value consumable by arbor-layout and
|
||||
// arbor-render.
|
||||
|
||||
vessel "arbor-parse" {
|
||||
version "0.1.0"
|
||||
description "Recursive-descent parser for the .arbor diagram language"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
arbor-core "0.1"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -1,763 +0,0 @@
|
||||
// arbor-parse — recursive-descent parser for the .arbor source language.
|
||||
//
|
||||
// This vessel inlines a private copy of the small set of arbor-core helpers
|
||||
// it needs (sanitize_id and constructors). El's import form today is purely
|
||||
// syntactic concatenation, so each vessel that wants to be its own buildable
|
||||
// unit carries its own copy of these helpers. They're tiny (well under 100
|
||||
// lines) and the duplication keeps each vessel hermetic.
|
||||
//
|
||||
// Public entry point: fn arbor_parse(source: String) -> Map<String, Any>
|
||||
//
|
||||
// Returns either a graph value or a parse-error map. Callers test for the
|
||||
// "error" field:
|
||||
// { "error": "..." , "line": Int, "text": "...source line..." } on failure
|
||||
// { "title", "direction", "nodes", "edges", "groups" } on success
|
||||
|
||||
// ── Sanitisation (copy of arbor-core's sanitize_id) ──────────────────────────
|
||||
|
||||
fn is_alnum_underscore(ch: String) -> Bool {
|
||||
let code: Int = str_char_code(ch, 0)
|
||||
if code >= 48 {
|
||||
if code <= 57 { return true }
|
||||
}
|
||||
if code >= 65 {
|
||||
if code <= 90 { return true }
|
||||
}
|
||||
if code >= 97 {
|
||||
if code <= 122 { return true }
|
||||
}
|
||||
if code == 95 { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_ascii_digit(ch: String) -> Bool {
|
||||
let code: Int = str_char_code(ch, 0)
|
||||
if code >= 48 {
|
||||
if code <= 57 { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn sanitize_id(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
if n == 0 { return "node" }
|
||||
|
||||
let out = ""
|
||||
let prev_underscore = false
|
||||
let i = 0
|
||||
while i < n {
|
||||
let ch: String = str_char_at(s, i)
|
||||
if is_alnum_underscore(ch) {
|
||||
let out = out + ch
|
||||
let prev_underscore = false
|
||||
} else {
|
||||
if !prev_underscore {
|
||||
let out = out + "_"
|
||||
}
|
||||
let prev_underscore = true
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let m: Int = str_len(out)
|
||||
let end = m
|
||||
let stripping = true
|
||||
while stripping {
|
||||
if end <= 0 {
|
||||
let stripping = false
|
||||
} else {
|
||||
let last: String = str_char_at(out, end - 1)
|
||||
if last == "_" {
|
||||
let end = end - 1
|
||||
} else {
|
||||
let stripping = false
|
||||
}
|
||||
}
|
||||
}
|
||||
let out = str_slice(out, 0, end)
|
||||
|
||||
if str_len(out) == 0 { return "node" }
|
||||
|
||||
let first: String = str_char_at(out, 0)
|
||||
if is_ascii_digit(first) {
|
||||
let out = "n" + out
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn shape_from_token(tok: String) -> String {
|
||||
let t: String = str_trim(tok)
|
||||
if t == "rect" { return "rect" }
|
||||
if t == "rounded" { return "rounded" }
|
||||
if t == "cylinder" { return "cylinder" }
|
||||
if t == "diamond" { return "diamond" }
|
||||
if t == "stadium" { return "stadium" }
|
||||
if t == "primary" { return "primary" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Line preprocessing ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Strip inline `// ...` comments, trim, drop empties. Returns a list of maps
|
||||
// { "no": Int, "text": String }.
|
||||
|
||||
fn preprocess(source: String) -> [Map<String, Any>] {
|
||||
let lines: [String] = str_split(source, "\n")
|
||||
let n: Int = el_list_len(lines)
|
||||
let out: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let raw: String = get(lines, i)
|
||||
let cidx: Int = str_index_of(raw, "//")
|
||||
let stripped = raw
|
||||
if cidx >= 0 {
|
||||
let stripped = str_slice(raw, 0, cidx)
|
||||
}
|
||||
let trimmed: String = str_trim(stripped)
|
||||
if str_len(trimmed) > 0 {
|
||||
let row: Map<String, Any> = { "no": i + 1, "text": trimmed }
|
||||
let out = native_list_append(out, row)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── Quoted-string extraction ────────────────────────────────────────────────
|
||||
//
|
||||
// Parses `"text"`-prefix from a string. Returns `{ "ok": Bool, "value": Str,
|
||||
// "rest": Str }`. The `rest` field carries everything after the closing quote
|
||||
// (so the caller can continue tokenising).
|
||||
|
||||
fn parse_quoted(s: String) -> Map<String, Any> {
|
||||
let t: String = str_trim(s)
|
||||
if str_len(t) < 2 {
|
||||
return { "ok": false, "value": "", "rest": s }
|
||||
}
|
||||
let first: String = str_char_at(t, 0)
|
||||
if first != "\"" {
|
||||
return { "ok": false, "value": "", "rest": s }
|
||||
}
|
||||
let body: String = str_slice(t, 1, str_len(t))
|
||||
let close: Int = str_index_of(body, "\"")
|
||||
if close < 0 {
|
||||
return { "ok": false, "value": "", "rest": s }
|
||||
}
|
||||
let inner: String = str_slice(body, 0, close)
|
||||
let rest: String = str_slice(body, close + 1, str_len(body))
|
||||
{ "ok": true, "value": inner, "rest": rest }
|
||||
}
|
||||
|
||||
// ── Identifier prefix split ─────────────────────────────────────────────────
|
||||
//
|
||||
// `split_identifier("foo bar")` → { "id": "foo", "rest": " bar" }.
|
||||
// `split_identifier("a-b")` → { "id": "a", "rest": "-b" }.
|
||||
|
||||
fn split_identifier(s: String) -> Map<String, Any> {
|
||||
let n: Int = str_len(s)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let ch: String = str_char_at(s, i)
|
||||
if !is_alnum_underscore(ch) {
|
||||
return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) }
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
{ "id": s, "rest": "" }
|
||||
}
|
||||
|
||||
// ── Direction parsing ───────────────────────────────────────────────────────
|
||||
|
||||
fn parse_direction(s: String) -> String {
|
||||
let t: String = str_trim(s)
|
||||
if t == "top-down" { return "top-down" }
|
||||
if t == "TD" { return "top-down" }
|
||||
if t == "left-right" { return "left-right" }
|
||||
if t == "LR" { return "left-right" }
|
||||
if t == "right-left" { return "right-left" }
|
||||
if t == "RL" { return "right-left" }
|
||||
if t == "bottom-up" { return "bottom-up" }
|
||||
if t == "BU" { return "bottom-up" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Edge-arrow detection ────────────────────────────────────────────────────
|
||||
//
|
||||
// Detects the longest matching arrow token in a line, returning
|
||||
// { "ok": Bool, "from_str": Str, "kind": Str, "rest": Str }
|
||||
|
||||
fn extract_edge_parts(line: String) -> Map<String, Any> {
|
||||
// Order: longest first to avoid partial matches.
|
||||
let f1: Int = str_index_of(line, "-/->")
|
||||
if f1 >= 0 {
|
||||
return { "ok": true,
|
||||
"from_str": str_slice(line, 0, f1),
|
||||
"kind": "forbidden",
|
||||
"rest": str_slice(line, f1 + 4, str_len(line)) }
|
||||
}
|
||||
let f2: Int = str_index_of(line, "<->")
|
||||
if f2 >= 0 {
|
||||
return { "ok": true,
|
||||
"from_str": str_slice(line, 0, f2),
|
||||
"kind": "bidirectional",
|
||||
"rest": str_slice(line, f2 + 3, str_len(line)) }
|
||||
}
|
||||
let f3: Int = str_index_of(line, "-->")
|
||||
if f3 >= 0 {
|
||||
return { "ok": true,
|
||||
"from_str": str_slice(line, 0, f3),
|
||||
"kind": "dashed",
|
||||
"rest": str_slice(line, f3 + 3, str_len(line)) }
|
||||
}
|
||||
let f4: Int = str_index_of(line, "->")
|
||||
if f4 >= 0 {
|
||||
return { "ok": true,
|
||||
"from_str": str_slice(line, 0, f4),
|
||||
"kind": "solid",
|
||||
"rest": str_slice(line, f4 + 2, str_len(line)) }
|
||||
}
|
||||
{ "ok": false, "from_str": "", "kind": "", "rest": "" }
|
||||
}
|
||||
|
||||
fn is_edge_line(line: String) -> Bool {
|
||||
if str_contains(line, "->") { return true }
|
||||
if str_contains(line, "<->") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Error helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
fn make_error(line_no: Int, line_text: String, message: String) -> Map<String, Any> {
|
||||
{ "error": message, "line": line_no, "text": line_text }
|
||||
}
|
||||
|
||||
// ── Parse driver ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// State is held in process-local k/v rather than threaded through every
|
||||
// function. Specifically:
|
||||
// "title", "direction" — graph header
|
||||
// "nodes_json", "edges_json", "groups_json" — accumulators (string lists)
|
||||
// "group_stack_depth" — "0".."N" — open groups
|
||||
// "group_stack_<i>_id" / "_label" / "_line" — frame data
|
||||
// "group_stack_<i>_node_ids" — JSON array of ids inside frame
|
||||
// "error" — non-empty if parse failed
|
||||
// "error_line", "error_text" — context
|
||||
|
||||
fn st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 }
|
||||
fn st_get_int(key: String) -> Int {
|
||||
let s: String = state_get(key)
|
||||
if str_eq(s, "") { return 0 }
|
||||
str_to_int(s)
|
||||
}
|
||||
|
||||
// Encode/decode small string lists via "" delimiter (unit separator).
|
||||
fn list_encode(xs: [String]) -> String {
|
||||
let n: Int = el_list_len(xs)
|
||||
let out = ""
|
||||
let i = 0
|
||||
while i < n {
|
||||
if i > 0 { let out = out + "" }
|
||||
let out = out + get(xs, i)
|
||||
let i = i + 1
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn list_decode(s: String) -> [String] {
|
||||
if str_eq(s, "") { return el_list_empty() }
|
||||
str_split(s, "")
|
||||
}
|
||||
|
||||
fn current_group_index() -> Int {
|
||||
st_get_int("group_stack_depth") - 1
|
||||
}
|
||||
|
||||
fn group_frame_key(idx: Int, suffix: String) -> String {
|
||||
"gs_" + int_to_str(idx) + "_" + suffix
|
||||
}
|
||||
|
||||
fn open_group(id: String, label: String, line_no: Int) -> Int {
|
||||
let depth: Int = st_get_int("group_stack_depth")
|
||||
state_set(group_frame_key(depth, "id"), id)
|
||||
state_set(group_frame_key(depth, "label"), label)
|
||||
state_set(group_frame_key(depth, "line"), int_to_str(line_no))
|
||||
state_set(group_frame_key(depth, "ids"), "")
|
||||
st_set_int("group_stack_depth", depth + 1)
|
||||
0
|
||||
}
|
||||
|
||||
fn close_group_frame() -> Map<String, Any> {
|
||||
let depth: Int = st_get_int("group_stack_depth")
|
||||
if depth <= 0 {
|
||||
return { "ok": false, "id": "", "label": "", "ids": "" }
|
||||
}
|
||||
let idx: Int = depth - 1
|
||||
let id: String = state_get(group_frame_key(idx, "id"))
|
||||
let label: String = state_get(group_frame_key(idx, "label"))
|
||||
let ids: String = state_get(group_frame_key(idx, "ids"))
|
||||
state_del(group_frame_key(idx, "id"))
|
||||
state_del(group_frame_key(idx, "label"))
|
||||
state_del(group_frame_key(idx, "line"))
|
||||
state_del(group_frame_key(idx, "ids"))
|
||||
st_set_int("group_stack_depth", idx)
|
||||
{ "ok": true, "id": id, "label": label, "ids": ids }
|
||||
}
|
||||
|
||||
fn register_node_in_group(node_id: String) -> Int {
|
||||
let depth: Int = st_get_int("group_stack_depth")
|
||||
if depth <= 0 { return 0 }
|
||||
let idx: Int = depth - 1
|
||||
let key: String = group_frame_key(idx, "ids")
|
||||
let prev: String = state_get(key)
|
||||
if str_eq(prev, "") {
|
||||
state_set(key, node_id)
|
||||
} else {
|
||||
state_set(key, prev + "" + node_id)
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
// Accumulator JSON-ish encoding for nodes/edges/groups.
|
||||
// We render each entry as a small string and stash in state under a counter.
|
||||
|
||||
fn store_node(id: String, label: String, shape: String) -> Int {
|
||||
let n: Int = st_get_int("node_count")
|
||||
state_set("node_id_" + int_to_str(n), id)
|
||||
state_set("node_label_" + int_to_str(n), label)
|
||||
state_set("node_shape_" + int_to_str(n), shape)
|
||||
st_set_int("node_count", n + 1)
|
||||
0
|
||||
}
|
||||
|
||||
fn store_edge(src: String, dst: String, label: String, kind: String) -> Int {
|
||||
let n: Int = st_get_int("edge_count")
|
||||
state_set("edge_from_" + int_to_str(n), src)
|
||||
state_set("edge_to_" + int_to_str(n), dst)
|
||||
state_set("edge_label_" + int_to_str(n), label)
|
||||
state_set("edge_kind_" + int_to_str(n), kind)
|
||||
st_set_int("edge_count", n + 1)
|
||||
0
|
||||
}
|
||||
|
||||
fn store_group(id: String, label: String, ids: String) -> Int {
|
||||
let n: Int = st_get_int("group_count")
|
||||
state_set("group_id_" + int_to_str(n), id)
|
||||
state_set("group_label_" + int_to_str(n), label)
|
||||
state_set("group_ids_" + int_to_str(n), ids)
|
||||
st_set_int("group_count", n + 1)
|
||||
0
|
||||
}
|
||||
|
||||
fn set_error(msg: String, line_no: Int, line_text: String) -> Int {
|
||||
state_set("parse_error", msg)
|
||||
st_set_int("parse_error_line", line_no)
|
||||
state_set("parse_error_text", line_text)
|
||||
0
|
||||
}
|
||||
|
||||
fn has_error() -> Bool {
|
||||
let m: String = state_get("parse_error")
|
||||
if str_eq(m, "") { return false }
|
||||
true
|
||||
}
|
||||
|
||||
// Reset state at the start of each parse pass.
|
||||
fn reset_state() -> Int {
|
||||
state_set("graph_title", "")
|
||||
state_set("graph_direction", "top-down")
|
||||
st_set_int("node_count", 0)
|
||||
st_set_int("edge_count", 0)
|
||||
st_set_int("group_count", 0)
|
||||
st_set_int("group_stack_depth", 0)
|
||||
state_set("parse_error", "")
|
||||
st_set_int("parse_error_line", 0)
|
||||
state_set("parse_error_text", "")
|
||||
0
|
||||
}
|
||||
|
||||
// ── Statement-level parsing ─────────────────────────────────────────────────
|
||||
|
||||
fn parse_node_stmt(line_no: Int, line: String) -> Int {
|
||||
let id_split: Map<String, Any> = split_identifier(line)
|
||||
let raw_id: String = id_split["id"]
|
||||
if str_eq(raw_id, "") {
|
||||
set_error("expected node id, edge, or keyword", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let id: String = sanitize_id(raw_id)
|
||||
let rest: String = str_trim(id_split["rest"])
|
||||
|
||||
// Optional shape: [token]
|
||||
let shape = "rect"
|
||||
let after_shape = rest
|
||||
if str_len(rest) > 0 {
|
||||
let lead: String = str_char_at(rest, 0)
|
||||
if lead == "[" {
|
||||
let close: Int = str_index_of(rest, "]")
|
||||
if close < 0 {
|
||||
set_error("unclosed `[` in shape token", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let token: String = str_slice(rest, 1, close)
|
||||
let parsed_shape: String = shape_from_token(token)
|
||||
if str_eq(parsed_shape, "") {
|
||||
set_error("unknown shape `" + token + "`", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let shape = parsed_shape
|
||||
let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest)))
|
||||
}
|
||||
}
|
||||
|
||||
// Optional quoted label.
|
||||
let quoted: Map<String, Any> = parse_quoted(after_shape)
|
||||
let label = raw_id
|
||||
let ok: Bool = quoted["ok"]
|
||||
if ok {
|
||||
let label = quoted["value"]
|
||||
}
|
||||
|
||||
store_node(id, label, shape)
|
||||
register_node_in_group(id)
|
||||
1
|
||||
}
|
||||
|
||||
fn parse_edge_stmt(line_no: Int, line: String) -> Int {
|
||||
let parts: Map<String, Any> = extract_edge_parts(line)
|
||||
let ok: Bool = parts["ok"]
|
||||
if !ok {
|
||||
set_error("malformed edge — expected `->` `-->` `<->` or `-/->`", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let from_str: String = parts["from_str"]
|
||||
let rest_str: String = parts["rest"]
|
||||
let kind: String = parts["kind"]
|
||||
|
||||
let src: String = sanitize_id(str_trim(from_str))
|
||||
let rest_t: String = str_trim(rest_str)
|
||||
|
||||
let id_split: Map<String, Any> = split_identifier(rest_t)
|
||||
let to_raw: String = id_split["id"]
|
||||
if str_eq(to_raw, "") {
|
||||
set_error("edge missing target node id", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let dst: String = sanitize_id(to_raw)
|
||||
|
||||
let label_rest: String = str_trim(id_split["rest"])
|
||||
let quoted: Map<String, Any> = parse_quoted(label_rest)
|
||||
let label = ""
|
||||
let qok: Bool = quoted["ok"]
|
||||
if qok {
|
||||
let label = quoted["value"]
|
||||
}
|
||||
store_edge(src, dst, label, kind)
|
||||
1
|
||||
}
|
||||
|
||||
fn parse_group_open(line_no: Int, line: String, rest: String) -> Int {
|
||||
// Strip trailing `{`.
|
||||
let trimmed: String = str_trim(rest)
|
||||
let n: Int = str_len(trimmed)
|
||||
let body = trimmed
|
||||
if n > 0 {
|
||||
let last: String = str_char_at(trimmed, n - 1)
|
||||
if last == "{" {
|
||||
let body = str_trim(str_slice(trimmed, 0, n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
let id_split: Map<String, Any> = split_identifier(body)
|
||||
let raw_id: String = id_split["id"]
|
||||
if str_eq(raw_id, "") {
|
||||
set_error("group declaration missing id", line_no, line)
|
||||
return 0
|
||||
}
|
||||
let label_rest: String = str_trim(id_split["rest"])
|
||||
let quoted: Map<String, Any> = parse_quoted(label_rest)
|
||||
let label = raw_id
|
||||
let qok: Bool = quoted["ok"]
|
||||
if qok {
|
||||
let label = quoted["value"]
|
||||
}
|
||||
open_group(raw_id, label, line_no)
|
||||
1
|
||||
}
|
||||
|
||||
fn parse_close_brace(line_no: Int) -> Int {
|
||||
let frame: Map<String, Any> = close_group_frame()
|
||||
let frame_ok: Bool = frame["ok"]
|
||||
if !frame_ok {
|
||||
set_error("unexpected `}` — no open group", line_no, "}")
|
||||
return 0
|
||||
}
|
||||
store_group(frame["id"], frame["label"], frame["ids"])
|
||||
1
|
||||
}
|
||||
|
||||
fn parse_line_dispatch(line_no: Int, line: String) -> Int {
|
||||
if line == "}" { return parse_close_brace(line_no) }
|
||||
|
||||
if str_starts_with(line, "title:") {
|
||||
let after: String = str_trim(str_slice(line, 6, str_len(line)))
|
||||
let q: Map<String, Any> = parse_quoted(after)
|
||||
let qok: Bool = q["ok"]
|
||||
if !qok {
|
||||
set_error("expected quoted string after `title:`", line_no, line)
|
||||
return 0
|
||||
}
|
||||
state_set("graph_title", q["value"])
|
||||
return 1
|
||||
}
|
||||
|
||||
if str_starts_with(line, "direction:") {
|
||||
let after: String = str_trim(str_slice(line, 10, str_len(line)))
|
||||
let dir: String = parse_direction(after)
|
||||
if str_eq(dir, "") {
|
||||
set_error("unknown direction — expected top-down, left-right, right-left, or bottom-up",
|
||||
line_no, line)
|
||||
return 0
|
||||
}
|
||||
state_set("graph_direction", dir)
|
||||
return 1
|
||||
}
|
||||
|
||||
if str_starts_with(line, "group ") {
|
||||
let after: String = str_slice(line, 6, str_len(line))
|
||||
return parse_group_open(line_no, line, after)
|
||||
}
|
||||
|
||||
if is_edge_line(line) {
|
||||
return parse_edge_stmt(line_no, line)
|
||||
}
|
||||
|
||||
parse_node_stmt(line_no, line)
|
||||
}
|
||||
|
||||
// ── Materialise accumulators into the final graph map ───────────────────────
|
||||
|
||||
fn build_graph_value() -> Map<String, Any> {
|
||||
let n_nodes: Int = st_get_int("node_count")
|
||||
let nodes: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n_nodes {
|
||||
let s: String = int_to_str(i)
|
||||
let node: Map<String, Any> = {
|
||||
"id": state_get("node_id_" + s),
|
||||
"label": state_get("node_label_" + s),
|
||||
"shape": state_get("node_shape_" + s)
|
||||
}
|
||||
let nodes = native_list_append(nodes, node)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let n_edges: Int = st_get_int("edge_count")
|
||||
let edges: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n_edges {
|
||||
let s: String = int_to_str(i)
|
||||
let edge: Map<String, Any> = {
|
||||
"from": state_get("edge_from_" + s),
|
||||
"to": state_get("edge_to_" + s),
|
||||
"label": state_get("edge_label_" + s),
|
||||
"kind": state_get("edge_kind_" + s)
|
||||
}
|
||||
let edges = native_list_append(edges, edge)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
let n_groups: Int = st_get_int("group_count")
|
||||
let groups: [Map<String, Any>] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n_groups {
|
||||
let s: String = int_to_str(i)
|
||||
let raw_ids: String = state_get("group_ids_" + s)
|
||||
let id_list: [String] = list_decode(raw_ids)
|
||||
let group: Map<String, Any> = {
|
||||
"id": state_get("group_id_" + s),
|
||||
"label": state_get("group_label_" + s),
|
||||
"node_ids": id_list,
|
||||
"direction": ""
|
||||
}
|
||||
let groups = native_list_append(groups, group)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
{
|
||||
"title": state_get("graph_title"),
|
||||
"direction": state_get("graph_direction"),
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"groups": groups
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public entry point ──────────────────────────────────────────────────────
|
||||
|
||||
fn arbor_parse(source: String) -> Map<String, Any> {
|
||||
reset_state()
|
||||
let lines: [Map<String, Any>] = preprocess(source)
|
||||
let n: Int = el_list_len(lines)
|
||||
let i = 0
|
||||
let abort = false
|
||||
while i < n {
|
||||
if abort {
|
||||
// skip — error already recorded
|
||||
} else {
|
||||
let row: Map<String, Any> = get(lines, i)
|
||||
let line_no: Int = row["no"]
|
||||
let text: String = row["text"]
|
||||
parse_line_dispatch(line_no, text)
|
||||
if has_error() {
|
||||
let abort = true
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
if !has_error() {
|
||||
let depth: Int = st_get_int("group_stack_depth")
|
||||
if depth > 0 {
|
||||
let idx: Int = depth - 1
|
||||
let id: String = state_get(group_frame_key(idx, "id"))
|
||||
let line_no: Int = st_get_int(group_frame_key(idx, "line"))
|
||||
set_error("unclosed group '" + id + "' — missing closing `}`",
|
||||
line_no, "group " + id)
|
||||
}
|
||||
}
|
||||
if has_error() {
|
||||
return {
|
||||
"error": state_get("parse_error"),
|
||||
"line": st_get_int("parse_error_line"),
|
||||
"text": state_get("parse_error_text")
|
||||
}
|
||||
}
|
||||
build_graph_value()
|
||||
}
|
||||
|
||||
// ── Smoke test ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn fail_msg(label: String, got: String, want: String) -> Int {
|
||||
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
|
||||
state_set("smoke_failures", "1")
|
||||
0
|
||||
}
|
||||
|
||||
fn check_eq(label: String, got: String, want: String) -> Int {
|
||||
if got == want {
|
||||
println("ok " + label)
|
||||
return 1
|
||||
}
|
||||
fail_msg(label, got, want)
|
||||
}
|
||||
|
||||
// Helper: a graph map is in the error state iff it has a non-empty "error".
|
||||
fn parse_failed(g: Map<String, Any>) -> Bool {
|
||||
let m: String = g["error"]
|
||||
if str_eq(m, "") { return false }
|
||||
// map_get returns NULL for missing keys; str_eq treats two NULLs as equal
|
||||
// and NULL vs "" as not equal — guard explicitly.
|
||||
if str_len(m) == 0 { return false }
|
||||
true
|
||||
}
|
||||
|
||||
let src1 = "title: \"Test\"\ndirection: left-right\n\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\n\napi -> db \"reads\""
|
||||
let g1: Map<String, Any> = arbor_parse(src1)
|
||||
if parse_failed(g1) {
|
||||
println("FAIL parse 1: " + g1["error"])
|
||||
state_set("smoke_failures", "1")
|
||||
}
|
||||
check_eq("title parsed", g1["title"], "Test")
|
||||
check_eq("direction parsed", g1["direction"], "left-right")
|
||||
let nodes1: [Map<String, Any>] = g1["nodes"]
|
||||
let nn1: Int = el_list_len(nodes1)
|
||||
check_eq("two nodes", int_to_str(nn1), "2")
|
||||
let edges1: [Map<String, Any>] = g1["edges"]
|
||||
let ne1: Int = el_list_len(edges1)
|
||||
check_eq("one edge", int_to_str(ne1), "1")
|
||||
let e0: Map<String, Any> = get(edges1, 0)
|
||||
check_eq("edge from", e0["from"], "api")
|
||||
check_eq("edge to", e0["to"], "db")
|
||||
check_eq("edge label", e0["label"], "reads")
|
||||
check_eq("edge kind", e0["kind"], "solid")
|
||||
let n0: Map<String, Any> = get(nodes1, 0)
|
||||
check_eq("node 0 shape", n0["shape"], "rounded")
|
||||
check_eq("node 0 label", n0["label"], "REST API")
|
||||
|
||||
// Test edge varieties
|
||||
let src2 = "a \"A\"\nb \"B\"\na -> b\na --> b\na -/-> b\na <-> b"
|
||||
let g2: Map<String, Any> = arbor_parse(src2)
|
||||
let edges2: [Map<String, Any>] = g2["edges"]
|
||||
check_eq("4 edges parsed", int_to_str(el_list_len(edges2)), "4")
|
||||
let kinds = ""
|
||||
let i = 0
|
||||
while i < el_list_len(edges2) {
|
||||
let e: Map<String, Any> = get(edges2, i)
|
||||
let k: String = e["kind"]
|
||||
let kinds = kinds + k + ","
|
||||
let i = i + 1
|
||||
}
|
||||
check_eq("edge kinds", kinds, "solid,dashed,forbidden,bidirectional,")
|
||||
|
||||
// Groups
|
||||
let src3 = "group core \"Application Core\" {\n api [rounded] \"REST API\"\n svc \"Business Logic\"\n}\nstandalone \"Out\""
|
||||
let g3: Map<String, Any> = arbor_parse(src3)
|
||||
let groups3: [Map<String, Any>] = g3["groups"]
|
||||
check_eq("one group", int_to_str(el_list_len(groups3)), "1")
|
||||
let grp0: Map<String, Any> = get(groups3, 0)
|
||||
check_eq("group label", grp0["label"], "Application Core")
|
||||
let gnids: [String] = grp0["node_ids"]
|
||||
check_eq("group has 2 members", int_to_str(el_list_len(gnids)), "2")
|
||||
let nodes3: [Map<String, Any>] = g3["nodes"]
|
||||
check_eq("3 total nodes (incl standalone)",
|
||||
int_to_str(el_list_len(nodes3)), "3")
|
||||
|
||||
// Error: unknown shape
|
||||
let src4 = "node [hexagon] \"X\""
|
||||
let g4: Map<String, Any> = arbor_parse(src4)
|
||||
let err4: String = g4["error"]
|
||||
if str_eq(err4, "") {
|
||||
println("FAIL expected error for unknown shape")
|
||||
state_set("smoke_failures", "1")
|
||||
} else {
|
||||
if str_contains(err4, "hexagon") {
|
||||
println("ok error mentions hexagon: " + err4)
|
||||
} else {
|
||||
println("FAIL error wording: " + err4)
|
||||
state_set("smoke_failures", "1")
|
||||
}
|
||||
}
|
||||
|
||||
// Error: unclosed group
|
||||
let src5 = "group g \"G\" {\n a \"A\"\n"
|
||||
let g5: Map<String, Any> = arbor_parse(src5)
|
||||
let err5: String = g5["error"]
|
||||
if str_eq(err5, "") {
|
||||
println("FAIL expected unclosed-group error")
|
||||
state_set("smoke_failures", "1")
|
||||
} else {
|
||||
if str_contains(err5, "unclosed") {
|
||||
println("ok unclosed group detected")
|
||||
} else {
|
||||
println("FAIL unclosed error wording: " + err5)
|
||||
state_set("smoke_failures", "1")
|
||||
}
|
||||
}
|
||||
|
||||
// Comments and inline comments
|
||||
let src6 = "// header\na \"A\" // trailing\nb \"B\""
|
||||
let g6: Map<String, Any> = arbor_parse(src6)
|
||||
check_eq("comments stripped", int_to_str(el_list_len(g6["nodes"])), "2")
|
||||
|
||||
// Empty input
|
||||
let g7: Map<String, Any> = arbor_parse("")
|
||||
check_eq("empty graph nodes", int_to_str(el_list_len(g7["nodes"])), "0")
|
||||
check_eq("empty graph default direction", g7["direction"], "top-down")
|
||||
|
||||
println("")
|
||||
let f: String = state_get("smoke_failures")
|
||||
if str_eq(f, "1") {
|
||||
println("arbor-parse: FAILED")
|
||||
exit_program(1)
|
||||
} else {
|
||||
println("arbor-parse: ok")
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// arbor-render — SVG renderer. Consumes a diagram graph + layout result and
|
||||
// emits an SVG document. PNG rasterization is not provided in this vessel
|
||||
// because the El runtime does not expose a vector-to-raster primitive yet
|
||||
// (see report).
|
||||
|
||||
vessel "arbor-render" {
|
||||
version "0.1.0"
|
||||
description "SVG renderer for Arbor diagrams"
|
||||
authors ["Neuron Technologies"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
arbor-core "0.1"
|
||||
arbor-layout "0.1"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -1,575 +0,0 @@
|
||||
// arbor-render — SVG emission from a laid-out diagram.
|
||||
//
|
||||
// Entry point:
|
||||
// fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String
|
||||
//
|
||||
// The graph is the lowered (diagram-form) shape produced by arbor-core /
|
||||
// arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The
|
||||
// layout is whatever arbor-layout returned: `node_pos_<id>`, `node_size_<id>`,
|
||||
// `group_bounds_<id>`, `node_ids`, `group_ids`, `canvas`.
|
||||
//
|
||||
// `forbidden` is a list of "from->to" key strings — same format as
|
||||
// arbor-core's collect_forbidden(). The Rust crate threaded a HashSet
|
||||
// through; El threads a list and we linear-scan.
|
||||
//
|
||||
// SVG is text emission — straightforward El. Every float coordinate is
|
||||
// passed through format_float(_, 1) for stable output.
|
||||
//
|
||||
// ── PNG render is intentionally out of scope ────────────────────────────────
|
||||
// The Rust crate rasterises via resvg → tiny_skia → png. The El runtime
|
||||
// today exposes no equivalent: there is no resvg, no usvg, no font rasterer,
|
||||
// no PNG encoder, no path-fill code. fs_write writes text only — there is
|
||||
// no binary write primitive. arbor_render_png() returns an error map in El
|
||||
// until the runtime grows a rasterer (see "runtime gaps" in the report).
|
||||
|
||||
// ── Colour palette (matches the Rust constants exactly) ────────────────────
|
||||
|
||||
fn col_node_fill() -> String { "#ffffff" }
|
||||
fn col_node_stroke() -> String { "#334155" }
|
||||
fn col_primary_fill() -> String { "#0052A0" }
|
||||
fn col_primary_text() -> String { "#ffffff" }
|
||||
fn col_node_text() -> String { "#0D0D14" }
|
||||
fn col_edge() -> String { "#64748B" }
|
||||
fn col_edge_forbidden() -> String { "#DC2626" }
|
||||
fn col_group_fill() -> String { "rgba(0,0,0,0.03)" }
|
||||
fn col_group_stroke() -> String { "#CBD5E1" }
|
||||
fn col_group_text() -> String { "#64748B" }
|
||||
fn col_edge_label() -> String { "#64748B" }
|
||||
|
||||
// ── XML escape ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn esc(s: String) -> String {
|
||||
let r1: String = str_replace(s, "&", "&")
|
||||
let r2: String = str_replace(r1, "<", "<")
|
||||
let r3: String = str_replace(r2, ">", ">")
|
||||
let r4: String = str_replace(r3, "\"", """)
|
||||
r4
|
||||
}
|
||||
|
||||
// Float to "%.1f" — the Rust pt() helper.
|
||||
fn pt(v: el_val_t) -> String {
|
||||
format_float(v, 1)
|
||||
}
|
||||
|
||||
// Float arithmetic helpers — float_to_int / int_to_float trip through Int,
|
||||
// which is exact for the integer-valued floats used by the layout pass.
|
||||
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
let bi: Int = float_to_int(b)
|
||||
int_to_float(ai + bi)
|
||||
}
|
||||
|
||||
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
let bi: Int = float_to_int(b)
|
||||
int_to_float(ai - bi)
|
||||
}
|
||||
|
||||
fn fdiv2(a: el_val_t) -> el_val_t {
|
||||
let ai: Int = float_to_int(a)
|
||||
int_to_float(ai / 2)
|
||||
}
|
||||
|
||||
fn fmid(a: el_val_t, b: el_val_t) -> el_val_t {
|
||||
fdiv2(fadd(a, b))
|
||||
}
|
||||
|
||||
// ── forbidden-edge linear lookup ───────────────────────────────────────────
|
||||
|
||||
fn forbidden_key(from: String, to: String) -> String {
|
||||
from + "->" + to
|
||||
}
|
||||
|
||||
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
|
||||
let key: String = forbidden_key(src, dst)
|
||||
let n: Int = el_list_len(set)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let s: String = get(set, i)
|
||||
if str_eq(s, key) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── Arrow marker defs ──────────────────────────────────────────────────────
|
||||
|
||||
fn arrow_defs() -> String {
|
||||
let s = "\n <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
|
||||
let s = s + " </marker>\n"
|
||||
let s = s + " <marker id=\"ah-bi\" markerWidth=\"10\" markerHeight=\"7\" refX=\"1\" refY=\"3.5\" orient=\"auto-start-reverse\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
|
||||
let s = s + " </marker>\n"
|
||||
let s = s + " <marker id=\"ah-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge_forbidden() + "\"/>\n"
|
||||
let s = s + " </marker>"
|
||||
s
|
||||
}
|
||||
|
||||
// ── Node rendering ─────────────────────────────────────────────────────────
|
||||
|
||||
fn render_node(buf: String, node: Map<String, Any>, layout: Map<String, Any>) -> String {
|
||||
let nid: String = node["id"]
|
||||
let pos: Map<String, Any> = el_map_get(layout, "node_pos_" + nid)
|
||||
let sz: Map<String, Any> = el_map_get(layout, "node_size_" + nid)
|
||||
|
||||
let cx: el_val_t = pos["x"]
|
||||
let cy: el_val_t = pos["y"]
|
||||
let w: el_val_t = sz["w"]
|
||||
let h: el_val_t = sz["h"]
|
||||
|
||||
let x: el_val_t = fsub(cx, fdiv2(w))
|
||||
let y: el_val_t = fsub(cy, fdiv2(h))
|
||||
|
||||
let fill_in: String = node["style_fill"]
|
||||
let stroke_in: String = node["style_stroke"]
|
||||
let color_in: String = node["style_color"]
|
||||
let fill = col_node_fill()
|
||||
if str_len(fill_in) > 0 { let fill = fill_in }
|
||||
let stroke = col_node_stroke()
|
||||
if str_len(stroke_in) > 0 { let stroke = stroke_in }
|
||||
let text_col = col_node_text()
|
||||
if str_len(color_in) > 0 { let text_col = color_in }
|
||||
|
||||
let shape: String = node["shape"]
|
||||
let buf = buf
|
||||
|
||||
if str_eq(shape, "rectangle") {
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"4\" fill=\"" + fill + "\" stroke=\"" + stroke
|
||||
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "rounded_rect") {
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"20\" fill=\"" + fill + "\" stroke=\"" + stroke
|
||||
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "stadium") {
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(h)) + "\" fill=\"" + fill
|
||||
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "cylinder") {
|
||||
// body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6
|
||||
// to stay in integer arithmetic — visually indistinguishable on the
|
||||
// canvas sizes the layout produces).
|
||||
let hi: Int = float_to_int(h)
|
||||
let ry: el_val_t = int_to_float(hi / 6)
|
||||
let body_y: el_val_t = fadd(y, ry)
|
||||
let body_h: el_val_t = fsub(h, ry)
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(body_y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(body_h)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
// top ellipse
|
||||
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(body_y)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
// bottom ellipse
|
||||
let bot_y: el_val_t = fadd(y, h)
|
||||
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(bot_y)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "diamond") {
|
||||
let hw: el_val_t = fdiv2(w)
|
||||
let hh: el_val_t = fdiv2(h)
|
||||
let buf = buf + " <polygon points=\""
|
||||
let buf = buf + pt(cx) + "," + pt(fsub(cy, hh)) + " "
|
||||
let buf = buf + pt(fadd(cx, hw)) + "," + pt(cy) + " "
|
||||
let buf = buf + pt(cx) + "," + pt(fadd(cy, hh)) + " "
|
||||
let buf = buf + pt(fsub(cx, hw)) + "," + pt(cy)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
|
||||
// Label.
|
||||
let label: String = node["label"]
|
||||
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(cy)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\">"
|
||||
let buf = buf + esc(label) + "</text>\n"
|
||||
|
||||
// Sublabel — Rust's DiagramNode stores Option<String>; El uses "" sentinel.
|
||||
let sub: String = node["sublabel"]
|
||||
if str_len(sub) > 0 {
|
||||
let sub_y: el_val_t = fadd(cy, int_to_float(14))
|
||||
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(sub_y)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\" font-size=\"10\">"
|
||||
let buf = buf + esc(sub) + "</text>\n"
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Edge rendering ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// We emit a straight line from one node centre to the other and let the
|
||||
// browser draw it; the Rust crate renders cubic bezier paths but the runtime
|
||||
// has no robust math layer, and the rectangles are large enough that
|
||||
// straight edges read clearly. (See "runtime gaps".)
|
||||
|
||||
fn render_edge(buf: String, edge: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
|
||||
let from_id: String = edge["from"]
|
||||
let to_id: String = edge["to"]
|
||||
let from_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + from_id)
|
||||
let to_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + to_id)
|
||||
|
||||
let fx: el_val_t = from_pos["x"]
|
||||
let fy: el_val_t = from_pos["y"]
|
||||
let tx: el_val_t = to_pos["x"]
|
||||
let ty: el_val_t = to_pos["y"]
|
||||
|
||||
let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id)
|
||||
let stroke = col_edge()
|
||||
if is_forbidden { let stroke = col_edge_forbidden() }
|
||||
|
||||
let line: String = edge["line"]
|
||||
let arrow: String = edge["arrow"]
|
||||
let dash_attr = ""
|
||||
if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" }
|
||||
if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" }
|
||||
|
||||
let marker_start = ""
|
||||
if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
|
||||
if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
|
||||
|
||||
let marker_end = " marker-end=\"url(#ah)\""
|
||||
if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" }
|
||||
if str_eq(arrow, "none") { let marker_end = "" }
|
||||
if str_eq(arrow, "backward") { let marker_end = "" }
|
||||
|
||||
let buf = buf + " <line x1=\"" + pt(fx) + "\" y1=\"" + pt(fy)
|
||||
let buf = buf + "\" x2=\"" + pt(tx) + "\" y2=\"" + pt(ty)
|
||||
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
|
||||
let buf = buf + dash_attr + marker_start + marker_end + "/>\n"
|
||||
|
||||
// Forbidden marker — circle-X at midpoint.
|
||||
if is_forbidden {
|
||||
let mx: el_val_t = fmid(fx, tx)
|
||||
let my: el_val_t = fmid(fy, ty)
|
||||
let r: el_val_t = int_to_float(7)
|
||||
let buf = buf + " <circle cx=\"" + pt(mx) + "\" cy=\"" + pt(my)
|
||||
let buf = buf + "\" r=\"" + pt(r) + "\" fill=\"white\" stroke=\""
|
||||
let buf = buf + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
|
||||
let off: el_val_t = int_to_float(4)
|
||||
let buf = buf + " <line x1=\"" + pt(fsub(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
|
||||
let buf = buf + "\" x2=\"" + pt(fadd(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
|
||||
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
|
||||
let buf = buf + " <line x1=\"" + pt(fadd(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
|
||||
let buf = buf + "\" x2=\"" + pt(fsub(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
|
||||
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
|
||||
// Edge label
|
||||
let label: String = edge["label"]
|
||||
if str_len(label) > 0 {
|
||||
let mx: el_val_t = fmid(fx, tx)
|
||||
let my: el_val_t = fmid(fy, ty)
|
||||
let lw: el_val_t = int_to_float(str_len(label) * 7 + 8)
|
||||
let lh: el_val_t = int_to_float(16)
|
||||
let buf = buf + " <rect x=\"" + pt(fsub(mx, fdiv2(lw))) + "\" y=\"" + pt(fsub(my, fdiv2(lh)))
|
||||
let buf = buf + "\" width=\"" + pt(lw) + "\" height=\"" + pt(lh)
|
||||
let buf = buf + "\" rx=\"3\" fill=\"white\" opacity=\"0.85\"/>\n"
|
||||
let buf = buf + " <text x=\"" + pt(mx) + "\" y=\"" + pt(my)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-edge-label\">" + esc(label) + "</text>\n"
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Group rendering ────────────────────────────────────────────────────────
|
||||
|
||||
fn render_group(buf: String, group: Map<String, Any>, layout: Map<String, Any>) -> String {
|
||||
let gid: String = group["id"]
|
||||
let bounds: Map<String, Any> = el_map_get(layout, "group_bounds_" + gid)
|
||||
// Layout may not have bounds for empty groups — defensive.
|
||||
let bx_check: el_val_t = bounds["x"]
|
||||
if float_to_int(bx_check) == 0 {
|
||||
// Could be a real 0; cheaper to skip via presence check on group_ids.
|
||||
}
|
||||
let bx: el_val_t = bounds["x"]
|
||||
let by: el_val_t = bounds["y"]
|
||||
let bw: el_val_t = bounds["w"]
|
||||
let bh: el_val_t = bounds["h"]
|
||||
let buf = buf + " <rect x=\"" + pt(bx) + "\" y=\"" + pt(by)
|
||||
let buf = buf + "\" width=\"" + pt(bw) + "\" height=\"" + pt(bh)
|
||||
let buf = buf + "\" rx=\"8\" fill=\"" + col_group_fill() + "\" stroke=\""
|
||||
let buf = buf + col_group_stroke() + "\" stroke-width=\"1\" stroke-dasharray=\"4,3\"/>\n"
|
||||
|
||||
// Group label in the top-left corner.
|
||||
let lx: el_val_t = fadd(bx, int_to_float(8))
|
||||
let ly: el_val_t = fadd(by, int_to_float(14))
|
||||
let label: String = group["label"]
|
||||
let buf = buf + " <text x=\"" + pt(lx) + "\" y=\"" + pt(ly)
|
||||
let buf = buf + "\" class=\"arbor-group-label\">" + esc(label) + "</text>\n"
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Public entry point ─────────────────────────────────────────────────────
|
||||
|
||||
fn arbor_render_svg(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
|
||||
let canvas: Map<String, Any> = el_map_get(layout, "canvas")
|
||||
let cw: el_val_t = canvas["w"]
|
||||
let ch: el_val_t = canvas["h"]
|
||||
|
||||
let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + pt(cw)
|
||||
let buf = buf + "\" height=\"" + pt(ch) + "\" viewBox=\"0 0 " + pt(cw) + " " + pt(ch) + "\">\n"
|
||||
let buf = buf + " <defs>"
|
||||
let buf = buf + arrow_defs()
|
||||
let buf = buf + "\n <style>\n"
|
||||
let buf = buf + " .arbor-node-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; }\n"
|
||||
let buf = buf + " .arbor-group-label { font-family: 'Helvetica Neue', Helvetica, Arial, monospace; font-size: 10px; fill: " + col_group_text() + "; letter-spacing: 0.08em; }\n"
|
||||
let buf = buf + " .arbor-edge-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; fill: " + col_edge_label() + "; }\n"
|
||||
let buf = buf + " </style>\n"
|
||||
let buf = buf + " </defs>\n"
|
||||
|
||||
// Groups first (behind everything).
|
||||
let buf = buf + " <!-- Groups -->\n"
|
||||
let groups: [Map<String, Any>] = graph["groups"]
|
||||
let gn: Int = el_list_len(groups)
|
||||
let i = 0
|
||||
while i < gn {
|
||||
let g: Map<String, Any> = get(groups, i)
|
||||
let gid: String = g["id"]
|
||||
// Only render groups the layout actually placed.
|
||||
let gids: [String] = el_map_get(layout, "group_ids")
|
||||
let placed = false
|
||||
let j = 0
|
||||
while j < el_list_len(gids) {
|
||||
if str_eq(get(gids, j), gid) { let placed = true }
|
||||
let j = j + 1
|
||||
}
|
||||
if placed {
|
||||
let buf = render_group(buf, g, layout)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Edges
|
||||
let buf = buf + " <!-- Edges -->\n"
|
||||
let edges: [Map<String, Any>] = graph["edges"]
|
||||
let en: Int = el_list_len(edges)
|
||||
let i = 0
|
||||
while i < en {
|
||||
let e: Map<String, Any> = get(edges, i)
|
||||
let buf = render_edge(buf, e, layout, forbidden)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Nodes
|
||||
let buf = buf + " <!-- Nodes -->\n"
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let nn: Int = el_list_len(nodes)
|
||||
let i = 0
|
||||
while i < nn {
|
||||
let n: Map<String, Any> = get(nodes, i)
|
||||
let buf = render_node(buf, n, layout)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Title
|
||||
let title: String = graph["title"]
|
||||
if str_len(title) > 0 {
|
||||
let title_x: el_val_t = fdiv2(cw)
|
||||
let buf = buf + " <text x=\"" + pt(title_x) + "\" y=\"22\" text-anchor=\"middle\""
|
||||
let buf = buf + " font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\""
|
||||
let buf = buf + " font-size=\"15\" font-weight=\"600\" fill=\"" + col_node_text() + "\">"
|
||||
let buf = buf + esc(title) + "</text>\n"
|
||||
}
|
||||
|
||||
let buf = buf + "</svg>\n"
|
||||
buf
|
||||
}
|
||||
|
||||
// PNG — not implemented; the runtime has no SVG rasterizer or PNG encoder.
|
||||
// Returns an error map that callers can inspect via map["error"].
|
||||
fn arbor_render_png(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> Map<String, Any> {
|
||||
{
|
||||
"error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary"
|
||||
}
|
||||
}
|
||||
|
||||
// ── Smoke test ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn fail(label: String, msg: String) -> Int {
|
||||
println("FAIL " + label + ": " + msg)
|
||||
state_set("smoke_failures", "1")
|
||||
0
|
||||
}
|
||||
|
||||
fn check_contains(label: String, haystack: String, needle: String) -> Int {
|
||||
if str_contains(haystack, needle) {
|
||||
println("ok " + label)
|
||||
return 1
|
||||
}
|
||||
fail(label, "missing [" + needle + "]")
|
||||
}
|
||||
|
||||
fn check_not_contains(label: String, haystack: String, needle: String) -> Int {
|
||||
if str_contains(haystack, needle) {
|
||||
return fail(label, "should not contain [" + needle + "]")
|
||||
}
|
||||
println("ok " + label)
|
||||
1
|
||||
}
|
||||
|
||||
fn make_test_node(id: String, label: String, shape: String) -> Map<String, Any> {
|
||||
{
|
||||
"id": id, "label": label, "sublabel": "",
|
||||
"shape": shape,
|
||||
"style_fill": "", "style_stroke": "", "style_color": ""
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map<String, Any> {
|
||||
{
|
||||
"from": src, "to": dst, "label": label,
|
||||
"line": line, "arrow": arrow
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_pos(x: Int, y: Int) -> Map<String, Any> {
|
||||
{ "x": int_to_float(x), "y": int_to_float(y) }
|
||||
}
|
||||
|
||||
fn make_test_size(w: Int, h: Int) -> Map<String, Any> {
|
||||
{ "w": int_to_float(w), "h": int_to_float(h) }
|
||||
}
|
||||
|
||||
// Build a minimal layout map by hand.
|
||||
fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map<String, Any> {
|
||||
let r: Map<String, Any> = el_map_new(0)
|
||||
let r = el_map_set(r, "node_ids", node_ids)
|
||||
let r = el_map_set(r, "group_ids", group_ids)
|
||||
let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) })
|
||||
r
|
||||
}
|
||||
|
||||
let n_a: Map<String, Any> = make_test_node("a", "Node A", "rectangle")
|
||||
let n_b: Map<String, Any> = make_test_node("b", "Node B", "rectangle")
|
||||
let e_ab: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
|
||||
|
||||
let nodes: [Map<String, Any>] = native_list_empty()
|
||||
let nodes = native_list_append(nodes, n_a)
|
||||
let nodes = native_list_append(nodes, n_b)
|
||||
let edges: [Map<String, Any>] = native_list_empty()
|
||||
let edges = native_list_append(edges, e_ab)
|
||||
let groups: [Map<String, Any>] = native_list_empty()
|
||||
|
||||
let g: Map<String, Any> = {
|
||||
"title": "Test", "direction": "top-down",
|
||||
"nodes": nodes, "edges": edges, "groups": groups
|
||||
}
|
||||
|
||||
let nid_list: [String] = native_list_empty()
|
||||
let nid_list = native_list_append(nid_list, "a")
|
||||
let nid_list = native_list_append(nid_list, "b")
|
||||
let gid_list: [String] = native_list_empty()
|
||||
let layout: Map<String, Any> = build_layout(nid_list, gid_list, 400, 300)
|
||||
let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60))
|
||||
let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200))
|
||||
let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40))
|
||||
let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40))
|
||||
|
||||
let forbidden: [String] = native_list_empty()
|
||||
let svg: String = arbor_render_svg(g, layout, forbidden)
|
||||
|
||||
check_contains("svg starts with <svg", svg, "<svg xmlns=")
|
||||
check_contains("svg ends with </svg>", svg, "</svg>")
|
||||
check_contains("svg contains node label", svg, "Node A")
|
||||
check_contains("svg contains title", svg, ">Test</text>")
|
||||
check_contains("svg has rect for rectangle node", svg, "<rect")
|
||||
check_contains("svg has line for edge", svg, "<line")
|
||||
check_contains("svg has arrow marker def", svg, "id=\"ah\"")
|
||||
|
||||
// Escape test
|
||||
let n_esc: Map<String, Any> = make_test_node("x", "A & B <C>", "rectangle")
|
||||
let nodes2: [Map<String, Any>] = native_list_empty()
|
||||
let nodes2 = native_list_append(nodes2, n_esc)
|
||||
let g2: Map<String, Any> = {
|
||||
"title": "Test <Title>", "direction": "top-down",
|
||||
"nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty()
|
||||
}
|
||||
let nid2: [String] = native_list_empty()
|
||||
let nid2 = native_list_append(nid2, "x")
|
||||
let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100)
|
||||
let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40))
|
||||
let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40))
|
||||
let svg2: String = arbor_render_svg(g2, layout2, native_list_empty())
|
||||
check_contains("escapes ampersand", svg2, "&")
|
||||
check_contains("escapes <", svg2, "<")
|
||||
check_not_contains("no raw <C>", svg2, "<C>")
|
||||
|
||||
// Forbidden edge
|
||||
let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
|
||||
let edges3: [Map<String, Any>] = native_list_empty()
|
||||
let edges3 = native_list_append(edges3, e_fb)
|
||||
let g3: Map<String, Any> = {
|
||||
"title": "F", "direction": "top-down",
|
||||
"nodes": nodes, "edges": edges3, "groups": native_list_empty()
|
||||
}
|
||||
let fb: [String] = native_list_empty()
|
||||
let fb = native_list_append(fb, forbidden_key("a", "b"))
|
||||
let svg3: String = arbor_render_svg(g3, layout, fb)
|
||||
check_contains("forbidden uses red marker", svg3, "ah-red")
|
||||
check_contains("forbidden colour present", svg3, col_edge_forbidden())
|
||||
|
||||
// Diamond shape → polygon
|
||||
let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond")
|
||||
let g4: Map<String, Any> = {
|
||||
"title": "", "direction": "top-down",
|
||||
"nodes": native_list_append(native_list_empty(), n_d),
|
||||
"edges": native_list_empty(), "groups": native_list_empty()
|
||||
}
|
||||
let nid4: [String] = native_list_append(native_list_empty(), "d")
|
||||
let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100)
|
||||
let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50))
|
||||
let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40))
|
||||
let svg4: String = arbor_render_svg(g4, layout4, native_list_empty())
|
||||
check_contains("diamond uses polygon", svg4, "<polygon")
|
||||
|
||||
// Cylinder shape → ellipses
|
||||
let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder")
|
||||
let g5: Map<String, Any> = {
|
||||
"title": "", "direction": "top-down",
|
||||
"nodes": native_list_append(native_list_empty(), n_cy),
|
||||
"edges": native_list_empty(), "groups": native_list_empty()
|
||||
}
|
||||
let nid5: [String] = native_list_append(native_list_empty(), "cy")
|
||||
let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100)
|
||||
let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50))
|
||||
let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40))
|
||||
let svg5: String = arbor_render_svg(g5, layout5, native_list_empty())
|
||||
check_contains("cylinder uses ellipse", svg5, "<ellipse")
|
||||
|
||||
// Dashed edge
|
||||
let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "")
|
||||
let g6: Map<String, Any> = {
|
||||
"title": "", "direction": "top-down",
|
||||
"nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash),
|
||||
"groups": native_list_empty()
|
||||
}
|
||||
let svg6: String = arbor_render_svg(g6, layout, native_list_empty())
|
||||
check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"")
|
||||
|
||||
// PNG returns an error map
|
||||
let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty())
|
||||
let err: String = png["error"]
|
||||
if str_len(err) > 0 {
|
||||
println("ok PNG returns error map")
|
||||
} else {
|
||||
println("FAIL PNG should have returned error")
|
||||
state_set("smoke_failures", "1")
|
||||
}
|
||||
|
||||
println("")
|
||||
let f: String = state_get("smoke_failures")
|
||||
if str_eq(f, "1") {
|
||||
println("arbor-render: FAILED")
|
||||
exit_program(1)
|
||||
} else {
|
||||
println("arbor-render: ok")
|
||||
}
|
||||
Vendored
+765
-2124
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
+437
-35
@@ -129,6 +129,9 @@ el_val_t js_str_lit(el_val_t s);
|
||||
el_val_t js_emit_line(el_val_t line);
|
||||
el_val_t js_emit_blank(void);
|
||||
el_val_t js_binop(el_val_t op);
|
||||
el_val_t js_is_async_builtin(el_val_t name);
|
||||
el_val_t js_register_async_fn(el_val_t name);
|
||||
el_val_t js_is_async_fn(el_val_t name);
|
||||
el_val_t js_is_int_name(el_val_t name);
|
||||
el_val_t js_add_int_name(el_val_t name);
|
||||
el_val_t js_build_int_names_for_params(el_val_t params);
|
||||
@@ -149,17 +152,32 @@ el_val_t js_cg_fn(el_val_t stmt);
|
||||
el_val_t js_is_fndef(el_val_t stmt);
|
||||
el_val_t js_is_top_level_decl(el_val_t stmt);
|
||||
el_val_t codegen_js(el_val_t stmts, el_val_t source);
|
||||
el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content);
|
||||
el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content);
|
||||
el_val_t js_strip_es_exports(el_val_t content);
|
||||
el_val_t compile(el_val_t source);
|
||||
el_val_t compile_js(el_val_t source);
|
||||
el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path);
|
||||
el_val_t compile_dispatch(el_val_t tgt, el_val_t source);
|
||||
el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path);
|
||||
el_val_t detect_target(el_val_t argv);
|
||||
el_val_t strip_flags(el_val_t argv);
|
||||
el_val_t detect_emit_header(el_val_t argv);
|
||||
el_val_t detect_bundle(el_val_t argv);
|
||||
el_val_t detect_minify(el_val_t argv);
|
||||
el_val_t detect_obfuscate(el_val_t argv);
|
||||
el_val_t make_temp_path(el_val_t suffix);
|
||||
el_val_t js_reserved_names(void);
|
||||
el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir);
|
||||
el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir);
|
||||
el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir);
|
||||
el_val_t resolve_runtime_path(el_val_t src_path);
|
||||
el_val_t type_node_to_el(el_val_t t);
|
||||
el_val_t emit_header(el_val_t stmts, el_val_t hdr_path);
|
||||
el_val_t dirname_of(el_val_t path);
|
||||
el_val_t parse_import_line(el_val_t trimmed, el_val_t dir);
|
||||
el_val_t resolve_imports(el_val_t src_path);
|
||||
el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional);
|
||||
|
||||
el_val_t lex_is_digit(el_val_t ch) {
|
||||
if (str_eq(ch, EL_STR("0"))) {
|
||||
@@ -1474,6 +1492,11 @@ el_val_t parse_pattern(el_val_t tokens, el_val_t pos) {
|
||||
if (str_eq(v, EL_STR("_"))) {
|
||||
return make_result(el_map_new(1, "pattern", EL_STR("Wildcard")), (pos + 1));
|
||||
}
|
||||
el_val_t next_k = tok_kind(tokens, (pos + 1));
|
||||
if (str_eq(next_k, EL_STR("ColonColon"))) {
|
||||
el_val_t variant_name = tok_value(tokens, (pos + 2));
|
||||
return make_result(el_map_new(3, "pattern", EL_STR("Variant"), "enum_name", v, "variant", variant_name), (pos + 3));
|
||||
}
|
||||
return make_result(el_map_new(2, "pattern", EL_STR("Binding"), "name", v), (pos + 1));
|
||||
}
|
||||
if (str_eq(k, EL_STR("Int"))) {
|
||||
@@ -1855,6 +1878,10 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) {
|
||||
el_val_t p = (pos + 1);
|
||||
el_val_t name = tok_value(tokens, p);
|
||||
p = (p + 1);
|
||||
el_val_t pk = tok_kind(tokens, p);
|
||||
if (str_eq(pk, EL_STR("Eq"))) {
|
||||
p = (p + 1);
|
||||
}
|
||||
p = expect(tokens, p, EL_STR("LBrace"));
|
||||
el_val_t fields = native_list_empty();
|
||||
el_val_t running = 1;
|
||||
@@ -2923,7 +2950,12 @@ el_val_t cg_match(el_val_t expr) {
|
||||
}
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if ("), subj_var), EL_STR(" == ")), bv), EL_STR(") { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
|
||||
if (str_eq(pkind, EL_STR("Variant"))) {
|
||||
el_val_t variant = el_get_field(pat, EL_STR("variant"));
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if (str_eq("), subj_var), EL_STR(", EL_STR(")), c_str_lit(variant)), EL_STR("))) { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4569,6 +4601,15 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("exit_program"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("getpid_now"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(name, EL_STR("stdout_to_file"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("stdout_restore"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(name, EL_STR("exec_command"))) {
|
||||
return 1;
|
||||
}
|
||||
@@ -5594,6 +5635,49 @@ el_val_t js_binop(el_val_t op) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_is_async_builtin(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("http_get"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_post"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_post_json"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_get_with_headers"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_post_with_headers"))) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_register_async_fn(el_val_t name) {
|
||||
el_val_t csv = state_get(EL_STR("__js_async_fns"));
|
||||
if (str_eq(csv, EL_STR(""))) {
|
||||
csv = EL_STR(",");
|
||||
}
|
||||
el_val_t key = el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","));
|
||||
if (str_contains(csv, key)) {
|
||||
return 1;
|
||||
}
|
||||
state_set(EL_STR("__js_async_fns"), el_str_concat(el_str_concat(csv, name), EL_STR(",")));
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_is_async_fn(el_val_t name) {
|
||||
el_val_t csv = state_get(EL_STR("__js_async_fns"));
|
||||
if (str_eq(csv, EL_STR(""))) {
|
||||
return 0;
|
||||
}
|
||||
return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(",")));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_is_int_name(el_val_t name) {
|
||||
el_val_t csv = state_get(EL_STR("__js_int_names"));
|
||||
if (str_eq(csv, EL_STR(""))) {
|
||||
@@ -5937,7 +6021,14 @@ el_val_t js_cg_expr(el_val_t expr) {
|
||||
el_val_t args_c = str_join(args_parts, EL_STR(", "));
|
||||
if (str_eq(func_kind, EL_STR("Ident"))) {
|
||||
el_val_t fn_name = el_get_field(func, EL_STR("name"));
|
||||
return el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")"));
|
||||
el_val_t call_expr = el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")"));
|
||||
if (js_is_async_builtin(fn_name)) {
|
||||
return el_str_concat(EL_STR("await "), call_expr);
|
||||
}
|
||||
if (js_is_async_fn(fn_name)) {
|
||||
return el_str_concat(EL_STR("await "), call_expr);
|
||||
}
|
||||
return call_expr;
|
||||
}
|
||||
if (str_eq(func_kind, EL_STR("Field"))) {
|
||||
el_val_t obj = el_get_field(func, EL_STR("object"));
|
||||
@@ -5954,6 +6045,12 @@ el_val_t js_cg_expr(el_val_t expr) {
|
||||
if (str_eq(kind, EL_STR("Field"))) {
|
||||
el_val_t obj = el_get_field(expr, EL_STR("object"));
|
||||
el_val_t field = el_get_field(expr, EL_STR("field"));
|
||||
el_val_t obj_kind = el_get_field(obj, EL_STR("expr"));
|
||||
if (str_eq(obj_kind, EL_STR("Try"))) {
|
||||
el_val_t inner = el_get_field(obj, EL_STR("inner"));
|
||||
el_val_t inner_c = js_cg_expr(inner);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), js_str_lit(field)), EL_STR("] ?? null"));
|
||||
}
|
||||
el_val_t obj_c = js_cg_expr(obj);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), js_str_lit(field)), EL_STR(")"));
|
||||
}
|
||||
@@ -5963,6 +6060,12 @@ el_val_t js_cg_expr(el_val_t expr) {
|
||||
el_val_t obj_c = js_cg_expr(obj);
|
||||
el_val_t idx_c = js_cg_expr(idx);
|
||||
el_val_t idx_kind = el_get_field(idx, EL_STR("expr"));
|
||||
el_val_t obj_kind = el_get_field(obj, EL_STR("expr"));
|
||||
if (str_eq(obj_kind, EL_STR("Try"))) {
|
||||
el_val_t inner = el_get_field(obj, EL_STR("inner"));
|
||||
el_val_t inner_c = js_cg_expr(inner);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), idx_c), EL_STR("] ?? null"));
|
||||
}
|
||||
if (str_eq(idx_kind, EL_STR("Str"))) {
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), idx_c), EL_STR(")"));
|
||||
}
|
||||
@@ -6069,7 +6172,12 @@ el_val_t js_cg_match(el_val_t expr) {
|
||||
}
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if ("), subj_var), EL_STR(" === ")), bv), EL_STR(") return (")), body_c), EL_STR("); ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); ")));
|
||||
if (str_eq(pkind, EL_STR("Variant"))) {
|
||||
el_val_t variant = el_get_field(pat, EL_STR("variant"));
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if (str_eq("), subj_var), EL_STR(", ")), js_str_lit(variant)), EL_STR(")) return (")), body_c), EL_STR("); ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6182,12 +6290,12 @@ el_val_t js_cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) {
|
||||
}
|
||||
if (str_eq(kind, EL_STR("CgiBlock"))) {
|
||||
el_val_t cname = el_get_field(stmt, EL_STR("name"));
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' - no-op in JS target (server-side concept)")));
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' \xe2\x80\x94 no-op in JS target (server-side concept)")));
|
||||
return declared;
|
||||
}
|
||||
if (str_eq(kind, EL_STR("ServiceBlock"))) {
|
||||
el_val_t sname = el_get_field(stmt, EL_STR("name"));
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' - no-op in JS target")));
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' \xe2\x80\x94 no-op in JS target")));
|
||||
return declared;
|
||||
}
|
||||
return declared;
|
||||
@@ -6330,12 +6438,22 @@ el_val_t js_cg_fn(el_val_t stmt) {
|
||||
el_val_t params = el_get_field(stmt, EL_STR("params"));
|
||||
el_val_t body = el_get_field(stmt, EL_STR("body"));
|
||||
el_val_t ret_type = el_get_field(stmt, EL_STR("ret_type"));
|
||||
el_val_t decorator = el_get_field(stmt, EL_STR("decorator"));
|
||||
el_val_t params_str = js_params_str(params);
|
||||
js_build_int_names_for_params(params);
|
||||
if (str_eq(fn_name, EL_STR("main"))) {
|
||||
js_emit_line(el_str_concat(el_str_concat(EL_STR("function main("), params_str), EL_STR(") {")));
|
||||
if (str_eq(decorator, EL_STR("async"))) {
|
||||
js_register_async_fn(fn_name);
|
||||
if (str_eq(fn_name, EL_STR("main"))) {
|
||||
js_emit_line(el_str_concat(el_str_concat(EL_STR("async function main("), params_str), EL_STR(") {")));
|
||||
} else {
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("async function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
|
||||
}
|
||||
} else {
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
|
||||
if (str_eq(fn_name, EL_STR("main"))) {
|
||||
js_emit_line(el_str_concat(el_str_concat(EL_STR("function main("), params_str), EL_STR(") {")));
|
||||
} else {
|
||||
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
|
||||
}
|
||||
}
|
||||
el_val_t decl = native_list_empty();
|
||||
el_val_t np = native_list_len(params);
|
||||
@@ -6387,35 +6505,81 @@ el_val_t js_is_top_level_decl(el_val_t stmt) {
|
||||
}
|
||||
|
||||
el_val_t codegen_js(el_val_t stmts, el_val_t source) {
|
||||
return codegen_js_inner(stmts, source, 0, EL_STR(""));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content) {
|
||||
return codegen_js_inner(stmts, source, 1, runtime_content);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content) {
|
||||
state_set(EL_STR("__js_int_names"), EL_STR(""));
|
||||
state_set(EL_STR("__js_match_counter"), EL_STR(""));
|
||||
state_set(EL_STR("__js_async_fns"), EL_STR(""));
|
||||
js_emit_line(EL_STR("// Generated by elc --target=js"));
|
||||
js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js"));
|
||||
js_emit_line(EL_STR("import \"./el_runtime.js\";"));
|
||||
js_emit_line(EL_STR("const {"));
|
||||
js_emit_line(EL_STR(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,"));
|
||||
js_emit_line(EL_STR(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,"));
|
||||
js_emit_line(EL_STR(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,"));
|
||||
js_emit_line(EL_STR(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,"));
|
||||
js_emit_line(EL_STR(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,"));
|
||||
js_emit_line(EL_STR(" list_push, list_join, list_range,"));
|
||||
js_emit_line(EL_STR(" el_map_new, el_get_field, el_map_get, el_map_set,"));
|
||||
js_emit_line(EL_STR(" http_get, http_post, http_post_json,"));
|
||||
js_emit_line(EL_STR(" fs_read, fs_write, fs_list,"));
|
||||
js_emit_line(EL_STR(" json_parse, json_stringify, json_get, json_get_string, json_get_int,"));
|
||||
js_emit_line(EL_STR(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,"));
|
||||
js_emit_line(EL_STR(" el_retain, el_release,"));
|
||||
js_emit_line(EL_STR(" append, len, get, map_get, map_set,"));
|
||||
js_emit_line(EL_STR(" native_list_get, native_list_len, native_list_append, native_list_empty,"));
|
||||
js_emit_line(EL_STR(" native_list_clone, native_string_chars, native_int_to_str,"));
|
||||
js_emit_line(EL_STR(" args, state_set, state_get, state_del, state_keys, env,"));
|
||||
js_emit_line(EL_STR(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,"));
|
||||
js_emit_line(EL_STR(" engram_node, engram_search, engram_activate,"));
|
||||
js_emit_line(EL_STR(" llm_call, llm_call_system,"));
|
||||
js_emit_line(EL_STR("} = globalThis.__el;"));
|
||||
js_emit_blank();
|
||||
if (bundle_mode) {
|
||||
js_emit_line(EL_STR("// Bundle mode: runtime inlined, no import statement needed."));
|
||||
js_emit_line(EL_STR(""));
|
||||
js_emit_line(EL_STR(";(function() {"));
|
||||
js_emit_line(EL_STR("\"use strict\";"));
|
||||
js_emit_line(js_strip_es_exports(runtime_content));
|
||||
js_emit_line(EL_STR(""));
|
||||
} else {
|
||||
js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js"));
|
||||
js_emit_line(EL_STR("import \"./el_runtime.js\";"));
|
||||
}
|
||||
if (!bundle_mode) {
|
||||
js_emit_line(EL_STR("const {"));
|
||||
js_emit_line(EL_STR(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,"));
|
||||
js_emit_line(EL_STR(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,"));
|
||||
js_emit_line(EL_STR(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,"));
|
||||
js_emit_line(EL_STR(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,"));
|
||||
js_emit_line(EL_STR(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,"));
|
||||
js_emit_line(EL_STR(" list_push, list_join, list_range,"));
|
||||
js_emit_line(EL_STR(" el_map_new, el_get_field, el_map_get, el_map_set,"));
|
||||
js_emit_line(EL_STR(" http_get, http_post, http_post_json,"));
|
||||
js_emit_line(EL_STR(" fs_read, fs_write, fs_list,"));
|
||||
js_emit_line(EL_STR(" json_parse, json_stringify, json_get, json_get_string, json_get_int,"));
|
||||
js_emit_line(EL_STR(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,"));
|
||||
js_emit_line(EL_STR(" el_retain, el_release,"));
|
||||
js_emit_line(EL_STR(" append, len, get, map_get, map_set,"));
|
||||
js_emit_line(EL_STR(" native_list_get, native_list_len, native_list_append, native_list_empty,"));
|
||||
js_emit_line(EL_STR(" native_list_clone, native_string_chars, native_int_to_str,"));
|
||||
js_emit_line(EL_STR(" args, state_set, state_get, state_del, state_keys, env,"));
|
||||
js_emit_line(EL_STR(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,"));
|
||||
js_emit_line(EL_STR(" engram_node, engram_search, engram_activate,"));
|
||||
js_emit_line(EL_STR(" llm_call, llm_call_system,"));
|
||||
js_emit_line(EL_STR(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,"));
|
||||
js_emit_line(EL_STR(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,"));
|
||||
js_emit_line(EL_STR(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,"));
|
||||
js_emit_line(EL_STR(" dom_append, dom_remove, dom_is_null,"));
|
||||
js_emit_line(EL_STR(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,"));
|
||||
js_emit_line(EL_STR(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,"));
|
||||
js_emit_line(EL_STR(" set_timeout, set_interval, clear_interval,"));
|
||||
js_emit_line(EL_STR(" local_storage_get, local_storage_set, local_storage_remove,"));
|
||||
js_emit_line(EL_STR(" window_location, window_redirect, window_on_load,"));
|
||||
js_emit_line(EL_STR(" console_log,"));
|
||||
js_emit_line(EL_STR(" window_set, window_get, native_js, native_js_call,"));
|
||||
js_emit_line(EL_STR("} = globalThis.__el;"));
|
||||
js_emit_blank();
|
||||
}
|
||||
el_val_t n = native_list_len(stmts);
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t stmt = native_list_get(stmts, i);
|
||||
el_val_t sk = el_get_field(stmt, EL_STR("stmt"));
|
||||
if (str_eq(sk, EL_STR("FnDef"))) {
|
||||
el_val_t dec = el_get_field(stmt, EL_STR("decorator"));
|
||||
if (str_eq(dec, EL_STR("async"))) {
|
||||
el_val_t aname = el_get_field(stmt, EL_STR("name"));
|
||||
js_register_async_fn(aname);
|
||||
}
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
i = 0;
|
||||
while (i < n) {
|
||||
el_val_t stmt = native_list_get(stmts, i);
|
||||
if (js_is_fndef(stmt)) {
|
||||
@@ -6453,10 +6617,37 @@ el_val_t codegen_js(el_val_t stmts, el_val_t source) {
|
||||
js_emit_blank();
|
||||
js_emit_line(EL_STR("main();"));
|
||||
}
|
||||
if (bundle_mode) {
|
||||
js_emit_line(EL_STR(""));
|
||||
js_emit_line(EL_STR("})();"));
|
||||
}
|
||||
return EL_STR("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_strip_es_exports(el_val_t content) {
|
||||
el_val_t lines = str_split(content, EL_STR("\n"));
|
||||
el_val_t n = native_list_len(lines);
|
||||
el_val_t out = native_list_empty();
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t line = native_list_get(lines, i);
|
||||
el_val_t trimmed = str_trim(line);
|
||||
if (str_starts_with(trimmed, EL_STR("export {"))) {
|
||||
i = n;
|
||||
} else {
|
||||
if (str_starts_with(trimmed, EL_STR("export default"))) {
|
||||
i = n;
|
||||
} else {
|
||||
out = native_list_append(out, line);
|
||||
}
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return str_join(out, EL_STR("\n"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t compile(el_val_t source) {
|
||||
el_val_t tokens = lex(source);
|
||||
el_val_t stmts = parse(tokens);
|
||||
@@ -6473,6 +6664,19 @@ el_val_t compile_js(el_val_t source) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path) {
|
||||
el_val_t tokens = lex(source);
|
||||
el_val_t stmts = parse(tokens);
|
||||
el_release(tokens);
|
||||
el_val_t runtime_content = fs_read(runtime_path);
|
||||
if (str_eq(runtime_content, EL_STR(""))) {
|
||||
println(el_str_concat(EL_STR("el-compiler: warning: --bundle: could not read runtime at "), runtime_path));
|
||||
println(EL_STR("el-compiler: warning: bundle output will be incomplete"));
|
||||
}
|
||||
return codegen_js_bundle(stmts, source, runtime_content);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t compile_dispatch(el_val_t tgt, el_val_t source) {
|
||||
if (str_eq(tgt, EL_STR("js"))) {
|
||||
return compile_js(source);
|
||||
@@ -6481,6 +6685,14 @@ el_val_t compile_dispatch(el_val_t tgt, el_val_t source) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path) {
|
||||
if (str_eq(tgt, EL_STR("js"))) {
|
||||
return compile_js_with_bundle(source, runtime_path);
|
||||
}
|
||||
return compile(source);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t detect_target(el_val_t argv) {
|
||||
el_val_t n = native_list_len(argv);
|
||||
el_val_t i = 0;
|
||||
@@ -6525,6 +6737,127 @@ el_val_t detect_emit_header(el_val_t argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t detect_bundle(el_val_t argv) {
|
||||
el_val_t n = native_list_len(argv);
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t a = native_list_get(argv, i);
|
||||
if (str_eq(a, EL_STR("--bundle"))) {
|
||||
return 1;
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t detect_minify(el_val_t argv) {
|
||||
el_val_t n = native_list_len(argv);
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t a = native_list_get(argv, i);
|
||||
if (str_eq(a, EL_STR("--minify"))) {
|
||||
return 1;
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t detect_obfuscate(el_val_t argv) {
|
||||
el_val_t n = native_list_len(argv);
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t a = native_list_get(argv, i);
|
||||
if (str_eq(a, EL_STR("--obfuscate"))) {
|
||||
return 1;
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t make_temp_path(el_val_t suffix) {
|
||||
el_val_t pid = getpid_now();
|
||||
el_val_t ts = time_now();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("/tmp/elc-"), native_int_to_str(pid)), EL_STR("-")), native_int_to_str(ts)), EL_STR(".")), suffix);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t js_reserved_names(void) {
|
||||
return EL_STR("neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir) {
|
||||
el_val_t cand1 = el_str_concat(el_str_concat(src_dir, EL_STR("/node_modules/.bin/")), tool_name);
|
||||
el_val_t check1 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand1), EL_STR(" && echo yes 2>/dev/null"))));
|
||||
if (str_eq(check1, EL_STR("yes"))) {
|
||||
return cand1;
|
||||
}
|
||||
el_val_t parent_dir = dirname_of(src_dir);
|
||||
el_val_t cand2 = el_str_concat(el_str_concat(parent_dir, EL_STR("/node_modules/.bin/")), tool_name);
|
||||
el_val_t check2 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand2), EL_STR(" && echo yes 2>/dev/null"))));
|
||||
if (str_eq(check2, EL_STR("yes"))) {
|
||||
return cand2;
|
||||
}
|
||||
el_val_t npx_path = str_trim(exec_capture(EL_STR("which npx 2>/dev/null")));
|
||||
if (!str_eq(npx_path, EL_STR(""))) {
|
||||
return el_str_concat(EL_STR("npx --yes "), tool_name);
|
||||
}
|
||||
return EL_STR("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir) {
|
||||
el_val_t terser = find_node_tool(EL_STR("terser"), src_dir);
|
||||
if (str_eq(terser, EL_STR(""))) {
|
||||
println(EL_STR("el-compiler: error: terser not found. Run 'npm install terser' in your project directory."));
|
||||
return 0;
|
||||
}
|
||||
el_val_t names = js_reserved_names();
|
||||
el_val_t compress_opts = EL_STR("passes=2,drop_console=false,drop_debugger=true");
|
||||
el_val_t mangle_reserved = el_str_concat(el_str_concat(EL_STR("'reserved=["), names), EL_STR("]'"));
|
||||
el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(terser, EL_STR(" ")), js_path), EL_STR(" --compress ")), compress_opts), EL_STR(" --mangle ")), mangle_reserved), EL_STR(" --output ")), out_path);
|
||||
el_val_t ret = exec_command(cmd);
|
||||
if (ret == 0) {
|
||||
return 1;
|
||||
}
|
||||
println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: terser failed (exit "), native_int_to_str(ret)), EL_STR(")")));
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir) {
|
||||
el_val_t obfuscator = find_node_tool(EL_STR("javascript-obfuscator"), src_dir);
|
||||
if (str_eq(obfuscator, EL_STR(""))) {
|
||||
println(EL_STR("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory."));
|
||||
return 0;
|
||||
}
|
||||
el_val_t names = js_reserved_names();
|
||||
el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(obfuscator, EL_STR(" ")), js_path), EL_STR(" --output ")), out_path), EL_STR(" --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names ")), names);
|
||||
el_val_t ret = exec_command(cmd);
|
||||
if (ret == 0) {
|
||||
return 1;
|
||||
}
|
||||
println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: javascript-obfuscator failed (exit "), native_int_to_str(ret)), EL_STR(")")));
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t resolve_runtime_path(el_val_t src_path) {
|
||||
el_val_t src_dir = dirname_of(src_path);
|
||||
el_val_t candidate = el_str_concat(src_dir, EL_STR("/el_runtime.js"));
|
||||
el_val_t existing = fs_read(candidate);
|
||||
if (!str_eq(existing, EL_STR(""))) {
|
||||
return candidate;
|
||||
}
|
||||
return EL_STR("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t type_node_to_el(el_val_t t) {
|
||||
el_val_t k = el_get_field(t, EL_STR("kind"));
|
||||
if (str_eq(k, EL_STR("Simple"))) {
|
||||
@@ -6547,7 +6880,7 @@ el_val_t emit_header(el_val_t stmts, el_val_t hdr_path) {
|
||||
el_val_t n = native_list_len(stmts);
|
||||
el_val_t i = 0;
|
||||
el_val_t parts = native_list_empty();
|
||||
parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header - do not edit\n"));
|
||||
parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header \xe2\x80\x94 do not edit\n"));
|
||||
while (i < n) {
|
||||
el_val_t stmt = native_list_get(stmts, i);
|
||||
el_val_t kind = el_get_field(stmt, EL_STR("stmt"));
|
||||
@@ -6661,17 +6994,76 @@ el_val_t resolve_imports(el_val_t src_path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional) {
|
||||
el_val_t src_dir = dirname_of(src_path);
|
||||
el_val_t tmp_gen = make_temp_path(EL_STR("js"));
|
||||
el_val_t tmp_min = make_temp_path(EL_STR("min.js"));
|
||||
stdout_to_file(tmp_gen);
|
||||
if (do_bundle) {
|
||||
el_val_t runtime_path = resolve_runtime_path(src_path);
|
||||
compile_dispatch_bundle(tgt, source, runtime_path);
|
||||
} else {
|
||||
compile_dispatch(tgt, source);
|
||||
}
|
||||
stdout_restore();
|
||||
el_val_t ok_min = apply_minify(tmp_gen, tmp_min, src_dir);
|
||||
if (!ok_min) {
|
||||
exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min));
|
||||
exit(1);
|
||||
}
|
||||
state_set(EL_STR("__elc_final_js"), tmp_min);
|
||||
if (do_obfuscate) {
|
||||
el_val_t tmp_obf = make_temp_path(EL_STR("obf.js"));
|
||||
el_val_t ok_obf = apply_obfuscate(tmp_min, tmp_obf, src_dir);
|
||||
if (!ok_obf) {
|
||||
exec_command(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min), EL_STR(" ")), tmp_obf));
|
||||
exit(1);
|
||||
}
|
||||
state_set(EL_STR("__elc_final_js"), tmp_obf);
|
||||
}
|
||||
el_val_t final_path = state_get(EL_STR("__elc_final_js"));
|
||||
el_val_t final_js = fs_read(final_path);
|
||||
exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min));
|
||||
if (do_obfuscate) {
|
||||
exec_command(el_str_concat(EL_STR("rm -f "), final_path));
|
||||
}
|
||||
if (argc >= 2) {
|
||||
el_val_t out_path = native_list_get(positional, 1);
|
||||
el_val_t ok = fs_write(out_path, final_js);
|
||||
if (ok) {
|
||||
return 0;
|
||||
} else {
|
||||
println(EL_STR("el-compiler: failed to write output"));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
print(final_js);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int _argc, char** _argv) {
|
||||
el_runtime_init_args(_argc, _argv);
|
||||
el_val_t argv = args();
|
||||
el_val_t tgt = detect_target(argv);
|
||||
el_val_t do_emit_header = detect_emit_header(argv);
|
||||
el_val_t do_bundle = detect_bundle(argv);
|
||||
el_val_t do_minify = detect_minify(argv);
|
||||
el_val_t do_obfuscate = detect_obfuscate(argv);
|
||||
if (do_obfuscate) {
|
||||
do_minify = 1;
|
||||
}
|
||||
el_val_t positional = strip_flags(argv);
|
||||
el_val_t argc = native_list_len(positional);
|
||||
if (argc < 1) {
|
||||
println(EL_STR("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]"));
|
||||
println(EL_STR("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]"));
|
||||
exit(1);
|
||||
}
|
||||
if (do_minify) {
|
||||
if (!str_eq(tgt, EL_STR("js"))) {
|
||||
println(EL_STR("el-compiler: error: --minify and --obfuscate require --target=js"));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
el_val_t src_path = native_list_get(positional, 0);
|
||||
if (do_emit_header) {
|
||||
el_val_t raw_source = fs_read(src_path);
|
||||
@@ -6683,7 +7075,17 @@ int main(int _argc, char** _argv) {
|
||||
el_release(hdr_stmts);
|
||||
}
|
||||
el_val_t source = resolve_imports(src_path);
|
||||
el_val_t out = compile_dispatch(tgt, source);
|
||||
if (do_minify) {
|
||||
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional);
|
||||
exit(0);
|
||||
}
|
||||
el_val_t out = EL_STR("");
|
||||
if (do_bundle) {
|
||||
el_val_t runtime_path = resolve_runtime_path(src_path);
|
||||
out = compile_dispatch_bundle(tgt, source, runtime_path);
|
||||
} else {
|
||||
out = compile_dispatch(tgt, source);
|
||||
}
|
||||
if (argc >= 2) {
|
||||
el_val_t out_path = native_list_get(positional, 1);
|
||||
el_val_t ok = fs_write(out_path, out);
|
||||
+130
-1548
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,8 @@ extern "C" {
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect println to a file */
|
||||
el_val_t stdout_restore(void); /* restore stdout after capture */
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -212,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
* 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);
|
||||
|
||||
/* ── HTML template helpers ───────────────────────────────────────────────────
|
||||
* Used by compiled El HTML template expressions.
|
||||
* html_escape(s) — escape & < > " ' for safe inline interpolation.
|
||||
* html_raw(s) — identity; explicit opt-out from escaping (`raw()` form). */
|
||||
el_val_t html_escape(el_val_t s);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
@@ -601,13 +610,6 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
|
||||
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);
|
||||
/* Working memory introspection — count, mean weight, and top-N snapshot.
|
||||
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void);
|
||||
el_val_t engram_wm_top_json(el_val_t n);
|
||||
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
|
||||
el_val_t engram_load_merge(el_val_t path);
|
||||
/* 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
|
||||
@@ -758,18 +760,6 @@ 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);
|
||||
|
||||
/* ── Runtime symbols required by the soul modules ──────────────────────────── */
|
||||
/* All implemented in el_runtime.c but omitted from this release header; the soul dist modules
|
||||
* reference them directly, so the public header must export them. Declarations only — mirrors the
|
||||
* mainline el_runtime.h and is platform-independent (no behavioural change to the POSIX build). */
|
||||
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);
|
||||
el_val_t el_arena_push(void);
|
||||
el_val_t el_arena_pop(el_val_t mark);
|
||||
void http_serve_async(el_val_t port, el_val_t handler);
|
||||
el_val_t engram_get_node_by_label(el_val_t label);
|
||||
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) {
|
||||
return String(s).padEnd(width, String(pad));
|
||||
}
|
||||
|
||||
// ── HTML template helpers ────────────────────────────────────────────────────
|
||||
// Used by compiled El HTML template expressions.
|
||||
// html_escape(s) — escape & < > " ' for safe inline interpolation.
|
||||
// html_raw(s) — identity; explicit opt-out from escaping (raw() form).
|
||||
|
||||
function html_escape(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function html_raw(s) { return s; }
|
||||
|
||||
// ── Math ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function el_abs(n) { return Math.abs(n); }
|
||||
@@ -1017,6 +1033,8 @@ export {
|
||||
fs_read, fs_write, fs_list,
|
||||
json_parse, json_stringify, json_get, json_get_string, json_get_int,
|
||||
time_now, time_now_utc, sleep_ms,
|
||||
// HTML template helpers
|
||||
html_escape, html_raw,
|
||||
bool_to_str, exit_program, args, env,
|
||||
state_set, state_get, state_del, state_keys,
|
||||
el_cgi_init,
|
||||
+9
-21
@@ -1673,7 +1673,6 @@ static void* http_worker_v2(void* arg) {
|
||||
HttpWorkerArg* a = (HttpWorkerArg*)arg;
|
||||
int fd = a->fd;
|
||||
free(a);
|
||||
int is_sse = 0;
|
||||
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
|
||||
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
|
||||
http_handler4_fn h = http_lookup_active_v2();
|
||||
@@ -1681,39 +1680,28 @@ static void* http_worker_v2(void* arg) {
|
||||
int head_only = (method && strcmp(method, "HEAD") == 0);
|
||||
const char* dispatch_method = head_only ? "GET" : method;
|
||||
el_request_start(); /* begin per-request arena */
|
||||
/* Expose the raw fd to El SSE builtins (__http_conn_fd etc.). */
|
||||
el_seed_set_http_conn_fd(fd);
|
||||
if (h) {
|
||||
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
|
||||
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
|
||||
const char* rs = EL_CSTR(r);
|
||||
/* Detect SSE sentinel — handler took ownership of the fd. */
|
||||
if (rs && strcmp(rs, "__sse__") == 0) {
|
||||
is_sse = 1;
|
||||
} else {
|
||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
||||
response = malloc(rlen + 1);
|
||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||
else if (response) { response[0] = '\0'; }
|
||||
}
|
||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
||||
response = malloc(rlen + 1);
|
||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||
else if (response) { response[0] = '\0'; }
|
||||
el_release(hmap);
|
||||
} else {
|
||||
response = el_strdup_persist(
|
||||
"el-runtime: no v2 http handler registered "
|
||||
"(call http_set_handler_v2)");
|
||||
}
|
||||
el_seed_set_http_conn_fd(-1); /* clear before arena teardown */
|
||||
el_request_end(); /* free all intermediate strings */
|
||||
if (!is_sse) {
|
||||
_tl_http_head_only = head_only;
|
||||
http_send_response(fd, response);
|
||||
_tl_http_head_only = 0;
|
||||
free(response);
|
||||
}
|
||||
_tl_http_head_only = head_only;
|
||||
http_send_response(fd, response);
|
||||
_tl_http_head_only = 0;
|
||||
free(response);
|
||||
}
|
||||
free(method); free(path); free(body); free(hdr_block);
|
||||
/* SSE handlers close the fd themselves via __http_sse_close. */
|
||||
if (!is_sse) close(fd);
|
||||
close(fd);
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
@@ -176,11 +176,6 @@ void http_set_handler_v2(el_val_t name);
|
||||
* 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. */
|
||||
@@ -948,6 +948,10 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
if kind == "Import" { return declared }
|
||||
// TestDef: skip in normal mode; handled by js_codegen_test in test mode.
|
||||
if kind == "TestDef" { return declared }
|
||||
// Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode.
|
||||
if kind == "Assert" { return declared }
|
||||
|
||||
if kind == "TryCatch" {
|
||||
let try_body = stmt["try_body"]
|
||||
@@ -1168,20 +1172,178 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ServiceBlock" { return true }
|
||||
if kind == "ExternFn" { return true }
|
||||
if kind == "TestDef" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// ── Test mode codegen (JS) ────────────────────────────────────────────────────
|
||||
//
|
||||
// reporter = "text" → human-readable output to stderr (console.error)
|
||||
// reporter = "json" → newline-delimited JSON to stdout (process.stdout.write)
|
||||
//
|
||||
// The test function returns bool: true = pass, false = fail.
|
||||
|
||||
fn js_cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let expr_c: String = js_cg_expr(expr_node)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
js_emit_line(" if (!(" + expr_c + ")) {")
|
||||
js_emit_line(" process.stderr.write(\" FAIL " + js_escape(test_name) + " — " + js_escape(disp_msg) + "\\n\");")
|
||||
js_emit_line(" return false;")
|
||||
js_emit_line(" }")
|
||||
}
|
||||
|
||||
fn js_cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
|
||||
let expr_node = stmt["expr"]
|
||||
let msg: String = stmt["msg"]
|
||||
let assert_line: Int = stmt["line"]
|
||||
let expr_c: String = js_cg_expr(expr_node)
|
||||
let disp_msg = "assert failed"
|
||||
if !str_eq(msg, "") { let disp_msg = msg }
|
||||
js_emit_line(" if (!(" + expr_c + ")) {")
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",assert_line:" + native_int_to_str(assert_line) + ",message:" + js_str_lit(disp_msg) + "}) + \"\\n\");")
|
||||
js_emit_line(" return false;")
|
||||
js_emit_line(" }")
|
||||
}
|
||||
|
||||
// js_cg_stmts_in_test: emit test body, routing Assert to the right handler.
|
||||
fn js_cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
let decl = declared
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "Assert") {
|
||||
if str_eq(reporter, "json") {
|
||||
js_cg_stmt_assert_json(stmt, test_name, file_name, test_line)
|
||||
} else {
|
||||
js_cg_stmt_assert_text(stmt, test_name)
|
||||
}
|
||||
} else {
|
||||
let decl = js_cg_stmt(stmt, indent, decl)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
decl
|
||||
}
|
||||
|
||||
// js_cg_test_fn: emit a single async test function.
|
||||
fn js_cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
|
||||
let fn_name: String = "el_test_" + native_int_to_str(idx)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let body = test_def["body"]
|
||||
js_emit_line("async function " + fn_name + "() {")
|
||||
js_cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
|
||||
js_emit_line(" return true;")
|
||||
js_emit_line("}")
|
||||
js_emit_blank()
|
||||
fn_name
|
||||
}
|
||||
|
||||
// js_codegen_test: emit the test runner (replaces main() when --test active).
|
||||
// reporter: "text" or "json"
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn js_codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
|
||||
// Collect TestDef nodes in order.
|
||||
let n: Int = native_list_len(stmts)
|
||||
let test_defs: [Map<String, Any>] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk: String = stmt["stmt"]
|
||||
if str_eq(sk, "TestDef") {
|
||||
let test_defs = native_list_append(test_defs, stmt)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let n_tests: Int = native_list_len(test_defs)
|
||||
|
||||
// Emit non-test function definitions (skip fn main and TestDef nodes).
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
if js_is_fndef(stmt) {
|
||||
let fn_name: String = stmt["name"]
|
||||
if !str_eq(fn_name, "main") {
|
||||
js_cg_fn(stmt)
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Emit each test function.
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
js_cg_test_fn(test_def, ti, reporter, file_name)
|
||||
let ti = ti + 1
|
||||
}
|
||||
|
||||
// Emit the test runner IIFE.
|
||||
let test_word = "tests"
|
||||
if n_tests == 1 { let test_word = "test" }
|
||||
js_emit_line("(async () => {")
|
||||
js_emit_line(" let pass = 0; let fail = 0;")
|
||||
|
||||
if str_eq(reporter, "json") {
|
||||
// JSON reporter: suite_start to stdout
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:" + js_str_lit(file_name) + ",total:" + native_int_to_str(n_tests) + "}) + \"\\n\");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let test_line: Int = test_def["line"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_start\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + "}) + \"\\n\");")
|
||||
js_emit_line(" if (await " + fn_name + "()) {")
|
||||
js_emit_line(" pass++;")
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",duration_ms:0}) + \"\\n\");")
|
||||
js_emit_line(" } else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");")
|
||||
} else {
|
||||
// Text reporter: human-readable to stderr
|
||||
js_emit_line(" process.stderr.write(\"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
|
||||
let ti = 0
|
||||
while ti < n_tests {
|
||||
let test_def = native_list_get(test_defs, ti)
|
||||
let test_name: String = test_def["name"]
|
||||
let fn_name: String = "el_test_" + native_int_to_str(ti)
|
||||
js_emit_line(" process.stderr.write(\" RUN " + js_escape(test_name) + "\\n\");")
|
||||
js_emit_line(" if (await " + fn_name + "()) { pass++; process.stderr.write(\" PASS " + js_escape(test_name) + "\\n\"); }")
|
||||
js_emit_line(" else { fail++; }")
|
||||
let ti = ti + 1
|
||||
}
|
||||
js_emit_line(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");")
|
||||
}
|
||||
|
||||
js_emit_line(" process.exit(fail > 0 ? 1 : 0);")
|
||||
js_emit_line("})();")
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
|
||||
codegen_js_inner(stmts, source, false, "")
|
||||
codegen_js_inner(stmts, source, false, "", false, "text", "")
|
||||
}
|
||||
|
||||
// codegen_js_test: emit a JS test binary.
|
||||
// reporter: "text" or "json"
|
||||
// file_name: basename of the source file (used in JSON output)
|
||||
fn codegen_js_test(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
|
||||
codegen_js_inner(stmts, source, false, "", true, reporter, file_name)
|
||||
}
|
||||
|
||||
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
|
||||
codegen_js_inner(stmts, source, true, runtime_content)
|
||||
codegen_js_inner(stmts, source, true, runtime_content, false, "text", "")
|
||||
}
|
||||
|
||||
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
|
||||
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String, test_mode: Bool, reporter: String, file_name: String) -> String {
|
||||
// Reset per-compile state.
|
||||
state_set("__js_int_names", "")
|
||||
state_set("__js_match_counter", "")
|
||||
@@ -1292,6 +1454,12 @@ fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Test mode: emit test functions and runner, skip normal program logic.
|
||||
if test_mode {
|
||||
js_codegen_test(stmts, reporter, file_name)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Function definitions
|
||||
let i = 0
|
||||
while i < n {
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,44 +20,18 @@ import "codegen.el"
|
||||
import "codegen-js.el"
|
||||
|
||||
// compile — full pipeline (C target): source string -> C source string
|
||||
// Uses JIT function-at-a-time streaming: parse one decl → emit C → discard AST.
|
||||
// Peak memory is O(one function's AST) instead of O(whole program AST).
|
||||
fn compile(source: String) -> String {
|
||||
// Top-level arena scope: activates the string arena before lex() so that
|
||||
// ALL strdup allocations (token strings, sig strings, codegen fragments)
|
||||
// are tracked and freed on pop. Without this, lex() and scan_fn_sigs()
|
||||
// run before any push, leaving _tl_arena_active=0 and leaking every
|
||||
// token string. Also prevents inner pop(mark=0) calls from deactivating
|
||||
// the arena between per-function scopes.
|
||||
let top_mark: Any = el_arena_push()
|
||||
let tokens: [Any] = lex(source)
|
||||
// Fast pre-scan: collect fn signatures + program kind without building
|
||||
// full expression ASTs. O(tokens) time, minimal allocation.
|
||||
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
|
||||
// Stream parse-emit: parse one decl at a time, emit C, discard.
|
||||
// All output written to stdout via println before pop.
|
||||
codegen_streaming(tokens, sigs, source)
|
||||
el_arena_pop(top_mark)
|
||||
""
|
||||
}
|
||||
|
||||
// compile_test — like compile() but sets __test_mode so codegen_streaming
|
||||
// compiles test { } blocks instead of skipping them, and emits the test
|
||||
// harness main() instead of the normal int main().
|
||||
fn compile_test(source: String) -> String {
|
||||
state_set("__test_mode", "1")
|
||||
let top_mark: Any = el_arena_push()
|
||||
let tokens: [Any] = lex(source)
|
||||
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
|
||||
codegen_streaming(tokens, sigs, source)
|
||||
el_arena_pop(top_mark)
|
||||
state_set("__test_mode", "")
|
||||
""
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
// Token list is no longer needed after parsing — release it to free memory
|
||||
// before codegen allocates its own working data on large source files.
|
||||
el_release(tokens)
|
||||
codegen(stmts, source)
|
||||
}
|
||||
|
||||
// compile_js — full pipeline (JS target, module mode): source string -> JS source string
|
||||
fn compile_js(source: String) -> String {
|
||||
let tokens: [Any] = lex(source)
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
// Token list is no longer needed after parsing — release it to free memory.
|
||||
el_release(tokens)
|
||||
@@ -67,7 +41,7 @@ fn compile_js(source: String) -> String {
|
||||
// compile_js_with_bundle — JS target in bundle mode.
|
||||
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
|
||||
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
|
||||
let tokens: [Any] = lex(source)
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
let runtime_content: String = fs_read(runtime_path)
|
||||
@@ -78,6 +52,24 @@ fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
|
||||
codegen_js_bundle(stmts, source, runtime_content)
|
||||
}
|
||||
|
||||
// compile_test — full pipeline (C target, test mode): source -> C test runner.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_test(source: String, reporter: String, file_name: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
codegen_with_tests(stmts, source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_js_test — full pipeline (JS target, test mode): source -> JS test runner.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_js_test(source: String, reporter: String, file_name: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
codegen_js_test(stmts, source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_dispatch — pick a backend based on the requested target.
|
||||
// tgt = "c" | "js"
|
||||
// (The parameter is named `tgt` because `target` is a reserved keyword
|
||||
@@ -88,6 +80,13 @@ fn compile_dispatch(tgt: String, source: String) -> String {
|
||||
compile(source)
|
||||
}
|
||||
|
||||
// compile_dispatch_test — pick test-mode backend.
|
||||
// reporter: "text" or "json"; file_name: basename of the source file.
|
||||
fn compile_dispatch_test(tgt: String, source: String, reporter: String, file_name: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js_test(source, reporter, file_name) }
|
||||
compile_test(source, reporter, file_name)
|
||||
}
|
||||
|
||||
// compile_dispatch_bundle — like compile_dispatch but bundle mode for JS.
|
||||
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
|
||||
@@ -185,6 +184,36 @@ fn detect_test(argv: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --reporter=<value> flag in argv.
|
||||
// Returns "json" if --reporter=json, otherwise "text" (default).
|
||||
fn detect_reporter(argv: [String]) -> String {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_starts_with(a, "--reporter=") {
|
||||
let v: String = str_slice(a, 11, str_len(a))
|
||||
return v
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
|
||||
// basename_of — extract the filename portion of a path (after last '/').
|
||||
fn basename_of(path: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
let c: String = str_slice(path, i, i + 1)
|
||||
if str_eq(c, "/") {
|
||||
return str_slice(path, i + 1, n)
|
||||
}
|
||||
let i = i - 1
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
|
||||
fn make_temp_path(suffix: String) -> String {
|
||||
let pid: Int = getpid_now()
|
||||
@@ -287,9 +316,6 @@ fn type_node_to_el(t: Map<String, Any>) -> String {
|
||||
|
||||
// emit_header — write a .elh file from parsed statements.
|
||||
// Scans for FnDef nodes and emits 'extern fn' declarations.
|
||||
// NOTE: This function requires the full AST. Prefer emit_header_from_sigs
|
||||
// for the --emit-header path — it works from a token-level scan without
|
||||
// building expression ASTs, avoiding OOM on large files.
|
||||
fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
@@ -328,32 +354,6 @@ fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let ok: Bool = fs_write(hdr_path, content)
|
||||
}
|
||||
|
||||
// emit_header_from_sigs — write a .elh file from pre-scanned El signatures.
|
||||
// Uses the output of scan_fn_sigs_el() — no full AST required.
|
||||
// Peak memory is O(tokens) rather than O(whole-program AST), which prevents
|
||||
// OOM on large files with HTML template bodies or deep BinOp chains.
|
||||
fn emit_header_from_sigs(sigs: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let n: Int = native_list_len(sigs)
|
||||
let i: Int = 0
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n")
|
||||
while i < n {
|
||||
let sig = native_list_get(sigs, i)
|
||||
let kind: String = sig["kind"]
|
||||
if str_eq(kind, "fn") {
|
||||
let name: String = sig["name"]
|
||||
let params_el: String = sig["params_el"]
|
||||
let ret_el: String = sig["ret_el"]
|
||||
if str_eq(ret_el, "") { let ret_el = "Any" }
|
||||
let line: String = "extern fn " + name + "(" + params_el + ") -> " + ret_el
|
||||
let parts = native_list_append(parts, line + "\n")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let content: String = str_join(parts, "")
|
||||
let ok: Bool = fs_write(hdr_path, content)
|
||||
}
|
||||
|
||||
// ── Import resolution ────────────────────────────────────────────────────────
|
||||
//
|
||||
// elc supports two forms of import:
|
||||
@@ -525,7 +525,9 @@ fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle
|
||||
// main — CLI entry point.
|
||||
//
|
||||
// elc <source.el> # emit C to stdout
|
||||
// elc --test <source.el> # emit C test runner to stdout
|
||||
// elc --target=js <source.el> # emit JS (module) to stdout
|
||||
// elc --target=js --test <source.el> # emit JS test runner to stdout
|
||||
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
|
||||
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
|
||||
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
|
||||
@@ -544,6 +546,7 @@ fn main() -> Void {
|
||||
let do_minify: Bool = detect_minify(argv)
|
||||
let do_obfuscate: Bool = detect_obfuscate(argv)
|
||||
let do_test: Bool = detect_test(argv)
|
||||
let reporter: String = detect_reporter(argv)
|
||||
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||
if do_obfuscate {
|
||||
let do_minify = true
|
||||
@@ -551,7 +554,7 @@ fn main() -> Void {
|
||||
let positional: [String] = strip_flags(argv)
|
||||
let argc: Int = native_list_len(positional)
|
||||
if argc < 1 {
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] [--test] <source.el> [<output>]")
|
||||
println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
@@ -565,23 +568,33 @@ fn main() -> Void {
|
||||
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
|
||||
// When --emit-header is requested, lex the source file and do a
|
||||
// token-level signature scan (no full AST) to write a .elh file.
|
||||
// This avoids OOM on large files with HTML template bodies or deep
|
||||
// BinOp chains (e.g. checkout.el) — parse() builds O(whole-program AST)
|
||||
// while scan_fn_sigs_el keeps peak memory at O(tokens).
|
||||
// When --emit-header is requested, parse the source file directly
|
||||
// (without inlining imports) and write out a .elh file alongside the .c.
|
||||
if do_emit_header {
|
||||
el_mem_check()
|
||||
let raw_source: String = fs_read(src_path)
|
||||
let hdr_tokens: [Any] = lex(raw_source)
|
||||
let hdr_sigs: [Map<String, Any>] = scan_fn_sigs_el(hdr_tokens)
|
||||
let hdr_tokens: [Map<String, Any>] = lex(raw_source)
|
||||
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
|
||||
el_release(hdr_tokens)
|
||||
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
|
||||
emit_header_from_sigs(hdr_sigs, hdr_path)
|
||||
el_release(hdr_sigs)
|
||||
emit_header(hdr_stmts, hdr_path)
|
||||
el_release(hdr_stmts)
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
let file_name: String = basename_of(src_path)
|
||||
|
||||
// --test mode: emit a test runner binary instead of the normal program.
|
||||
if do_test {
|
||||
let out: String = compile_dispatch_test(tgt, source, reporter, file_name)
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, out)
|
||||
if ok { exit(0) }
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// When post-processing (--minify or --obfuscate) is requested, redirect
|
||||
// stdout to a temp file so codegen output can be captured and piped through
|
||||
@@ -592,12 +605,6 @@ fn main() -> Void {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// --test mode: compile with test harness (C target only).
|
||||
if do_test {
|
||||
compile_test(source)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Standard path (no post-processing).
|
||||
let out: String = ""
|
||||
if do_bundle {
|
||||
@@ -0,0 +1,763 @@
|
||||
// lexer.el — el self-hosting lexer
|
||||
//
|
||||
// Tokenises an el source string into a list of token maps.
|
||||
// Each token is a Map<String, Any> with keys:
|
||||
// "kind" -> String (e.g. "Int", "Ident", "Plus")
|
||||
// "value" -> String (the raw text of the token)
|
||||
//
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get — avoids O(N²) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
if ch == "1" { return true }
|
||||
if ch == "2" { return true }
|
||||
if ch == "3" { return true }
|
||||
if ch == "4" { return true }
|
||||
if ch == "5" { return true }
|
||||
if ch == "6" { return true }
|
||||
if ch == "7" { return true }
|
||||
if ch == "8" { return true }
|
||||
if ch == "9" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_alpha(ch: String) -> Bool {
|
||||
if ch == "a" { return true }
|
||||
if ch == "b" { return true }
|
||||
if ch == "c" { return true }
|
||||
if ch == "d" { return true }
|
||||
if ch == "e" { return true }
|
||||
if ch == "f" { return true }
|
||||
if ch == "g" { return true }
|
||||
if ch == "h" { return true }
|
||||
if ch == "i" { return true }
|
||||
if ch == "j" { return true }
|
||||
if ch == "k" { return true }
|
||||
if ch == "l" { return true }
|
||||
if ch == "m" { return true }
|
||||
if ch == "n" { return true }
|
||||
if ch == "o" { return true }
|
||||
if ch == "p" { return true }
|
||||
if ch == "q" { return true }
|
||||
if ch == "r" { return true }
|
||||
if ch == "s" { return true }
|
||||
if ch == "t" { return true }
|
||||
if ch == "u" { return true }
|
||||
if ch == "v" { return true }
|
||||
if ch == "w" { return true }
|
||||
if ch == "x" { return true }
|
||||
if ch == "y" { return true }
|
||||
if ch == "z" { return true }
|
||||
if ch == "A" { return true }
|
||||
if ch == "B" { return true }
|
||||
if ch == "C" { return true }
|
||||
if ch == "D" { return true }
|
||||
if ch == "E" { return true }
|
||||
if ch == "F" { return true }
|
||||
if ch == "G" { return true }
|
||||
if ch == "H" { return true }
|
||||
if ch == "I" { return true }
|
||||
if ch == "J" { return true }
|
||||
if ch == "K" { return true }
|
||||
if ch == "L" { return true }
|
||||
if ch == "M" { return true }
|
||||
if ch == "N" { return true }
|
||||
if ch == "O" { return true }
|
||||
if ch == "P" { return true }
|
||||
if ch == "Q" { return true }
|
||||
if ch == "R" { return true }
|
||||
if ch == "S" { return true }
|
||||
if ch == "T" { return true }
|
||||
if ch == "U" { return true }
|
||||
if ch == "V" { return true }
|
||||
if ch == "W" { return true }
|
||||
if ch == "X" { return true }
|
||||
if ch == "Y" { return true }
|
||||
if ch == "Z" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_alnum_or_underscore(ch: String) -> Bool {
|
||||
if lex_is_digit(ch) { return true }
|
||||
if lex_is_alpha(ch) { return true }
|
||||
if ch == "_" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_whitespace(ch: String) -> Bool {
|
||||
if ch == " " { return true }
|
||||
if ch == "\t" { return true }
|
||||
if ch == "\n" { return true }
|
||||
if ch == "\r" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
let ln_s: String = state_get("__lex_line")
|
||||
let ln: Int = 1
|
||||
if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) }
|
||||
{ "kind": kind, "value": value, "line": ln }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
if word == "fn" { return "Fn" }
|
||||
if word == "type" { return "Type" }
|
||||
if word == "enum" { return "Enum" }
|
||||
if word == "match" { return "Match" }
|
||||
if word == "return" { return "Return" }
|
||||
if word == "if" { return "If" }
|
||||
if word == "else" { return "Else" }
|
||||
if word == "for" { return "For" }
|
||||
if word == "in" { return "In" }
|
||||
if word == "while" { return "While" }
|
||||
if word == "import" { return "Import" }
|
||||
if word == "from" { return "From" }
|
||||
if word == "as" { return "As" }
|
||||
if word == "with" { return "With" }
|
||||
if word == "sealed" { return "Sealed" }
|
||||
if word == "activate" { return "Activate" }
|
||||
if word == "where" { return "Where" }
|
||||
if word == "test" { return "Test" }
|
||||
if word == "seed" { return "Seed" }
|
||||
if word == "assert" { return "Assert" }
|
||||
if word == "protocol" { return "Protocol" }
|
||||
if word == "impl" { return "Impl" }
|
||||
if word == "retry" { return "Retry" }
|
||||
if word == "times" { return "Times" }
|
||||
if word == "fallback" { return "Fallback" }
|
||||
if word == "reason" { return "Reason" }
|
||||
if word == "parallel" { return "Parallel" }
|
||||
if word == "trace" { return "Trace" }
|
||||
if word == "requires" { return "Requires" }
|
||||
if word == "deploy" { return "Deploy" }
|
||||
if word == "to" { return "To" }
|
||||
if word == "via" { return "Via" }
|
||||
if word == "target" { return "Target" }
|
||||
if word == "true" { return "Bool" }
|
||||
if word == "false" { return "Bool" }
|
||||
if word == "cgi" { return "Cgi" }
|
||||
if word == "service" { return "Service" }
|
||||
if word == "manager" { return "Manager" }
|
||||
if word == "engine" { return "Engine" }
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
if word == "try" { return "Try" }
|
||||
if word == "catch" { return "Catch" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if lex_is_digit(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if is_alnum_or_underscore(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Code-bearing string detection + comment strip ────────────────────────────
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
|
||||
// or stylesheet payloads inside string literals) carry their own line and
|
||||
// block comments. Those comments leak into the served HTML and reveal build
|
||||
// notes the visitor should never see. We strip them at the lexer so every
|
||||
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
|
||||
//
|
||||
// looks_like_code — heuristic gate so we only strip strings that actually
|
||||
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
|
||||
|
||||
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||
let nchars: [String] = native_string_chars(needle)
|
||||
let nlen: Int = native_list_len(nchars)
|
||||
if start + nlen > total { return false }
|
||||
let i = 0
|
||||
let matched = true
|
||||
while i < nlen {
|
||||
let a: String = native_list_get(chars, start + i)
|
||||
let b: String = native_list_get(nchars, i)
|
||||
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
|
||||
}
|
||||
matched
|
||||
}
|
||||
|
||||
fn str_has(s: String, needle: String) -> Bool {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let i = 0
|
||||
let found = false
|
||||
while i < total {
|
||||
if substr_at(chars, i, total, needle) {
|
||||
let found = true
|
||||
let i = total
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
fn looks_like_code(s: String) -> Bool {
|
||||
if str_has(s, "<script") { return true }
|
||||
if str_has(s, "<style") { return true }
|
||||
if str_has(s, "function") {
|
||||
if str_has(s, ";") { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments — character-by-character walk. Tracks JS string state
|
||||
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||
// preserved by checking the previous char before treating // as a line
|
||||
// comment opener: if the char immediately before '/' is ':', emit the '/'
|
||||
// literally and advance one position.
|
||||
fn strip_code_comments(s: String) -> String {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let out_parts: [String] = native_list_empty()
|
||||
let i = 0
|
||||
let in_squote = false
|
||||
let in_dquote = false
|
||||
let in_btick = false
|
||||
let prev = ""
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
let in_js_string = false
|
||||
if in_squote { let in_js_string = true }
|
||||
if in_dquote { let in_js_string = true }
|
||||
if in_btick { let in_js_string = true }
|
||||
|
||||
if in_js_string {
|
||||
// Backslash escape: consume next char verbatim regardless of which.
|
||||
if ch == "\\" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let nc: String = native_list_get(chars, next_i)
|
||||
let out_parts = native_list_append(out_parts, nc)
|
||||
let prev = nc
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let prev = ch
|
||||
let i = next_i
|
||||
}
|
||||
} else {
|
||||
if in_squote {
|
||||
if ch == "'" { let in_squote = false }
|
||||
} else {
|
||||
if in_dquote {
|
||||
if ch == "\"" { let in_dquote = false }
|
||||
} else {
|
||||
if in_btick {
|
||||
if ch == "`" { let in_btick = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not in a JS string. Check for comment openers.
|
||||
let next_i = i + 1
|
||||
let next_ch = ""
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
}
|
||||
|
||||
if ch == "/" {
|
||||
if next_ch == "/" {
|
||||
// URL guard: prev char ':' means this is "://", not a comment.
|
||||
if prev == ":" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Skip until newline (newline itself is preserved so
|
||||
// surrounding line counts/structure stay sane).
|
||||
let i = i + 2
|
||||
let scanning = true
|
||||
while scanning {
|
||||
if i >= total {
|
||||
let scanning = false
|
||||
} else {
|
||||
let lc: String = native_list_get(chars, i)
|
||||
if lc == "\n" {
|
||||
let scanning = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
}
|
||||
} else {
|
||||
if next_ch == "*" {
|
||||
// Skip until matching "*/".
|
||||
let i = i + 2
|
||||
let scanning2 = true
|
||||
while scanning2 {
|
||||
if i >= total {
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let bc: String = native_list_get(chars, i)
|
||||
if bc == "*" {
|
||||
let after = i + 1
|
||||
if after < total {
|
||||
let nc2: String = native_list_get(chars, after)
|
||||
if nc2 == "/" {
|
||||
let i = after + 1
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Open a JS string?
|
||||
if ch == "'" {
|
||||
let in_squote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let in_dquote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "`" {
|
||||
let in_btick = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "\\" {
|
||||
// escape: peek next char
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "\"" {
|
||||
let parts = native_list_append(parts, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let parts = native_list_append(parts, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let parts = native_list_append(parts, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let parts = native_list_append(parts, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let parts = native_list_append(parts, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let parts = native_list_append(parts, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
let i: Int = 0
|
||||
let line_num: Int = 1
|
||||
state_set("__lex_line", "1")
|
||||
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
// Skip whitespace; track newlines for line-number reporting
|
||||
if lex_is_whitespace(ch) {
|
||||
if ch == "\n" {
|
||||
let line_num = line_num + 1
|
||||
state_set("__lex_line", native_int_to_str(line_num))
|
||||
}
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Line comments: //
|
||||
if ch == "/" {
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "/" {
|
||||
// skip to end of line
|
||||
let i = i + 2
|
||||
let running2 = true
|
||||
while running2 {
|
||||
if i >= total {
|
||||
let running2 = false
|
||||
} else {
|
||||
let lch: String = native_list_get(chars, i)
|
||||
if lch == "\n" {
|
||||
let running2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// String literal
|
||||
if ch == "\"" {
|
||||
let result = scan_string(chars, i + 1, total)
|
||||
let str_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// Compile-time scrub: strings that embed JS or CSS get
|
||||
// their // line comments and /* block comments stripped
|
||||
// before the token reaches the parser. Plain prose passes
|
||||
// through untouched.
|
||||
let clean_text = str_text
|
||||
if looks_like_code(str_text) {
|
||||
let clean_text = strip_code_comments(str_text)
|
||||
}
|
||||
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Number literal
|
||||
if lex_is_digit(ch) {
|
||||
let result = scan_digits(chars, i, total)
|
||||
let num_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// check for float (dot followed by digit)
|
||||
if new_pos < total {
|
||||
let dot_ch: String = native_list_get(chars, new_pos)
|
||||
if dot_ch == "." {
|
||||
let after_dot = new_pos + 1
|
||||
if after_dot < total {
|
||||
let after_dot_ch: String = native_list_get(chars, after_dot)
|
||||
if lex_is_digit(after_dot_ch) {
|
||||
let frac_result = scan_digits(chars, after_dot, total)
|
||||
let frac_text: String = frac_result["text"]
|
||||
let frac_pos: Int = frac_result["pos"]
|
||||
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
|
||||
let i = frac_pos
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
// Identifier or keyword
|
||||
if lex_is_alpha(ch) || ch == "_" {
|
||||
let result = scan_ident(chars, i, total)
|
||||
let word: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
let kw = keyword_kind(word)
|
||||
if kw == "" {
|
||||
let tokens = native_list_append(tokens, make_tok("Ident", word))
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok(kw, word))
|
||||
}
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Multi-char and single-char operators/delimiters
|
||||
let peek_i = i + 1
|
||||
let peek_ch = ""
|
||||
if peek_i < total {
|
||||
let peek_ch: String = native_list_get(chars, peek_i)
|
||||
}
|
||||
|
||||
if ch == "=" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Eq", "="))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "!" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Not", "!"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "<" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ">" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "&" {
|
||||
if peek_ch == "&" {
|
||||
let tokens = native_list_append(tokens, make_tok("And", "&&"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "|" {
|
||||
if peek_ch == "|" {
|
||||
let tokens = native_list_append(tokens, make_tok("Or", "||"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "-" {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ":" {
|
||||
if peek_ch == ":" {
|
||||
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "+" {
|
||||
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "*" {
|
||||
let tokens = native_list_append(tokens, make_tok("Star", "*"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "%" {
|
||||
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "(" {
|
||||
let tokens = native_list_append(tokens, make_tok("LParen", "("))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == ")" {
|
||||
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "{" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "[" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "]" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "," {
|
||||
let tokens = native_list_append(tokens, make_tok("Comma", ","))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == ";" {
|
||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "@" {
|
||||
let tokens = native_list_append(tokens, make_tok("At", "@"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "?" {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "#" {
|
||||
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tokens = native_list_append(tokens, make_tok("Eof", ""))
|
||||
tokens
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+5
-79
@@ -77,33 +77,6 @@ fn parse_manifest_entry(src: String) -> String {
|
||||
return ""
|
||||
}
|
||||
|
||||
// parse_manifest_c_sources - collect all `c_source "path"` lines from the
|
||||
// build block. Returns a flat list of path strings.
|
||||
fn parse_manifest_c_sources(src: String) -> [String] {
|
||||
let result: [String] = native_list_empty()
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "c_source ") {
|
||||
let after: String = str_slice(t, 9, str_len(t))
|
||||
let trimmed: String = str_trim(after)
|
||||
if str_starts_with(trimmed, "\"") {
|
||||
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
|
||||
let q: Int = str_index_of(inner, "\"")
|
||||
if q >= 0 {
|
||||
let path: String = str_slice(inner, 0, q)
|
||||
let result = native_list_append(result, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn parse_manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
@@ -252,7 +225,6 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
|
||||
let bname: String = basename_noext(src_path)
|
||||
let c_out: String = out_dir + "/" + bname + ".c"
|
||||
let elh_out: String = out_dir + "/" + bname + ".elh"
|
||||
let err_tmp: String = "/tmp/elb-err-" + bname + ".txt"
|
||||
|
||||
// Check if recompile needed
|
||||
if !file_is_newer(src_path, c_out) {
|
||||
@@ -262,26 +234,18 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
|
||||
return true
|
||||
}
|
||||
|
||||
// elc streams C to stdout; redirect stderr to a temp file so we can
|
||||
// surface the actual error message on failure instead of swallowing it.
|
||||
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp
|
||||
// elc streams C to stdout (collect mode not yet implemented); use
|
||||
// shell redirection so the output lands in the file, not the terminal.
|
||||
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1"
|
||||
println(" compile " + src_path)
|
||||
|
||||
if dry_run { return true }
|
||||
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret != 0 {
|
||||
// Surface the actual compiler error from stderr
|
||||
let err_msg: String = str_trim(fs_read(err_tmp))
|
||||
if !str_eq(err_msg, "") {
|
||||
println(err_msg)
|
||||
}
|
||||
// Remove partial output so a retry starts clean
|
||||
exec_command("rm -f " + c_out + " " + err_tmp)
|
||||
println("elb: compile failed: " + src_path)
|
||||
return false
|
||||
}
|
||||
exec_command("rm -f " + err_tmp)
|
||||
|
||||
// Move the generated .elh (written next to the source by elc) into
|
||||
// out_dir so that #include "module.elh" lines in the generated .c
|
||||
@@ -298,21 +262,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let parts: [String] = native_list_empty()
|
||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||
// (for module.elh cross-module forward declarations).
|
||||
// Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored
|
||||
// if unsupported but gcc rejects it with an error.
|
||||
let bracket_flag: String = "$(cc --version 2>&1 | grep -q clang && printf -- '-fbracket-depth=1024' || true)"
|
||||
// On macOS, OpenSSL is not on the default linker path. Detect homebrew
|
||||
// prefix and add it if present (no-op on Linux where libssl is in /usr/lib).
|
||||
let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)"
|
||||
let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
|
||||
// Force-include the C-level master declarations header so every translation
|
||||
// unit sees all cross-module function signatures. Handles packages (like ELP)
|
||||
// where modules call each other without explicit El import statements.
|
||||
// The header is generated by elb --gen-decls or manually placed in out_dir.
|
||||
let master_decls: String = out_dir + "/elp-c-decls.h"
|
||||
let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no"))
|
||||
let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" }
|
||||
let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let f: String = native_list_get(c_files, i)
|
||||
@@ -320,7 +270,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let i = i + 1
|
||||
}
|
||||
let parts = native_list_append(parts, runtime_path)
|
||||
let parts = native_list_append(parts, ossl_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread")
|
||||
let parts = native_list_append(parts, "-o " + out_bin)
|
||||
let cmd: String = str_join(parts, " ")
|
||||
println(" link " + out_bin)
|
||||
@@ -353,7 +303,6 @@ fn main() -> Void {
|
||||
|
||||
let pkg_name: String = parse_manifest_name(manifest_src)
|
||||
let entry: String = parse_manifest_entry(manifest_src)
|
||||
let extra_c: [String] = parse_manifest_c_sources(manifest_src)
|
||||
if str_eq(entry, "") {
|
||||
println("elb: manifest.el has no 'entry' declaration")
|
||||
exit(1)
|
||||
@@ -371,20 +320,6 @@ fn main() -> Void {
|
||||
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/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") {
|
||||
let candidate: String = runtime_path + "/el_runtime.c"
|
||||
let has_file: String = str_trim(exec_capture("test -f " + candidate + " && echo yes || echo no"))
|
||||
if str_eq(has_file, "yes") {
|
||||
let runtime_path = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
if str_eq(runtime_path, "") {
|
||||
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
|
||||
exit(1)
|
||||
@@ -432,15 +367,6 @@ fn main() -> Void {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Append any extra C sources declared in the manifest (e.g. platform stubs)
|
||||
let ei = 0
|
||||
let en: Int = native_list_len(extra_c)
|
||||
while ei < en {
|
||||
let ec: String = native_list_get(extra_c, ei)
|
||||
let c_files = native_list_append(c_files, ec)
|
||||
let ei = ei + 1
|
||||
}
|
||||
|
||||
// Link
|
||||
let out_bin: String = out_dir + "/" + pkg_name
|
||||
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
+2489
-286
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
# Compiled El bytecode
|
||||
*.elc
|
||||
|
||||
# C codegen output
|
||||
*.c
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Combined build artifacts
|
||||
_combined.el
|
||||
*-combined.el
|
||||
|
||||
# Distribution / build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@@ -1,85 +0,0 @@
|
||||
package "elp" {
|
||||
version "0.7.0"
|
||||
description "Engram Language Protocol — bidirectional engine mapping between Engram semantic forms and natural language surface text. 31 languages."
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/elp.el"
|
||||
|
||||
// Compilation order (dependency order):
|
||||
// language-profile (no deps)
|
||||
// vocabulary (no deps)
|
||||
// morphology (depends on: language-profile)
|
||||
// morphology-es (depends on: morphology) Spanish
|
||||
// morphology-fr (depends on: morphology) French
|
||||
// morphology-de (depends on: morphology) German
|
||||
// morphology-ru (depends on: morphology) Russian
|
||||
// morphology-ja (depends on: morphology) Japanese
|
||||
// morphology-fi (depends on: morphology) Finnish
|
||||
// morphology-ar (depends on: morphology) Arabic
|
||||
// morphology-hi (depends on: morphology) Hindi
|
||||
// morphology-sw (depends on: morphology) Swahili
|
||||
// morphology-la (depends on: morphology) Latin
|
||||
// morphology-he (depends on: morphology) Hebrew
|
||||
// morphology-grc (depends on: morphology) Ancient Greek
|
||||
// morphology-ang (depends on: morphology) Old English
|
||||
// morphology-sa (depends on: morphology) Sanskrit
|
||||
// morphology-got (depends on: morphology) Gothic
|
||||
// morphology-non (depends on: morphology) Old Norse
|
||||
// morphology-enm (depends on: morphology) Middle English
|
||||
// morphology-pi (depends on: morphology) Pali
|
||||
// morphology-fro (depends on: morphology) Old French
|
||||
// morphology-goh (depends on: morphology) Old High German
|
||||
// morphology-sga (depends on: morphology) Old Irish
|
||||
// morphology-txb (depends on: morphology) Tocharian B
|
||||
// morphology-peo (depends on: morphology) Old Persian
|
||||
// morphology-akk (depends on: morphology) Akkadian
|
||||
// morphology-uga (depends on: morphology) Ugaritic
|
||||
// morphology-egy (depends on: morphology) Ancient Egyptian
|
||||
// morphology-sux (depends on: morphology) Sumerian
|
||||
// morphology-gez (depends on: morphology) Ge'ez (Classical Ethiopic)
|
||||
// morphology-cop (depends on: morphology) Coptic (Sahidic)
|
||||
// grammar (depends on: language-profile)
|
||||
// realizer (depends on: morphology, grammar, language-profile)
|
||||
// semantics (depends on: grammar, realizer, language-profile)
|
||||
// elp (depends on: semantics, realizer)
|
||||
sources [
|
||||
"src/language-profile.el",
|
||||
"src/vocabulary.el",
|
||||
"src/morphology.el",
|
||||
"src/morphology-es.el",
|
||||
"src/morphology-fr.el",
|
||||
"src/morphology-de.el",
|
||||
"src/morphology-ru.el",
|
||||
"src/morphology-ja.el",
|
||||
"src/morphology-fi.el",
|
||||
"src/morphology-ar.el",
|
||||
"src/morphology-hi.el",
|
||||
"src/morphology-sw.el",
|
||||
"src/morphology-la.el",
|
||||
"src/morphology-he.el",
|
||||
"src/morphology-grc.el",
|
||||
"src/morphology-ang.el",
|
||||
"src/morphology-sa.el",
|
||||
"src/morphology-got.el",
|
||||
"src/morphology-non.el",
|
||||
"src/morphology-enm.el",
|
||||
"src/morphology-pi.el",
|
||||
"src/morphology-fro.el",
|
||||
"src/morphology-goh.el",
|
||||
"src/morphology-sga.el",
|
||||
"src/morphology-txb.el",
|
||||
"src/morphology-peo.el",
|
||||
"src/morphology-akk.el",
|
||||
"src/morphology-uga.el",
|
||||
"src/morphology-egy.el",
|
||||
"src/morphology-sux.el",
|
||||
"src/morphology-gez.el",
|
||||
"src/morphology-cop.el",
|
||||
"src/grammar.el",
|
||||
"src/realizer.el",
|
||||
"src/semantics.el",
|
||||
"src/elp.el",
|
||||
]
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
> **STATUS: STAGING / PROOF-OF-SHAPE — not the deliverable.** This Python package
|
||||
> proved the architecture end-to-end against the proven realizer faculty (faithful
|
||||
> md/docx/midi from real geometry: 0 ungrounded claims, SACRED polarity). Per Will's
|
||||
> steer, the DELIVERABLE is NATIVE: the seam lives on the existing EL realizer as
|
||||
> **surface-as-profile** — see `../src/surface-profile.el` and
|
||||
> `../tests/examples/surface-profile-demo.el` (compiles + runs through elc → C →
|
||||
> binary). The concepts below (one geometry-carrying frame; surface = a pluggable
|
||||
> profile; plan/realize; deterministic-from-meaning) are exactly what the native
|
||||
> module implements. Keep this package as the validated proof; build native.
|
||||
|
||||
# Efferent Multimodal Projector
|
||||
|
||||
**geometry → any surface, faithfully.** Neuron's own document-generation faculty:
|
||||
the efferent twin of the ingest organ. Ingest is afferent (world → geometry);
|
||||
this is efferent (geometry → an arbitrary-format document / any modality).
|
||||
|
||||
Built against the **proven** realizer faculty (neuron-talk sidecar `:8756`,
|
||||
artifact `art-7affa557`). The live soul (`:8742` / `:7770`) is contacted **only**
|
||||
through the read-only, GET-only `engram_client` — never mutated.
|
||||
|
||||
## The pipeline (surface-agnostic)
|
||||
|
||||
```
|
||||
geometry region + surface/format spec
|
||||
→ PLAN (manifold → document skeleton/DAG; the geometry IS the outline) plan.py
|
||||
→ REALIZE (proven realizer, scaled sentence → passage, each section faithful) realize.py
|
||||
→ COHERE (document-level flow / transitions, not stitched sentences) cohere.py
|
||||
→ EMIT (pluggable SurfaceProjector → the target surface) projectors/
|
||||
```
|
||||
|
||||
**The surface is a PARAMETER.** `pipeline.build_ir(...)` builds ONE
|
||||
surface-neutral `DocumentIR` (`document_ir.py`); `pipeline.emit(doc, surface)`
|
||||
projects it to whichever surface you name. Markdown, docx, and MIDI are the same
|
||||
IR emitted three ways.
|
||||
|
||||
## The pivot: a geometry-carrying IR
|
||||
|
||||
`DocumentIR` is **not** a text tree. Every `Block` carries BOTH:
|
||||
- `.sentences` — realized faithful text (what **text** projectors read),
|
||||
- `.provenance` — the source geometry: `subj_id / relation / obj / polarity /
|
||||
confidence / importance / salience / node_id` (what **music / image / video**
|
||||
projectors read).
|
||||
|
||||
That single decision is what makes the projector multimodal: text renders the
|
||||
words; music/image decode the geometry. A claim with no provenance cannot exist
|
||||
in the IR — faithfulness is structural.
|
||||
|
||||
## The one shared seam
|
||||
|
||||
`projectors/base.py` — `SurfaceProjector.project(frame: DocumentIR) -> bytes`
|
||||
(+ `surface / media_type / ext / modality / profile`). Register with
|
||||
`register()`. Adding a surface changes nothing upstream.
|
||||
|
||||
`TwoStageProjector` blesses the peer plan/realize decomposition:
|
||||
`spec = plan(frame)`, `bytes = realize(spec)`, `project = realize∘plan`; the
|
||||
`profile` is the pluggable per-surface knob (text lang-profile, music
|
||||
instr/mode-profile). `projectors/midi.py` is the reference two-stage impl.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| surface | modality | status | emitter |
|
||||
|---|---|---|---|
|
||||
| `markdown` | text | landed | own (str) |
|
||||
| `docx` | text | landed | own minimal OOXML (stdlib `zipfile`+XML, no lib) |
|
||||
| `midi` | audio | landed (symbolic-music proof) | own minimal SMF (stdlib `struct`, no lib) |
|
||||
| `audio` (WAV) | audio | peer agent (additive synth) | conforms to `TwoStageProjector` |
|
||||
| `image` | image | documented seam | `projectors/seams.py` |
|
||||
| `video` | video | documented seam (image×sound×time) | `projectors/seams.py` |
|
||||
|
||||
Music maps: relation → scale degree (same relation → same pitch), **polarity →
|
||||
major/minor third (SACRED negation is audible)**, confidence → duration,
|
||||
importance → velocity, section → register. Deterministic projection from meaning
|
||||
— nothing invented.
|
||||
|
||||
## Faithfulness
|
||||
|
||||
`provenance.py` audits the IR: **zero** ungrounded claims, SACRED polarity
|
||||
preserved (negations reported, never dropped), COHERE introduces no new geometry
|
||||
(connectives are marked). `trace_table()` emits the geometry → section → claim
|
||||
table.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
PY=~/Desktop/lang-realizers/venv/bin/python
|
||||
PYTHONPATH=~/Desktop/neuron-talk:~/Desktop/lang-realizers $PY generate.py
|
||||
# writes ./out/{neuron-self,engram-temporal}.{md,docx,mid} + *.audit.json + *.provenance.md
|
||||
```
|
||||
|
||||
Requires the proven realizer env (spaCy + the neuron-talk/lang-realizers engine)
|
||||
and the read-only engram at `:8742`.
|
||||
@@ -1,79 +0,0 @@
|
||||
"""cohere.py — COHERE stage: document-level flow, not stitched sentences.
|
||||
|
||||
Fidelity is REALIZE's job; FLOW is this stage's. The hard part beyond sentence
|
||||
fidelity is that a document must read as one thing. We add connective tissue at
|
||||
the passage level:
|
||||
|
||||
* an opening abstract that names what the document covers (built ONLY from the
|
||||
section headings that already exist — it introduces no new claim),
|
||||
* a short transition lead into each section after the first, drawn from a
|
||||
fixed set of discourse connectives ("Beyond that,", "Relatedly,", ...) that
|
||||
carry no propositional content,
|
||||
* ordering so the highest-grounded section leads.
|
||||
|
||||
CRITICAL: every connective is marked ``kind="connective"`` in its provenance, so
|
||||
the faithfulness audit can prove COHERE introduced ZERO new geometry claims. A
|
||||
transition is discourse glue, never a fact.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from document_ir import Block, DocumentIR, Provenance
|
||||
|
||||
# discourse connectives — pure flow, no propositional content
|
||||
_TRANSITIONS = [
|
||||
"Beyond that,", "Relatedly,", "In the same region,", "From there,",
|
||||
"Alongside this,", "Further,", "Turning to the next facet,",
|
||||
]
|
||||
|
||||
|
||||
def _connective_prov() -> Provenance:
|
||||
return Provenance(subj_id=None, subject=None, relation="", obj=None,
|
||||
polarity="aff", confidence=1.0, node_id=None,
|
||||
kind="connective")
|
||||
|
||||
|
||||
def _abstract_block(doc: DocumentIR) -> Block:
|
||||
"""A grounded opening: names the sections, asserts nothing new."""
|
||||
headings = [s.heading for s in doc.sections]
|
||||
if not headings:
|
||||
return Block(role="lead")
|
||||
if len(headings) == 1:
|
||||
body = f"This document, generated from Neuron's geometry, covers {headings[0]}."
|
||||
else:
|
||||
listed = ", ".join(headings[:-1]) + f", and {headings[-1]}"
|
||||
body = ("This document is projected directly from Neuron's meaning-geometry. "
|
||||
f"It traces {listed}.")
|
||||
b = Block(role="lead")
|
||||
b.sentences.append(body)
|
||||
b.provenance.append(_connective_prov())
|
||||
return b
|
||||
|
||||
|
||||
def cohere_document(doc: DocumentIR, *, add_abstract: bool = True,
|
||||
add_transitions: bool = True) -> DocumentIR:
|
||||
"""Order sections by grounding, add abstract + transitions (flow only)."""
|
||||
# order: strongest-grounded section (mean confidence x #claims) first,
|
||||
# but keep an explicitly-first section if the plan pinned one via level 1.
|
||||
def _score(sec):
|
||||
provs = [p for p in sec.all_provenance() if p.kind == "fact"]
|
||||
if not provs:
|
||||
return 0.0
|
||||
mean_conf = sum(p.confidence for p in provs) / len(provs)
|
||||
return mean_conf * len(provs)
|
||||
|
||||
doc.sections.sort(key=_score, reverse=True)
|
||||
|
||||
if add_transitions:
|
||||
for i, sec in enumerate(doc.sections):
|
||||
if i == 0 or not sec.blocks:
|
||||
continue
|
||||
lead = _TRANSITIONS[(i - 1) % len(_TRANSITIONS)]
|
||||
first = sec.blocks[0]
|
||||
if first.sentences:
|
||||
# prepend the connective to the first sentence (flow, no new claim)
|
||||
first.sentences[0] = f"{lead} {first.sentences[0][0].lower()}{first.sentences[0][1:]}"
|
||||
|
||||
if add_abstract:
|
||||
doc.meta["abstract"] = _abstract_block(doc)
|
||||
|
||||
return doc
|
||||
@@ -1,111 +0,0 @@
|
||||
"""document_ir.py — the surface-neutral, GEOMETRY-CARRYING document intermediate.
|
||||
|
||||
This is the pivot of the whole efferent projector. A DocumentIR is NOT a text
|
||||
tree. It is a projection of a meaning-geometry region that carries, at every
|
||||
leaf, BOTH:
|
||||
|
||||
* the realized surface text (``Block.sentences``) — what a TEXT projector reads,
|
||||
* the source geometry (``Block.provenance``) — what a MUSIC / IMAGE /
|
||||
VIDEO projector reads.
|
||||
|
||||
Because the IR holds the geometry, not just the words, the SAME
|
||||
plan -> realize -> cohere pipeline drives every surface. A markdown projector
|
||||
renders the sentences; a music projector reads the provenance edges (salience,
|
||||
importance, polarity, relation) and maps them onto a symbolic-music surface;
|
||||
an image/video projector (documented seam) would read the same geometry.
|
||||
|
||||
Nothing in this module invents content. Every :class:`Provenance` points at a
|
||||
real engram node id and a real relation. That is the faithfulness contract made
|
||||
structural: a claim with no provenance cannot exist in the IR.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Provenance — the geometry an emitted claim traces to. FAITHFULNESS is here.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass
|
||||
class Provenance:
|
||||
"""One geometry edge behind one realized claim.
|
||||
|
||||
``kind`` distinguishes a FACT (a structural edge asserted by the geometry,
|
||||
spoken as fact) from an INTERPRETATION (something attributed, spoken with
|
||||
attribution) — the facts-as-facts + interpretations-attributed discipline
|
||||
(memory 80927e26). ``polarity`` is SACRED: a negated edge stays negated.
|
||||
"""
|
||||
subj_id: str | None # source engram node id of the subject
|
||||
subject: str | None # normalized subject surface
|
||||
relation: str # predicate lemma (e.g. "use", "contain", "be")
|
||||
obj: str | None # normalized object / complement surface
|
||||
polarity: str = "aff" # "aff" | "neg" (SACRED — never silently flipped)
|
||||
confidence: float = 0.0 # extraction confidence in [0,1]
|
||||
node_id: str | None = None # engram node the claim was extracted from
|
||||
kind: str = "fact" # "fact" | "interpretation"
|
||||
importance: float = 0.0 # source node importance (drives music/emphasis)
|
||||
salience: float = 0.0 # source node salience
|
||||
|
||||
def trace(self) -> str:
|
||||
arrow = "-->" if self.polarity == "aff" else "--NOT-->"
|
||||
return (f"[{(self.node_id or '?')[:8]}] {self.subject!r} {arrow}"
|
||||
f"{self.relation} {self.obj!r} (conf {self.confidence:.2f})")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block:
|
||||
"""A passage: one or more faithful sentences + the geometry they trace to.
|
||||
|
||||
``sentences`` and ``provenance`` are index-aligned where possible: sentence
|
||||
``i`` was realized from ``provenance[i]``. A COHERE transition sentence with
|
||||
no new geometry carries a provenance whose ``kind == "connective"`` so the
|
||||
audit can see it introduced no new claim.
|
||||
"""
|
||||
sentences: list[str] = field(default_factory=list)
|
||||
provenance: list[Provenance] = field(default_factory=list)
|
||||
role: str = "body" # "body" | "lead" | "transition"
|
||||
|
||||
def text(self) -> str:
|
||||
return " ".join(s.rstrip(". ") + "." for s in self.sentences if s.strip())
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
heading: str
|
||||
level: int = 2 # markdown heading level / outline depth
|
||||
blocks: list[Block] = field(default_factory=list)
|
||||
seed_ids: list[str] = field(default_factory=list) # geometry nodes of section
|
||||
summary: str = "" # one-line grounded gloss (for pptx bullets / TOC)
|
||||
|
||||
def all_provenance(self) -> list[Provenance]:
|
||||
out: list[Provenance] = []
|
||||
for b in self.blocks:
|
||||
out.extend(b.provenance)
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentIR:
|
||||
"""The surface-neutral document. Built ONCE, projected to ANY surface."""
|
||||
title: str
|
||||
subtitle: str = ""
|
||||
sections: list[Section] = field(default_factory=list)
|
||||
seed_id: str | None = None # the geometry region root
|
||||
format_spec: dict[str, Any] = field(default_factory=dict) # requested shape
|
||||
meta: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# -- geometry facets (what non-text projectors consume) ----------------- #
|
||||
def all_provenance(self) -> list[Provenance]:
|
||||
out: list[Provenance] = []
|
||||
for s in self.sections:
|
||||
out.extend(s.all_provenance())
|
||||
return out
|
||||
|
||||
def claim_count(self) -> int:
|
||||
return sum(1 for p in self.all_provenance() if p.kind in ("fact", "interpretation"))
|
||||
|
||||
def ungrounded_count(self) -> int:
|
||||
"""Claims with no traceable node — MUST be zero for a faithful doc."""
|
||||
return sum(1 for p in self.all_provenance()
|
||||
if p.kind in ("fact", "interpretation") and not p.node_id)
|
||||
@@ -1,81 +0,0 @@
|
||||
"""generate.py — drive the projector: one geometry region -> many surfaces.
|
||||
|
||||
Proves the thesis with REAL output: builds ONE surface-neutral DocumentIR from
|
||||
Neuron's OWN self-geometry (read-only against the live soul via the proven
|
||||
faculty), then EMITS it to Markdown, docx, and MIDI — the same plan/realize/
|
||||
cohere, three surfaces. Writes the files + the faithfulness audit to ./out/.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import pipeline # noqa: E402
|
||||
import provenance # noqa: E402
|
||||
from geometry import load_self_region # noqa: E402
|
||||
|
||||
OUT = os.path.join(_HERE, "out")
|
||||
|
||||
|
||||
def _emit_all(doc, stem):
|
||||
"""Emit one IR to every text/audio surface + audit + provenance."""
|
||||
for surface in ("markdown", "docx", "midi"):
|
||||
data = pipeline.emit(doc, surface)
|
||||
proj = pipeline.get_projector(surface)
|
||||
path = os.path.join(OUT, f"{stem}.{proj.ext}")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
print(f" emitted {surface:9s} -> {os.path.basename(path)} ({len(data)} bytes)")
|
||||
a = provenance.audit(doc)
|
||||
with open(os.path.join(OUT, f"{stem}.audit.json"), "w") as f:
|
||||
json.dump(a, f, indent=2)
|
||||
with open(os.path.join(OUT, f"{stem}.provenance.md"), "w") as f:
|
||||
f.write(provenance.trace_table(doc))
|
||||
print(" audit:", {k: a[k] for k in ("claims", "ungrounded_claims",
|
||||
"negations_preserved", "distinct_source_nodes", "faithful")})
|
||||
return a
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
print("surfaces registered:", pipeline.available_surfaces())
|
||||
|
||||
# ---- Document 1: Neuron's self-description (marquee) ------------------- #
|
||||
print("\n[1] Neuron self-description")
|
||||
region = load_self_region(max_nodes=9)
|
||||
print(" self region:", region)
|
||||
doc1 = pipeline.build_ir(
|
||||
None, region=region,
|
||||
title="Neuron: A Self-Description from Its Own Geometry",
|
||||
subtitle="Projected efferently from the engram — every claim traces a node.",
|
||||
format_spec={"genre": "self-description", "register": "expository"},
|
||||
max_sections=5, conf_floor=0.6)
|
||||
print(f" IR: {len(doc1.sections)} sections, {doc1.claim_count()} claims, "
|
||||
f"ungrounded={doc1.ungrounded_count()}")
|
||||
_emit_all(doc1, "neuron-self")
|
||||
|
||||
# ---- Document 2: a coherent, clean whitepaper-style section ------------ #
|
||||
print("\n[2] Whitepaper-style section (coherent clean region)")
|
||||
doc2, _ = pipeline.project(
|
||||
["chronoception", "time", "awareness", "engram", "temporal"],
|
||||
surface="markdown",
|
||||
title="Temporal Awareness in the Engram",
|
||||
subtitle="A section projected from the geometry of chronoception.",
|
||||
format_spec={"genre": "whitepaper-section", "register": "technical"},
|
||||
max_sections=4)
|
||||
print(f" IR: {len(doc2.sections)} sections, {doc2.claim_count()} claims, "
|
||||
f"ungrounded={doc2.ungrounded_count()}")
|
||||
_emit_all(doc2, "engram-temporal")
|
||||
|
||||
# echo both markdowns so they are visible in the run log
|
||||
for stem, doc in (("neuron-self", doc1), ("engram-temporal", doc2)):
|
||||
print(f"\n===== GENERATED MARKDOWN — {stem} =====\n")
|
||||
print(pipeline.emit(doc, "markdown").decode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,129 +0,0 @@
|
||||
"""geometry.py — READ-ONLY loader for a meaning-geometry region.
|
||||
|
||||
The efferent projector never writes to the soul. This module reaches the
|
||||
geometry through the PROVEN, read-only neuron-talk faculty (``engram_client``,
|
||||
GET-only, which physically refuses non-GET methods) against the running sidecar
|
||||
soul. The live daemon :8742 / :7770 is contacted ONLY through that read-only
|
||||
client — never mutated.
|
||||
|
||||
A "region" is a seed node plus a bounded neighborhood: the manifold that will
|
||||
become the document's skeleton. We pool a few single-term lexical searches
|
||||
(the engram search is a single-term matcher) and, when available, walk one hop
|
||||
of reified neighbors, then rank by self/importance signal.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Wire in the proven faculty (own-the-core: we reuse it, we do not fork it).
|
||||
_NT = os.path.expanduser("~/Desktop/neuron-talk")
|
||||
_LR = os.path.expanduser("~/Desktop/lang-realizers")
|
||||
for _p in (_NT, _LR):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
from engram_client import ReadOnlyEngramClient # noqa: E402
|
||||
|
||||
|
||||
class Region:
|
||||
"""A geometry region: ranked nodes + the reified edges among them."""
|
||||
|
||||
def __init__(self, seed: str, nodes: list[dict], edges: list[dict]):
|
||||
self.seed = seed
|
||||
self.nodes = nodes # ranked engram node dicts
|
||||
self.edges = edges # [{src, dst, edge, ...}]
|
||||
self.by_id = {n["id"]: n for n in nodes if n.get("id")}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Region seed={self.seed!r} nodes={len(self.nodes)} edges={len(self.edges)}>"
|
||||
|
||||
|
||||
def _prose_quality(content: str) -> float:
|
||||
"""Reward clean expository prose; penalize shouty banner-dense nodes.
|
||||
|
||||
A high ALLCAPS-word ratio or very short content signals a banner/telegraphic
|
||||
memory node that extracts into garbage. Clean declarative prose scores high.
|
||||
"""
|
||||
if not content or not content.strip():
|
||||
return 0.0
|
||||
words = content.split()
|
||||
if len(words) < 8:
|
||||
return 0.1
|
||||
caps = sum(1 for w in words if len(w) > 2 and w.strip(".,:;'\"-").isupper())
|
||||
caps_ratio = caps / max(1, len(words))
|
||||
# sentences with lowercase interior words read as prose
|
||||
lower = sum(1 for w in words if w[:1].islower())
|
||||
lower_ratio = lower / max(1, len(words))
|
||||
return max(0.0, 1.2 * lower_ratio - 2.0 * caps_ratio)
|
||||
|
||||
|
||||
def _relevance(content: str, terms: list[str]) -> float:
|
||||
"""Topical relevance to the seed terms — keeps a region ON-THEME so a clean
|
||||
but off-topic node cannot hijack the document."""
|
||||
if not terms:
|
||||
return 0.0
|
||||
low = (content or "").lower()
|
||||
hits = sum(1 for t in terms if t.lower() in low)
|
||||
return hits / max(1, len(terms))
|
||||
|
||||
|
||||
def _node_rank(n: dict, terms: list[str] | None = None) -> float:
|
||||
return (float(n.get("importance") or 0.0) * 2.0
|
||||
+ float(n.get("salience") or 0.0)
|
||||
+ 1.5 * _prose_quality(n.get("content") or "")
|
||||
+ 2.0 * _relevance(n.get("content") or "", terms or [])
|
||||
+ (0.5 if (n.get("content") or "").strip() else 0.0))
|
||||
|
||||
|
||||
def load_region(seed_terms: list[str] | str, *, client: ReadOnlyEngramClient | None = None,
|
||||
max_nodes: int = 10, per_term: int = 20, hop: bool = True) -> Region:
|
||||
"""Pull a bounded geometry region around ``seed_terms`` (read-only).
|
||||
|
||||
``seed_terms`` may be a single string or several probe terms; results are
|
||||
pooled and de-duplicated. When ``hop`` and the reified neighbor endpoint is
|
||||
live, one hop of neighbors is folded in so the region is a real
|
||||
neighborhood, not just a keyword hit list.
|
||||
"""
|
||||
client = client or ReadOnlyEngramClient()
|
||||
if isinstance(seed_terms, str):
|
||||
seed_terms = [seed_terms]
|
||||
|
||||
pool: dict[str, dict] = {}
|
||||
for term in seed_terms:
|
||||
for n in client.search(term, limit=per_term):
|
||||
if isinstance(n, dict) and n.get("id"):
|
||||
pool.setdefault(n["id"], n)
|
||||
|
||||
ranked = sorted(pool.values(), key=lambda n: _node_rank(n, seed_terms),
|
||||
reverse=True)
|
||||
nodes = ranked[:max_nodes]
|
||||
|
||||
edges: list[dict] = []
|
||||
if hop and nodes:
|
||||
present = {n["id"] for n in nodes}
|
||||
for n in list(nodes):
|
||||
try:
|
||||
for nb in client.neighbors(n["id"]):
|
||||
node = nb.get("node") if isinstance(nb, dict) else None
|
||||
edge = nb.get("edge") if isinstance(nb, dict) else None
|
||||
if node and node.get("id"):
|
||||
edges.append({"src": n["id"], "dst": node["id"],
|
||||
"edge": edge})
|
||||
# fold a strong neighbor into the region (bounded)
|
||||
if (node["id"] not in present and len(nodes) < max_nodes + 6
|
||||
and _node_rank(node, seed_terms) > 0.4):
|
||||
present.add(node["id"])
|
||||
nodes.append(node)
|
||||
except Exception: # noqa: BLE001 — read-only best-effort; never fatal
|
||||
continue
|
||||
|
||||
return Region(seed=", ".join(seed_terms), nodes=nodes, edges=edges)
|
||||
|
||||
|
||||
def load_self_region(client: ReadOnlyEngramClient | None = None,
|
||||
max_nodes: int = 10) -> Region:
|
||||
"""The self/identity region — Neuron's own geometry, for self-description."""
|
||||
return load_region(["self", "identity", "Neuron", "values", "memory",
|
||||
"imprint", "consciousness"],
|
||||
client=client, max_nodes=max_nodes)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""pipeline.py — the Efferent Multimodal Projector, top level.
|
||||
|
||||
geometry region + surface/format spec
|
||||
-> PLAN (manifold -> document skeleton/DAG)
|
||||
-> REALIZE (proven realizer, sentence -> passage, each section faithful)
|
||||
-> COHERE (document-level flow / transitions, not stitched sentences)
|
||||
-> EMIT (pluggable SurfaceProjector -> the target surface)
|
||||
|
||||
THE SURFACE IS A PARAMETER. ``project(...)`` builds the geometry-carrying
|
||||
DocumentIR once, then hands it to whichever surface projector the caller named.
|
||||
Markdown, docx, and midi (music) are all the SAME IR emitted differently. That
|
||||
is the efferent multimodal projector: geometry -> any surface.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, _HERE)
|
||||
sys.path.insert(0, os.path.join(_HERE, "projectors"))
|
||||
|
||||
from cohere import cohere_document # noqa: E402
|
||||
from document_ir import DocumentIR # noqa: E402
|
||||
from geometry import Region, load_region # noqa: E402
|
||||
from plan import plan_document # noqa: E402
|
||||
from realize import realize_document # noqa: E402
|
||||
|
||||
# registering the projectors (import for side-effect: each self-registers)
|
||||
import projectors.markdown # noqa: E402,F401
|
||||
import projectors.docx # noqa: E402,F401
|
||||
import projectors.midi # noqa: E402,F401
|
||||
import projectors.seams # noqa: E402,F401
|
||||
from projectors.base import available_surfaces, get_projector # noqa: E402
|
||||
|
||||
|
||||
def build_ir(seed_terms, *, title: str, subtitle: str = "",
|
||||
format_spec: dict | None = None,
|
||||
region: Region | None = None,
|
||||
max_sections: int = 8, conf_floor: float = 0.55) -> DocumentIR:
|
||||
"""geometry -> PLAN -> REALIZE -> COHERE = the surface-neutral DocumentIR."""
|
||||
region = region or load_region(seed_terms)
|
||||
doc = plan_document(region, title=title, subtitle=subtitle,
|
||||
format_spec=format_spec or {},
|
||||
conf_floor=conf_floor, max_sections=max_sections)
|
||||
doc = realize_document(doc)
|
||||
doc = cohere_document(doc)
|
||||
return doc
|
||||
|
||||
|
||||
def emit(doc: DocumentIR, surface: str) -> bytes:
|
||||
"""EMIT: project the built IR onto one surface (surface = a parameter)."""
|
||||
return get_projector(surface).project(doc)
|
||||
|
||||
|
||||
def project(seed_terms, *, surface: str, title: str, subtitle: str = "",
|
||||
format_spec: dict | None = None, region: Region | None = None,
|
||||
max_sections: int = 8) -> tuple[DocumentIR, bytes]:
|
||||
"""The full efferent projection: geometry + surface -> (IR, bytes)."""
|
||||
doc = build_ir(seed_terms, title=title, subtitle=subtitle,
|
||||
format_spec=format_spec, region=region,
|
||||
max_sections=max_sections)
|
||||
return doc, emit(doc, surface)
|
||||
|
||||
|
||||
__all__ = ["build_ir", "emit", "project", "available_surfaces",
|
||||
"get_projector", "load_region", "DocumentIR"]
|
||||
@@ -1,192 +0,0 @@
|
||||
"""plan.py — PLAN stage: geometry region -> document skeleton (a DAG/outline).
|
||||
|
||||
The manifold becomes the skeleton. We extract faithful propositions from the
|
||||
region's nodes (the proven neuron-talk extractor, SACRED polarity preserved),
|
||||
apply a quality floor, then GROUP them into sections. Grouping is by source
|
||||
node — each engram node is one coherent topic, so one salient node becomes one
|
||||
section. The section ORDER is the node ranking (importance/salience): the
|
||||
geometry decides the outline, not a template.
|
||||
|
||||
Output: a DocumentIR whose sections carry seed node ids and empty blocks. REALIZE
|
||||
fills the blocks; the plan owns the structure.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
_NT = os.path.expanduser("~/Desktop/neuron-talk")
|
||||
_LR = os.path.expanduser("~/Desktop/lang-realizers")
|
||||
for _p in (_NT, _LR):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
import propositions # noqa: E402 (the proven, faithful extractor)
|
||||
|
||||
from document_ir import DocumentIR, Section # noqa: E402
|
||||
from geometry import Region # noqa: E402
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Proposition quality — keep only clean, well-grounded claims.
|
||||
# --------------------------------------------------------------------------- #
|
||||
_JUNK_RE = re.compile(r"[.][a-z]{1,3}\b|[^A-Za-z0-9 '\-]") # ".o", stray symbols
|
||||
|
||||
|
||||
def _has_banner_token(s: str) -> bool:
|
||||
"""True if any word is an ALLCAPS banner token (DHARMA, ENGRAM, MEASURED)."""
|
||||
for w in (s or "").split():
|
||||
core = w.strip(".,:;'\"-")
|
||||
if len(core) > 2 and core.isupper():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _clean_prop(p, floor: float) -> bool:
|
||||
if p.confidence < floor:
|
||||
return False
|
||||
if not p.subject or not (p.object or (p.obj_np is not None)):
|
||||
return False
|
||||
subj = (p.subject or "").strip()
|
||||
obj = (p.object or "").strip()
|
||||
if len(subj) < 2:
|
||||
return False
|
||||
# banner-derived shouty fragments read as garbage in prose
|
||||
if _has_banner_token(subj) or _has_banner_token(obj):
|
||||
return False
|
||||
if propositions._is_shouty(p.sentence or ""):
|
||||
return False
|
||||
# junk tokens: file-extension fragments (".o"), stray non-word symbols
|
||||
if _JUNK_RE.search(subj) or _JUNK_RE.search(obj):
|
||||
return False
|
||||
# a proposition whose object repeats the subject is usually a parse artifact
|
||||
if obj and subj.lower() == obj.lower():
|
||||
return False
|
||||
# a bare copula with no real complement ("X is it") reads as noise
|
||||
if p.predicate == "be" and obj.lower() in ("it", "no", "nothing", "empty", ""):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _dedup(props):
|
||||
"""Drop duplicate claims. Two axes: (a) identical (pred,obj,polarity), and
|
||||
(b) same (subject,predicate) — which collapses a mis-split compound like
|
||||
"detection is post-hoc eval" -> "Detection is post/hoc/eval" into one claim
|
||||
(keep the highest-confidence surface)."""
|
||||
props = sorted(props, key=lambda p: p.confidence, reverse=True)
|
||||
seen_po, seen_sp, out = set(), set(), []
|
||||
for p in props:
|
||||
subj = (p.subject or "").lower()
|
||||
po = (p.predicate, (p.object or "").lower(), p.polarity)
|
||||
sp = (subj, p.predicate, p.polarity)
|
||||
if po in seen_po or sp in seen_sp:
|
||||
continue
|
||||
seen_po.add(po)
|
||||
seen_sp.add(sp)
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Heading derivation — a clean human heading from a node.
|
||||
# --------------------------------------------------------------------------- #
|
||||
_HEADING_RE = re.compile(r"^\s*#{1,4}\s+(.{2,70})\s*$", re.M)
|
||||
# node-type / system labels that are NOT topical headings
|
||||
_NONTOPIC_LABEL = re.compile(r"^(memory|node|knowledge|doc|session)[:/]", re.I)
|
||||
|
||||
|
||||
def _titlecase_banner(s: str) -> str:
|
||||
"""A shouty banner ("CHRONOCEPTION — SCALE-INVARIANCE") makes a fine title
|
||||
once Title-cased. Keep short acronyms uppercase."""
|
||||
def fix(w):
|
||||
core = w.strip("—-:,.")
|
||||
if len(core) <= 3 and core.isupper():
|
||||
return w # acronym
|
||||
return w.capitalize()
|
||||
return " ".join(fix(w) for w in s.split())
|
||||
|
||||
|
||||
def _clean_heading(text: str) -> str | None:
|
||||
"""First line only, no markdown, capped, banner Title-cased. None if unusable."""
|
||||
if not text:
|
||||
return None
|
||||
line = text.strip().splitlines()[0]
|
||||
line = re.sub(r"^#+\s*", "", line).strip().strip("#").strip()
|
||||
# cut at a natural break so a long banner heading stays a heading, not a para
|
||||
for sep in (" — ", " – ", ": ", ". "):
|
||||
if sep in line and len(line) > 48:
|
||||
line = line.split(sep)[0].strip()
|
||||
break
|
||||
if not (3 <= len(line) <= 64):
|
||||
return None
|
||||
if propositions._is_shouty(line):
|
||||
line = _titlecase_banner(line)
|
||||
return line or None
|
||||
|
||||
|
||||
def _heading_for(node: dict, fallback: str) -> str:
|
||||
label = (node.get("label") or "").strip()
|
||||
content = node.get("content") or ""
|
||||
candidates: list[str] = []
|
||||
# a node-type label ("memory:remembered") is never a topic — skip it
|
||||
if label and not _NONTOPIC_LABEL.match(label):
|
||||
candidates.append(label)
|
||||
m = _HEADING_RE.search(content)
|
||||
if m:
|
||||
candidates.append(m.group(1))
|
||||
# the leading banner/first sentence of the content is often the real title
|
||||
first = re.split(r"(?<=[.\n])", content.strip(), maxsplit=1)[0] if content.strip() else ""
|
||||
candidates.append(first)
|
||||
for c in candidates:
|
||||
h = _clean_heading(c)
|
||||
if h:
|
||||
return h
|
||||
return fallback
|
||||
|
||||
|
||||
def plan_document(region: Region, *, title: str, subtitle: str = "",
|
||||
format_spec: dict | None = None,
|
||||
conf_floor: float = 0.55,
|
||||
max_sections: int = 8,
|
||||
max_claims_per_section: int = 6) -> DocumentIR:
|
||||
"""Region -> DocumentIR skeleton. The geometry dictates the outline."""
|
||||
format_spec = format_spec or {}
|
||||
doc = DocumentIR(title=title, subtitle=subtitle,
|
||||
seed_id=region.nodes[0]["id"] if region.nodes else None,
|
||||
format_spec=format_spec)
|
||||
|
||||
made = 0
|
||||
seen_headings: set[str] = set()
|
||||
for node in region.nodes:
|
||||
if made >= max_sections:
|
||||
break
|
||||
props = propositions.extract(node.get("content") or "",
|
||||
node_id=node.get("id"),
|
||||
node_importance=float(node.get("importance") or 0.0),
|
||||
max_sentences=10)
|
||||
props = [p for p in props if _clean_prop(p, conf_floor)]
|
||||
props = _dedup(props)
|
||||
props.sort(key=lambda p: p.confidence, reverse=True)
|
||||
props = props[:max_claims_per_section]
|
||||
if not props:
|
||||
continue
|
||||
heading = _heading_for(node, fallback=f"Region {made + 1}")
|
||||
# cross-section dedup: a topic appears once. Distinguish by top claim
|
||||
# subject, else drop the collision so the outline stays clean.
|
||||
if heading.lower() in seen_headings:
|
||||
subj = (props[0].subject or "").strip().title()
|
||||
alt = f"{heading}: {subj}" if subj and subj.lower() not in heading.lower() else None
|
||||
if alt and alt.lower() not in seen_headings and len(alt) <= 64:
|
||||
heading = alt
|
||||
else:
|
||||
continue
|
||||
seen_headings.add(heading.lower())
|
||||
sec = Section(heading=heading, level=2, seed_ids=[node["id"]])
|
||||
# stash the planned propositions on the section for REALIZE
|
||||
sec.__dict__["_planned_props"] = props
|
||||
sec.__dict__["_node"] = node
|
||||
doc.sections.append(sec)
|
||||
made += 1
|
||||
|
||||
return doc
|
||||
@@ -1,106 +0,0 @@
|
||||
"""base.py — the SurfaceProjector interface + registry.
|
||||
|
||||
THE key abstraction of the efferent projector: a projector is a pure function
|
||||
from the surface-neutral, geometry-carrying DocumentIR to bytes on a target
|
||||
SURFACE. The surface is a PARAMETER. Adding a surface = registering one more
|
||||
projector; nothing upstream (plan/realize/cohere) changes.
|
||||
|
||||
DocumentIR --project--> bytes (per surface)
|
||||
|
||||
A TEXT projector reads ``block.sentences``. A NON-TEXT projector (music, image,
|
||||
video) reads ``block.provenance`` — the geometry the IR carries — and decodes it
|
||||
onto its surface. Both consume the SAME IR. That symmetry is the whole design:
|
||||
the realizer generalizes into a multimodal projector, geometry -> any surface.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from document_ir import DocumentIR # noqa: E402
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SurfaceProjector(Protocol):
|
||||
"""Geometry-document -> one surface. Implementations MUST be pure & faithful.
|
||||
|
||||
THE ONE SHARED SEAM. Every surface — text, music, image, video — conforms to
|
||||
this single contract:
|
||||
|
||||
project(frame: DocumentIR) -> bytes
|
||||
|
||||
where ``frame`` is the geometry-carrying meaning-geometry (the SemFrame at
|
||||
document scale; a single utterance is the degenerate one-section frame).
|
||||
|
||||
RECOMMENDED INTERNAL SHAPE (the peer music/text decomposition, blessed here
|
||||
so all surfaces share it): a projector may split ``project`` into
|
||||
|
||||
spec = self.plan(frame) # meaning-geometry -> surface-specific spec
|
||||
bytes = self.realize(spec) # spec -> surface, via this projector's PROFILE
|
||||
|
||||
``project`` is then ``realize(plan(frame))``. The PROFILE (a text lang-profile,
|
||||
a music instr/mode-profile, an image layout-profile) is a property of the
|
||||
projector instance — the pluggable knob. See :class:`TwoStageProjector`.
|
||||
|
||||
A TEXT projector's plan reads ``frame`` sentences; a MUSIC/IMAGE projector's
|
||||
plan reads ``frame.all_provenance()`` — the geometry — and derives its spec
|
||||
(pitch/harmony/rhythm, or layout) FROM the meaning, deterministically. Same
|
||||
frame, different profile.
|
||||
"""
|
||||
|
||||
surface: str # "markdown" | "docx" | "midi" | "audio" | "image" | "video"
|
||||
media_type: str # MIME type of the emitted bytes
|
||||
ext: str # file extension (no dot)
|
||||
modality: str # "text" | "audio" | "image" | "video"
|
||||
profile: object # the pluggable per-surface profile (may be None)
|
||||
|
||||
def project(self, doc: DocumentIR) -> bytes:
|
||||
"""Emit the document on this surface. Returns raw bytes."""
|
||||
...
|
||||
|
||||
|
||||
class TwoStageProjector:
|
||||
"""Optional base for the peer plan()/realize() decomposition.
|
||||
|
||||
Subclasses implement ``plan(frame) -> spec`` and ``realize(spec) -> bytes``;
|
||||
``project`` is their composition. This is exactly the peer music interface
|
||||
(spec = plan(frame, profile); surface = realize(spec, profile)) expressed so
|
||||
that it still satisfies the single ``SurfaceProjector.project`` seam. Text,
|
||||
music, and image projectors can all subclass this and remain interchangeable.
|
||||
"""
|
||||
|
||||
surface: str = ""
|
||||
media_type: str = ""
|
||||
ext: str = ""
|
||||
modality: str = ""
|
||||
profile: object = None
|
||||
|
||||
def plan(self, doc: DocumentIR): # -> spec
|
||||
raise NotImplementedError
|
||||
|
||||
def realize(self, spec) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
def project(self, doc: DocumentIR) -> bytes:
|
||||
return self.realize(self.plan(doc))
|
||||
|
||||
|
||||
_REGISTRY: dict[str, SurfaceProjector] = {}
|
||||
|
||||
|
||||
def register(projector: SurfaceProjector) -> SurfaceProjector:
|
||||
_REGISTRY[projector.surface] = projector
|
||||
return projector
|
||||
|
||||
|
||||
def get_projector(surface: str) -> SurfaceProjector:
|
||||
if surface not in _REGISTRY:
|
||||
raise KeyError(f"no projector registered for surface {surface!r}; "
|
||||
f"have {sorted(_REGISTRY)}")
|
||||
return _REGISTRY[surface]
|
||||
|
||||
|
||||
def available_surfaces() -> list[str]:
|
||||
return sorted(_REGISTRY)
|
||||
@@ -1,113 +0,0 @@
|
||||
"""docx.py — the .docx surface projector: an OWN minimal OOXML emitter.
|
||||
|
||||
Own-the-core: a .docx is just a ZIP of a few XML parts (WordprocessingML). We
|
||||
emit it with the standard library only — ``zipfile`` + string XML — no
|
||||
python-docx, no external dependency. This proves a "richer structured format"
|
||||
surface without importing anyone else's toolkit.
|
||||
|
||||
Parts emitted (the minimal valid set + a styles part for real headings):
|
||||
[Content_Types].xml
|
||||
_rels/.rels
|
||||
word/_rels/document.xml.rels
|
||||
word/styles.xml (Title / Heading1 / Heading2 / Normal)
|
||||
word/document.xml (the content)
|
||||
|
||||
Like the markdown projector it reads only the IR's realized sentences; it
|
||||
invents nothing. The surface differs, the faithful content does not.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from document_ir import DocumentIR # noqa: E402
|
||||
from projectors.base import register # noqa: E402
|
||||
|
||||
_CONTENT_TYPES = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
||||
</Types>"""
|
||||
|
||||
_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||
</Relationships>"""
|
||||
|
||||
_DOC_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
</Relationships>"""
|
||||
|
||||
_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
_STYLES = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:styles xmlns:w="{_W}">
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/>
|
||||
<w:rPr><w:sz w:val="22"/></w:rPr></w:style>
|
||||
<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/>
|
||||
<w:pPr><w:spacing w:after="240"/></w:pPr>
|
||||
<w:rPr><w:b/><w:sz w:val="52"/></w:rPr></w:style>
|
||||
<w:style w:type="paragraph" w:styleId="Subtitle"><w:name w:val="Subtitle"/>
|
||||
<w:rPr><w:i/><w:sz w:val="28"/><w:color w:val="555555"/></w:rPr></w:style>
|
||||
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/>
|
||||
<w:pPr><w:spacing w:before="240" w:after="120"/><w:outlineLvl w:val="0"/></w:pPr>
|
||||
<w:rPr><w:b/><w:sz w:val="34"/></w:rPr></w:style>
|
||||
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/>
|
||||
<w:pPr><w:spacing w:before="200" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr>
|
||||
<w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style>
|
||||
</w:styles>"""
|
||||
|
||||
|
||||
def _para(text: str, style: str | None = None) -> str:
|
||||
ppr = f"<w:pPr><w:pStyle w:val=\"{style}\"/></w:pPr>" if style else ""
|
||||
return (f"<w:p>{ppr}<w:r><w:t xml:space=\"preserve\">"
|
||||
f"{escape(text)}</w:t></w:r></w:p>")
|
||||
|
||||
|
||||
class DocxProjector:
|
||||
surface = "docx"
|
||||
media_type = ("application/vnd.openxmlformats-officedocument."
|
||||
"wordprocessingml.document")
|
||||
ext = "docx"
|
||||
modality = "text"
|
||||
|
||||
def _document_xml(self, doc: DocumentIR) -> str:
|
||||
body: list[str] = [_para(doc.title, "Title")]
|
||||
if doc.subtitle:
|
||||
body.append(_para(doc.subtitle, "Subtitle"))
|
||||
abstract = doc.meta.get("abstract")
|
||||
if abstract is not None and abstract.sentences:
|
||||
body.append(_para(abstract.text()))
|
||||
for sec in doc.sections:
|
||||
style = "Heading1" if sec.level <= 1 else "Heading2"
|
||||
body.append(_para(sec.heading, style))
|
||||
for block in sec.blocks:
|
||||
t = block.text()
|
||||
if t:
|
||||
body.append(_para(t))
|
||||
return (f"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
f"<w:document xmlns:w=\"{_W}\"><w:body>"
|
||||
+ "".join(body)
|
||||
+ "<w:sectPr><w:pgSz w:w=\"12240\" w:h=\"15840\"/>"
|
||||
"<w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" "
|
||||
"w:left=\"1440\"/></w:sectPr></w:body></w:document>")
|
||||
|
||||
def project(self, doc: DocumentIR) -> bytes:
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
z.writestr("[Content_Types].xml", _CONTENT_TYPES)
|
||||
z.writestr("_rels/.rels", _RELS)
|
||||
z.writestr("word/_rels/document.xml.rels", _DOC_RELS)
|
||||
z.writestr("word/styles.xml", _STYLES)
|
||||
z.writestr("word/document.xml", self._document_xml(doc))
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
register(DocxProjector())
|
||||
@@ -1,45 +0,0 @@
|
||||
"""markdown.py — the Markdown surface projector (text facet).
|
||||
|
||||
The most tractable surface, and the reference implementation: reads the IR's
|
||||
realized sentences and lays them out as Markdown. Introduces no content — it is
|
||||
pure typography over the faithful text the realizer produced.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from document_ir import DocumentIR # noqa: E402
|
||||
from projectors.base import register # noqa: E402
|
||||
|
||||
|
||||
class MarkdownProjector:
|
||||
surface = "markdown"
|
||||
media_type = "text/markdown"
|
||||
ext = "md"
|
||||
modality = "text"
|
||||
|
||||
def render_str(self, doc: DocumentIR) -> str:
|
||||
lines: list[str] = [f"# {doc.title}"]
|
||||
if doc.subtitle:
|
||||
lines.append(f"\n*{doc.subtitle}*")
|
||||
abstract = doc.meta.get("abstract")
|
||||
if abstract is not None and abstract.sentences:
|
||||
lines.append("")
|
||||
lines.append(abstract.text())
|
||||
for sec in doc.sections:
|
||||
lines.append("")
|
||||
lines.append(f"{'#' * max(2, sec.level)} {sec.heading}")
|
||||
for block in sec.blocks:
|
||||
body = block.text()
|
||||
if body:
|
||||
lines.append("")
|
||||
lines.append(body)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def project(self, doc: DocumentIR) -> bytes:
|
||||
return self.render_str(doc).encode("utf-8")
|
||||
|
||||
|
||||
register(MarkdownProjector())
|
||||
@@ -1,133 +0,0 @@
|
||||
"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI).
|
||||
|
||||
The first NON-TEXT surface, and the proof of the general shape. "Music is
|
||||
language and it is math" (Will): symbolic music is tractable and geometry-native,
|
||||
so it is the natural efferent twin to try first after text.
|
||||
|
||||
CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's
|
||||
GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical
|
||||
surface. That is the whole thesis of the multimodal projector: the same
|
||||
geometry-carrying IR drives text AND music; a text projector reads the words, a
|
||||
music projector reads the meaning-geometry. The mapping is deterministic and
|
||||
faithful to the geometry's structure:
|
||||
|
||||
relation lemma -> scale degree (same relation -> same pitch class;
|
||||
meaning has a consistent sonic form)
|
||||
polarity -> mode (aff = major third above; neg = minor
|
||||
third / lowered — SACRED polarity is
|
||||
audible, a negated edge sounds negated)
|
||||
confidence -> note duration (stronger grounding rings longer)
|
||||
importance -> velocity (more important source = louder)
|
||||
section -> phrase + register shift (structure becomes musical form)
|
||||
|
||||
Own-the-core: a Standard MIDI File is a header chunk + a track chunk of
|
||||
delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI
|
||||
library. Format 0, one track.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from document_ir import DocumentIR, Provenance # noqa: E402
|
||||
from projectors.base import TwoStageProjector, register # noqa: E402
|
||||
|
||||
_TICKS = 480 # ticks per quarter note
|
||||
_C_MAJOR = [0, 2, 4, 5, 7, 9, 11] # semitone offsets of a diatonic scale
|
||||
|
||||
|
||||
def _vlq(n: int) -> bytes:
|
||||
"""MIDI variable-length quantity encoding of a delta time."""
|
||||
if n == 0:
|
||||
return b"\x00"
|
||||
out = bytearray()
|
||||
out.append(n & 0x7F)
|
||||
n >>= 7
|
||||
while n:
|
||||
out.insert(0, (n & 0x7F) | 0x80)
|
||||
n >>= 7
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _degree_for(relation: str) -> int:
|
||||
"""Stable scale degree for a relation lemma (same relation -> same pitch)."""
|
||||
if not relation:
|
||||
return 0
|
||||
return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR)
|
||||
|
||||
|
||||
def _note_for(p: Provenance, base: int) -> tuple[int, int, int]:
|
||||
"""(pitch, velocity, duration_ticks) for one geometry edge."""
|
||||
root = base + _C_MAJOR[_degree_for(p.relation)]
|
||||
# polarity -> mode: affirmed edges take the bright major third, negated edges
|
||||
# take the darker minor third. The negation is AUDIBLE and never dropped.
|
||||
third = 4 if p.polarity == "aff" else 3
|
||||
pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0)))
|
||||
velocity = int(56 + 60 * min(1.0, max(0.0, p.importance)))
|
||||
velocity = max(40, min(120, velocity))
|
||||
# confidence -> duration: quarter .. dotted-half
|
||||
dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence))))
|
||||
return pitch, velocity, dur
|
||||
|
||||
|
||||
# a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale +
|
||||
# tempo. Swapping this profile re-voices the SAME geometry — surface as parameter.
|
||||
_DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000,
|
||||
"registers": [60, 55, 64, 50, 67, 48], "program": 0}
|
||||
|
||||
|
||||
class MidiProjector(TwoStageProjector):
|
||||
"""geometry -> symbolic music, in the shared two-stage shape.
|
||||
|
||||
``plan(frame)`` -> a music_spec: an ordered list of note dicts derived
|
||||
deterministically from the frame's provenance geometry
|
||||
(the peer's ``plan(frame, profile) -> spec``).
|
||||
``realize(spec)`` -> Standard MIDI File bytes (the peer's
|
||||
``realize(spec, profile) -> surface``; here the surface
|
||||
is symbolic MIDI, the minimal audio proof — a richer
|
||||
additive-synth audio projector conforms identically).
|
||||
"""
|
||||
|
||||
surface = "midi"
|
||||
media_type = "audio/midi"
|
||||
ext = "mid"
|
||||
modality = "audio"
|
||||
|
||||
def __init__(self, profile: dict | None = None):
|
||||
self.profile = profile or _DEFAULT_PROFILE
|
||||
|
||||
# -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- #
|
||||
def plan(self, doc: DocumentIR) -> list[dict]:
|
||||
registers = self.profile["registers"]
|
||||
spec: list[dict] = []
|
||||
for si, sec in enumerate(doc.sections):
|
||||
base = registers[si % len(registers)]
|
||||
provs = [p for p in sec.all_provenance()
|
||||
if p.kind in ("fact", "interpretation")]
|
||||
for i, p in enumerate(provs):
|
||||
pitch, vel, dur = _note_for(p, base)
|
||||
spec.append({"pitch": pitch, "velocity": vel, "dur": dur,
|
||||
"rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0,
|
||||
"relation": p.relation, "polarity": p.polarity})
|
||||
return spec
|
||||
|
||||
# -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ #
|
||||
def realize(self, spec: list[dict]) -> bytes:
|
||||
ev = bytearray()
|
||||
ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:]
|
||||
ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F])
|
||||
for note in spec:
|
||||
ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]])
|
||||
ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0])
|
||||
ev += _vlq(0) + b"\xFF\x2F\x00"
|
||||
track = bytes(ev)
|
||||
buf = io.BytesIO()
|
||||
buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS))
|
||||
buf.write(b"MTrk" + struct.pack(">I", len(track)) + track)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
register(MidiProjector())
|
||||
@@ -1,60 +0,0 @@
|
||||
"""seams.py — documented efferent seams for IMAGE and VIDEO surfaces.
|
||||
|
||||
These are NOT implemented (per the build rails: architect, do not overbuild).
|
||||
They are registered as first-class seams so the interface PROVES it accepts
|
||||
future non-text projectors without any upstream change. Each documents exactly
|
||||
what its decoder would read from the geometry-carrying IR, making the multimodal
|
||||
generalization concrete rather than hand-wavy.
|
||||
|
||||
The symmetry that guarantees these are possible, not moonshots: they are the
|
||||
efferent twins of multimodal INGEST. If meaning can HOLD an image (ingest as
|
||||
first-class geometry), meaning can PROJECT one back. Video = image x sound x
|
||||
TIME, and the engram already stores time (chronoception). So video falls out of
|
||||
an image projector + the music projector + the stored temporal ordering.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from document_ir import DocumentIR # noqa: E402
|
||||
from projectors.base import register # noqa: E402
|
||||
|
||||
|
||||
class _Seam:
|
||||
"""A registered-but-unimplemented projector. Names its decoder contract."""
|
||||
|
||||
def project(self, doc: DocumentIR) -> bytes: # pragma: no cover - seam
|
||||
raise NotImplementedError(
|
||||
f"{self.surface!r} projector is a documented seam, not yet built. "
|
||||
f"Decoder contract: {self.decoder_contract}")
|
||||
|
||||
|
||||
class ImageProjector(_Seam):
|
||||
surface = "image"
|
||||
media_type = "image/png"
|
||||
ext = "png"
|
||||
modality = "image"
|
||||
decoder_contract = (
|
||||
"reads block.provenance as a spatial layout — nodes become regions, edges "
|
||||
"become adjacencies; salience/importance drive size/contrast; polarity "
|
||||
"drives figure/ground. The efferent twin of image ingest (a geometry->raster "
|
||||
"decoder, learned or engineered), exactly mirroring the embedder that turned "
|
||||
"the image INTO geometry.")
|
||||
|
||||
|
||||
class VideoProjector(_Seam):
|
||||
surface = "video"
|
||||
media_type = "video/mp4"
|
||||
ext = "mp4"
|
||||
modality = "video"
|
||||
decoder_contract = (
|
||||
"image x sound x TIME. Composes the image projector (per-keyframe geometry "
|
||||
"layout) with the midi/music projector (score) along the geometry's stored "
|
||||
"temporal ordering (chronoception). Needs no new principle once image + music "
|
||||
"exist — only a muxer.")
|
||||
|
||||
|
||||
register(ImageProjector())
|
||||
register(VideoProjector())
|
||||
@@ -1,63 +0,0 @@
|
||||
"""provenance.py — the faithfulness audit + geometry->section trace.
|
||||
|
||||
A document projected from geometry is only worth anything if every claim traces
|
||||
back. This module walks the DocumentIR and proves the discipline held:
|
||||
|
||||
* ZERO ungrounded claims (every fact/interpretation has a real node id),
|
||||
* every emitted sentence maps to a geometry edge (or is a marked connective),
|
||||
* SACRED polarity survived (negations are reported, never silently dropped),
|
||||
* COHERE introduced no new geometry (connectives carry no claim).
|
||||
|
||||
It emits both a machine verdict and a human-readable geometry->section table.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from document_ir import DocumentIR
|
||||
|
||||
|
||||
def audit(doc: DocumentIR) -> dict:
|
||||
provs = doc.all_provenance()
|
||||
facts = [p for p in provs if p.kind in ("fact", "interpretation")]
|
||||
connectives = [p for p in provs if p.kind == "connective"]
|
||||
ungrounded = [p for p in facts if not p.node_id]
|
||||
negations = [p for p in facts if p.polarity == "neg"]
|
||||
node_ids = sorted({p.node_id for p in facts if p.node_id})
|
||||
return {
|
||||
"claims": len(facts),
|
||||
"connectives": len(connectives),
|
||||
"ungrounded_claims": len(ungrounded),
|
||||
"negations_preserved": len(negations),
|
||||
"distinct_source_nodes": len(node_ids),
|
||||
"faithful": len(ungrounded) == 0,
|
||||
"source_nodes": node_ids,
|
||||
}
|
||||
|
||||
|
||||
def trace_table(doc: DocumentIR) -> str:
|
||||
"""Human-readable geometry -> section -> claim provenance table."""
|
||||
lines = ["# Provenance — every claim traces geometry", ""]
|
||||
lines.append(f"**Document:** {doc.title}")
|
||||
a = audit(doc)
|
||||
lines.append(f"**Claims:** {a['claims']} · **Ungrounded:** "
|
||||
f"{a['ungrounded_claims']} · **Negations preserved:** "
|
||||
f"{a['negations_preserved']} · **Source nodes:** "
|
||||
f"{a['distinct_source_nodes']} · **Faithful:** "
|
||||
f"{'YES' if a['faithful'] else 'NO'}")
|
||||
lines.append("")
|
||||
for si, sec in enumerate(doc.sections, 1):
|
||||
lines.append(f"## {si}. {sec.heading}")
|
||||
lines.append(f"_seed nodes: {', '.join(i[:8] for i in sec.seed_ids)}_")
|
||||
lines.append("")
|
||||
lines.append("| # | realized claim | traces geometry edge |")
|
||||
lines.append("|---|----------------|----------------------|")
|
||||
n = 0
|
||||
for block in sec.blocks:
|
||||
for sent, prov in zip(block.sentences, block.provenance):
|
||||
if prov.kind == "connective":
|
||||
continue
|
||||
n += 1
|
||||
edge = prov.trace().replace("|", "\\|")
|
||||
s = sent.replace("|", "\\|")
|
||||
lines.append(f"| {n} | {s} | {edge} |")
|
||||
lines.append("")
|
||||
return "\n".join(lines) + "\n"
|
||||
@@ -1,112 +0,0 @@
|
||||
"""realize.py — REALIZE stage: fill each planned section with faithful passages.
|
||||
|
||||
Scales the PROVEN realizer from a single assertion to a passage. For each
|
||||
planned proposition we build a realizer-ready clause (the proven
|
||||
``_prop_to_clause`` mapping) and run it through the proven engine
|
||||
(``engine.realize``), which is a deterministic grammar with the SACRED negation
|
||||
contract — it never invents. Each realized sentence is paired with a
|
||||
:class:`Provenance` that pins it to the exact geometry edge it came from.
|
||||
|
||||
"Passage, not a list of sentences": within a section we lightly vary sentence
|
||||
openings and group related claims, but we add NO content the geometry did not
|
||||
assert. The only non-geometry words are function words the grammar already owns
|
||||
(articles, "and", conjunction of same-subject claims). Document-level flow is
|
||||
COHERE's job; this stage owns intra-section fluency + fidelity.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_NT = os.path.expanduser("~/Desktop/neuron-talk")
|
||||
_LR = os.path.expanduser("~/Desktop/lang-realizers")
|
||||
for _p in (_NT, _LR):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
import engine # noqa: E402 (the proven no-LLM realizer)
|
||||
from dialogue import _prop_to_clause # noqa: E402 (proven prop -> clause)
|
||||
|
||||
from document_ir import Block, DocumentIR, Provenance, Section # noqa: E402
|
||||
|
||||
|
||||
def _provenance_from(p, kind: str = "fact") -> Provenance:
|
||||
return Provenance(
|
||||
subj_id=p.source_node_id, subject=p.subject, relation=p.predicate,
|
||||
obj=p.object, polarity=p.polarity, confidence=round(float(p.confidence), 3),
|
||||
node_id=p.source_node_id, kind=kind,
|
||||
importance=float(getattr(p, "node_importance", 0.0) or 0.0),
|
||||
salience=0.0,
|
||||
)
|
||||
|
||||
|
||||
import re as _re
|
||||
|
||||
# a well-formed declarative opens with a determiner, a proper noun, "I", or a
|
||||
# capitalized head — not a mis-parsed object pronoun or a copula fragment.
|
||||
_BAD_OPENERS = _re.compile(r"^(Me |It is I|There is|This is it|That is it)\b")
|
||||
_VACUOUS = _re.compile(r"^\w+ (is|are|was|were) (it|no|nothing|empty|those|this|that)\.?$",
|
||||
_re.I)
|
||||
|
||||
|
||||
def _good_sentence(text: str) -> bool:
|
||||
"""Fluency gate — drops degenerate realizations. NEVER loosens faithfulness;
|
||||
it only refuses to SPEAK a claim whose surface came out malformed."""
|
||||
words = text.rstrip(".").split()
|
||||
if len(words) < 3:
|
||||
return False
|
||||
if _BAD_OPENERS.search(text):
|
||||
return False
|
||||
if _VACUOUS.match(text):
|
||||
return False
|
||||
# a sentence that is mostly one-letter/two-letter tokens is a parse artifact
|
||||
short = sum(1 for w in words if len(w.strip(".,'")) <= 2)
|
||||
if short > len(words) / 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _realize_prop(p, lang: str = "en") -> tuple[str, Provenance] | None:
|
||||
"""One proposition -> (faithful sentence, provenance) or None if it drops."""
|
||||
clause = _prop_to_clause(p)
|
||||
text = engine.realize(clause, lang)
|
||||
if not text or not text.strip():
|
||||
return None
|
||||
text = text.strip()
|
||||
if not text.endswith((".", "!", "?")):
|
||||
text += "."
|
||||
# capitalize first character (proper nouns / "I" already handled by grammar)
|
||||
text = text[0].upper() + text[1:]
|
||||
if not _good_sentence(text):
|
||||
return None
|
||||
return text, _provenance_from(p)
|
||||
|
||||
|
||||
def realize_document(doc: DocumentIR, lang: str = "en") -> DocumentIR:
|
||||
"""Fill every planned section's blocks with faithful, realized passages."""
|
||||
for sec in doc.sections:
|
||||
planned = sec.__dict__.get("_planned_props", [])
|
||||
block = Block(role="body")
|
||||
summary_bits: list[str] = []
|
||||
for p in planned:
|
||||
r = _realize_prop(p, lang)
|
||||
if r is None:
|
||||
continue
|
||||
text, prov = r
|
||||
block.sentences.append(text)
|
||||
block.provenance.append(prov)
|
||||
if len(summary_bits) < 1:
|
||||
# a short grounded gloss for TOC / pptx bullets
|
||||
obj = (prov.obj or "").strip().rstrip(".")
|
||||
if obj:
|
||||
summary_bits.append(obj)
|
||||
if block.sentences:
|
||||
sec.blocks.append(block)
|
||||
sec.summary = summary_bits[0] if summary_bits else ""
|
||||
# drop the transient planning payload; the IR is now self-contained
|
||||
sec.__dict__.pop("_planned_props", None)
|
||||
sec.__dict__.pop("_node", None)
|
||||
|
||||
# prune sections that realized to nothing
|
||||
doc.sections = [s for s in doc.sections if s.blocks]
|
||||
return doc
|
||||
@@ -1,73 +0,0 @@
|
||||
// audio-demo.el - Drive the native audio surface: render a tone per instrument
|
||||
// from its LEARNED signature, then render a small meaning-phrase "piece".
|
||||
// Entry point: top-level statement calls main() (same convention as the
|
||||
// examples' top-level println(run_test())).
|
||||
|
||||
fn micros_to_str(xs: [Int]) -> String {
|
||||
let n: Int = native_list_len(xs)
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if i > 0 { let out: String = out + "," }
|
||||
let out: String = out + int_to_str(native_list_get(xs, i))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Render a 1.0s A4 (midi 69) tone from a signature file, print the parsed
|
||||
// partials (proving the numbers came from the engram .sig), write the WAV.
|
||||
fn render_tone(name: String, sigpath: String, outpath: String, table: [Int]) -> Int {
|
||||
let lines: [String] = sig_load(sigpath)
|
||||
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
|
||||
println("[" + name + "] partials_n=" + sig_field(lines, "partials_n") + " parsed_partials_micro(scale 1e6)=" + micros_to_str(partials))
|
||||
println("[" + name + "] raw partials line from .sig = " + sig_field(lines, "partials"))
|
||||
let freq: Int = freq_of_midi(69)
|
||||
let note: [Int] = synth_from_sig(lines, freq, 1000, 900, 44100, table)
|
||||
let n: Int = native_list_len(note)
|
||||
let ok: Int = wav_write(outpath, note, n, 44100)
|
||||
println("[" + name + "] rendered " + int_to_str(n) + " samples -> " + outpath + " (write_ok=" + int_to_str(ok) + ")")
|
||||
return n
|
||||
}
|
||||
|
||||
fn run_demo() -> Int {
|
||||
let table: [Int] = sin_table()
|
||||
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
|
||||
|
||||
println("=== TONES: render A4 (midi 69) from each learned signature ===")
|
||||
render_tone("flute", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/flute.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-flute.wav", table)
|
||||
render_tone("clarinet", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/clarinet.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-clarinet.wav", table)
|
||||
render_tone("violin", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/violin.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-violin.wav", table)
|
||||
render_tone("piano", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-piano.wav", table)
|
||||
render_tone("organ", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/organ.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-organ.wav", table)
|
||||
|
||||
println("")
|
||||
println("=== PIECE: a 6-frame meaning phrase (incl. a NEG frame) ===")
|
||||
let frames: [[String]] = native_list_empty()
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("theme", "aff", "0.7", "0.6", "0", "s2"))
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("cause", "aff", "0.8", "0.9", "1", "s3"))
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("negation", "neg", "0.85", "0.7", "0", "s4"))
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("goal", "aff", "0.6", "0.5", "1", "s5"))
|
||||
let frames: [[String]] = native_list_append(frames, audio_frame("result", "aff", "0.95", "1.0", "0", "s6"))
|
||||
|
||||
// Print the plan so the NEG frame's minor third (+3) vs major (+4) is visible.
|
||||
let nf: Int = native_list_len(frames)
|
||||
let fi: Int = 0
|
||||
while fi < nf {
|
||||
let frame: [String] = native_list_get(frames, fi)
|
||||
let plan: [Int] = plan_note(frame)
|
||||
let pol: String = surface_get(frame, "polarity")
|
||||
let third_name: String = "major(+4)"
|
||||
if str_eq(pol, "neg") { let third_name: String = "MINOR(+3)" }
|
||||
println("frame " + int_to_str(fi) + " relation=" + surface_get(frame, "relation") + " polarity=" + pol + " -> midi=" + int_to_str(native_list_get(plan, 0)) + " dur_ms=" + int_to_str(native_list_get(plan, 1)) + " amp_pm=" + int_to_str(native_list_get(plan, 2)) + " third=" + third_name)
|
||||
let fi: Int = fi + 1
|
||||
}
|
||||
|
||||
let piano_lines: [String] = sig_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig")
|
||||
let total: Int = realize_audio(frames, piano_lines, "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav", 44100, table)
|
||||
println("PIECE rendered " + int_to_str(total) + " samples -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav")
|
||||
return total
|
||||
}
|
||||
|
||||
println("audio-demo main returned samples=" + int_to_str(run_demo()))
|
||||
@@ -1,400 +0,0 @@
|
||||
// audio-surface.el - Native own-core additive-synthesis audio surface.
|
||||
//
|
||||
// The AUDIO efferent seam, native, no Python and no library. This renders real
|
||||
// PCM .wav bytes from instrument SIGNATURES read from engram-sourced .sig data
|
||||
// files (elp/faculty/sig/*.sig) - the partial amplitudes are NEVER literals in
|
||||
// this source; they are parsed from the learned signature at run time. That is
|
||||
// the whole proof: render-from-learned-signatures.
|
||||
//
|
||||
// EL has no float arithmetic operator (codegen emits raw int64 ops for + - * /
|
||||
// on the shared 64-bit slot) and no float-arithmetic natives - so ALL synthesis
|
||||
// math here is own-core INTEGER fixed-point. Angles use a quarter-wave sine
|
||||
// table (scale 10000) from a fixed-point Taylor series; amplitudes are parsed to
|
||||
// micro (scale 1e6) straight from the .sig text; frequencies are milliHz ints.
|
||||
//
|
||||
// Pipeline mirrors the two-stage projector (midi.py): plan_note(frame) reads a
|
||||
// frame's meaning-geometry slot-map and derives (pitch, duration, amplitude);
|
||||
// realize_audio SUPERPOSES the signature's partials (the compose op) and
|
||||
// serialises RIFF/WAVE. Same frame -> midi OR audio.
|
||||
|
||||
// -- integer decimal + string helpers -----------------------------------------
|
||||
|
||||
fn str_to_int_el(s: String) -> Int {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = 0
|
||||
let v: Int = 0
|
||||
let neg: Bool = false
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c == 45 { let neg: Bool = true }
|
||||
if c >= 48 {
|
||||
if c < 58 {
|
||||
let v: Int = v * 10 + (c - 48)
|
||||
}
|
||||
}
|
||||
let i: Int = i + 1
|
||||
}
|
||||
if neg { return 0 - v }
|
||||
return v
|
||||
}
|
||||
|
||||
fn parse_micro(s: String) -> Int {
|
||||
let dot: Int = str_index_of(s, ".")
|
||||
if dot < 0 {
|
||||
return str_to_int_el(s) * 1000000
|
||||
}
|
||||
let n: Int = str_len(s)
|
||||
let ipart: String = str_slice(s, 0, dot)
|
||||
let fpart: String = str_slice(s, dot + 1, n)
|
||||
let iv: Int = str_to_int_el(ipart)
|
||||
let fv: Int = 0
|
||||
let scale: Int = 100000
|
||||
let fn2: Int = str_len(fpart)
|
||||
let i: Int = 0
|
||||
while i < 6 {
|
||||
let d: Int = 0
|
||||
if i < fn2 {
|
||||
let d: Int = str_char_code(fpart, i) - 48
|
||||
}
|
||||
let fv: Int = fv + d * scale
|
||||
let scale: Int = scale / 10
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return iv * 1000000 + fv
|
||||
}
|
||||
|
||||
// -- signature (engram data file) loader ---------------------------------------
|
||||
|
||||
fn sig_load(path: String) -> [String] {
|
||||
let text: String = fs_read(path)
|
||||
return str_split(text, "\n")
|
||||
}
|
||||
|
||||
fn sig_field(lines: [String], key: String) -> String {
|
||||
let pref: String = key + ": "
|
||||
let n: Int = native_list_len(lines)
|
||||
let plen: Int = str_len(pref)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ln: String = native_list_get(lines, i)
|
||||
if str_starts_with(ln, pref) {
|
||||
return str_slice(ln, plen, str_len(ln))
|
||||
}
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn parse_micros(csv: String) -> [Int] {
|
||||
let parts: [String] = str_split(csv, ",")
|
||||
let n: Int = native_list_len(parts)
|
||||
let out: [Int] = native_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let out: [Int] = native_list_append(out, parse_micro(native_list_get(parts, i)))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// -- fixed-point sine (own-core, quarter-wave Taylor table, scale 10000) --------
|
||||
|
||||
fn sin_table() -> [Int] {
|
||||
let HP: Int = 1570796
|
||||
let t: [Int] = native_list_empty()
|
||||
let q: Int = 0
|
||||
while q < 257 {
|
||||
let x: Int = q * HP / 256
|
||||
let x2: Int = x * x / 1000000
|
||||
let x3: Int = x2 * x / 1000000
|
||||
let x5: Int = x3 * x2 / 1000000
|
||||
let x7: Int = x5 * x2 / 1000000
|
||||
let x9: Int = x7 * x2 / 1000000
|
||||
let s: Int = x - x3 / 6 + x5 / 120 - x7 / 5040 + x9 / 362880
|
||||
let t: [Int] = native_list_append(t, s / 100)
|
||||
let q: Int = q + 1
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
fn sin_lookup(t: [Int], phase: Int) -> Int {
|
||||
let p: Int = phase % 1024
|
||||
if p < 0 { let p: Int = p + 1024 }
|
||||
let quad: Int = p / 256
|
||||
let r: Int = p % 256
|
||||
if quad == 0 { return native_list_get(t, r) }
|
||||
if quad == 1 { return native_list_get(t, 256 - r) }
|
||||
if quad == 2 { return 0 - native_list_get(t, r) }
|
||||
return 0 - native_list_get(t, 256 - r)
|
||||
}
|
||||
|
||||
fn isqrt_int(n: Int) -> Int {
|
||||
if n <= 0 { return 0 }
|
||||
let x: Int = n
|
||||
let y: Int = (x + 1) / 2
|
||||
while y < x {
|
||||
let x: Int = y
|
||||
let y: Int = (x + n / x) / 2
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// freq_of_midi: equal-tempered frequency in milliHz. 440000 mHz at midi 69.
|
||||
fn freq_of_midi(m: Int) -> Int {
|
||||
let f: Int = 440000
|
||||
if m > 69 {
|
||||
let k: Int = m - 69
|
||||
let i: Int = 0
|
||||
while i < k {
|
||||
let f: Int = f * 1059463 / 1000000
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return f
|
||||
}
|
||||
if m < 69 {
|
||||
let k: Int = 69 - m
|
||||
let i: Int = 0
|
||||
while i < k {
|
||||
let f: Int = f * 1000000 / 1059463
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return f
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// -- envelope (ADSR), scale 1000 -----------------------------------------------
|
||||
|
||||
fn adsr_env(i: Int, total: Int, atk_n: Int, dec_n: Int, sus_pm: Int, rel_n: Int) -> Int {
|
||||
if i < atk_n {
|
||||
if atk_n == 0 { return 1000 }
|
||||
return 1000 * i / atk_n
|
||||
}
|
||||
if i < atk_n + dec_n {
|
||||
if dec_n == 0 { return sus_pm }
|
||||
return 1000 - (1000 - sus_pm) * (i - atk_n) / dec_n
|
||||
}
|
||||
let rel_start: Int = total - rel_n
|
||||
if i < rel_start {
|
||||
return sus_pm
|
||||
}
|
||||
if rel_n == 0 { return 0 }
|
||||
let left: Int = total - i
|
||||
return sus_pm * left / rel_n
|
||||
}
|
||||
|
||||
// -- note synthesis: SUPERPOSE the learned partials -> [Int] samples -----------
|
||||
fn note_samples(freq_mHz: Int, dur_ms: Int, rate: Int, partials: [Int], sumP: Int, b_micro: Int, vib_rate: Int, vib_cents: Int, atk_ms: Int, dec_ms: Int, sus_pm: Int, rel_ms: Int, amp_pm: Int, table: [Int]) -> [Int] {
|
||||
let total: Int = dur_ms * rate / 1000
|
||||
let atk_n: Int = atk_ms * rate / 1000
|
||||
let dec_n: Int = dec_ms * rate / 1000
|
||||
let rel_n: Int = rel_ms * rate / 1000
|
||||
let np: Int = native_list_len(partials)
|
||||
let half_mhz: Int = rate * 1000 / 2
|
||||
let out: [Int] = native_list_empty()
|
||||
let i: Int = 0
|
||||
while i < total {
|
||||
let acc: Int = 0
|
||||
let k: Int = 0
|
||||
while k < np {
|
||||
let harm: Int = k + 1
|
||||
let amp_k: Int = native_list_get(partials, k)
|
||||
let factor: Int = 1000000
|
||||
if b_micro > 0 {
|
||||
let val: Int = 1000000 + b_micro * harm * harm
|
||||
let factor: Int = isqrt_int(val * 1000000)
|
||||
}
|
||||
let fn_mhz: Int = freq_mHz * harm
|
||||
let fn_mhz: Int = fn_mhz * factor / 1000000
|
||||
if vib_cents > 0 {
|
||||
if vib_rate > 0 {
|
||||
let vphase: Int = i * vib_rate * 1024 / rate
|
||||
let vs: Int = sin_lookup(table, vphase)
|
||||
let vibf: Int = 1000000 + (vib_cents * vs * 833) / 10000
|
||||
let fn_mhz: Int = fn_mhz * vibf / 1000000
|
||||
}
|
||||
}
|
||||
if fn_mhz <= half_mhz {
|
||||
let phase: Int = i * fn_mhz * 1024 / (rate * 1000)
|
||||
let sv: Int = sin_lookup(table, phase)
|
||||
let acc: Int = acc + sv * amp_k / 1000000
|
||||
}
|
||||
let k: Int = k + 1
|
||||
}
|
||||
let env: Int = adsr_env(i, total, atk_n, dec_n, sus_pm, rel_n)
|
||||
let s16: Int = acc * 2800000 / sumP
|
||||
let s16: Int = s16 * env / 1000
|
||||
let s16: Int = s16 * amp_pm / 1000
|
||||
if s16 > 32767 { let s16: Int = 32767 }
|
||||
if s16 < 0 - 32767 { let s16: Int = 0 - 32767 }
|
||||
let out: [Int] = native_list_append(out, s16)
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn synth_from_sig(lines: [String], freq_mHz: Int, dur_ms: Int, amp_pm: Int, rate: Int, table: [Int]) -> [Int] {
|
||||
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
|
||||
let np: Int = native_list_len(partials)
|
||||
let sumP: Int = 0
|
||||
let j: Int = 0
|
||||
while j < np {
|
||||
let pj: Int = native_list_get(partials, j)
|
||||
let sumP: Int = sumP + pj
|
||||
let j: Int = j + 1
|
||||
}
|
||||
if sumP <= 0 { let sumP: Int = 1000000 }
|
||||
let adsr: [String] = str_split(sig_field(lines, "adsr"), ",")
|
||||
let atk_ms: Int = parse_micro(native_list_get(adsr, 0)) / 1000
|
||||
let dec_ms: Int = parse_micro(native_list_get(adsr, 1)) / 1000
|
||||
let sus_pm: Int = parse_micro(native_list_get(adsr, 2)) / 1000
|
||||
let rel_ms: Int = parse_micro(native_list_get(adsr, 3)) / 1000
|
||||
let b_micro: Int = parse_micro(sig_field(lines, "inharmonicity_B"))
|
||||
let vib_rate: Int = str_to_int_el(sig_field(lines, "vibrato_rate_hz"))
|
||||
let vib_cents: Int = str_to_int_el(sig_field(lines, "vibrato_depth_cents"))
|
||||
return note_samples(freq_mHz, dur_ms, rate, partials, sumP, b_micro, vib_rate, vib_cents, atk_ms, dec_ms, sus_pm, rel_ms, amp_pm, table)
|
||||
}
|
||||
|
||||
// -- byte-buffer helpers (own-core, no library) --------------------------------
|
||||
|
||||
fn put_tag(buf: String, pos: Int, s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let buf: String = __str_set_char(buf, pos + i, str_char_code(s, i))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
fn put_u32le(buf: String, pos: Int, v: Int) -> String {
|
||||
let buf: String = __str_set_char(buf, pos, v % 256)
|
||||
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
|
||||
let buf: String = __str_set_char(buf, pos + 2, (v / 65536) % 256)
|
||||
let buf: String = __str_set_char(buf, pos + 3, (v / 16777216) % 256)
|
||||
return buf
|
||||
}
|
||||
|
||||
fn put_u16le(buf: String, pos: Int, v: Int) -> String {
|
||||
let buf: String = __str_set_char(buf, pos, v % 256)
|
||||
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
|
||||
return buf
|
||||
}
|
||||
|
||||
// -- WAV serializer: own-core RIFF/WAVE, PCM mono 16-bit -----------------------
|
||||
|
||||
fn wav_write(path: String, samples: [Int], n: Int, rate: Int) -> Int {
|
||||
let data_len: Int = n * 2
|
||||
let total: Int = 44 + data_len
|
||||
let buf: String = __str_alloc(total)
|
||||
let buf: String = put_tag(buf, 0, "RIFF")
|
||||
let buf: String = put_u32le(buf, 4, 36 + data_len)
|
||||
let buf: String = put_tag(buf, 8, "WAVE")
|
||||
let buf: String = put_tag(buf, 12, "fmt ")
|
||||
let buf: String = put_u32le(buf, 16, 16)
|
||||
let buf: String = put_u16le(buf, 20, 1)
|
||||
let buf: String = put_u16le(buf, 22, 1)
|
||||
let buf: String = put_u32le(buf, 24, rate)
|
||||
let buf: String = put_u32le(buf, 28, rate * 2)
|
||||
let buf: String = put_u16le(buf, 32, 2)
|
||||
let buf: String = put_u16le(buf, 34, 16)
|
||||
let buf: String = put_tag(buf, 36, "data")
|
||||
let buf: String = put_u32le(buf, 40, data_len)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: Int = native_list_get(samples, i)
|
||||
if v < 0 { let v: Int = v + 65536 }
|
||||
let buf: String = __str_set_char(buf, 44 + i * 2, v % 256)
|
||||
let buf: String = __str_set_char(buf, 44 + i * 2 + 1, (v / 256) % 256)
|
||||
let i: Int = i + 1
|
||||
}
|
||||
let ok: Int = fs_write_bytes(path, buf, total)
|
||||
return ok
|
||||
}
|
||||
|
||||
// -- plan: frame slot-map -> note atom (pitch, duration, amplitude) ------------
|
||||
|
||||
fn audio_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
|
||||
let f: [String] = native_list_empty()
|
||||
let f: [String] = native_list_append(f, "relation")
|
||||
let f: [String] = native_list_append(f, relation)
|
||||
let f: [String] = native_list_append(f, "polarity")
|
||||
let f: [String] = native_list_append(f, polarity)
|
||||
let f: [String] = native_list_append(f, "confidence")
|
||||
let f: [String] = native_list_append(f, confidence)
|
||||
let f: [String] = native_list_append(f, "importance")
|
||||
let f: [String] = native_list_append(f, importance)
|
||||
let f: [String] = native_list_append(f, "salience")
|
||||
let f: [String] = native_list_append(f, salience)
|
||||
let f: [String] = native_list_append(f, "subj_id")
|
||||
let f: [String] = native_list_append(f, subj_id)
|
||||
return f
|
||||
}
|
||||
|
||||
fn degree_offset(deg: Int) -> Int {
|
||||
if deg == 0 { return 0 }
|
||||
if deg == 1 { return 2 }
|
||||
if deg == 2 { return 4 }
|
||||
if deg == 3 { return 5 }
|
||||
if deg == 4 { return 7 }
|
||||
if deg == 5 { return 9 }
|
||||
return 11
|
||||
}
|
||||
|
||||
// returns [midi, dur_ms, amp_pm]
|
||||
fn plan_note(frame: [String]) -> [Int] {
|
||||
let relation: String = surface_get(frame, "relation")
|
||||
let polarity: String = surface_get(frame, "polarity")
|
||||
let confidence: String = surface_get(frame, "confidence")
|
||||
let importance: String = surface_get(frame, "importance")
|
||||
let salience: String = surface_get(frame, "salience")
|
||||
let rn: Int = str_len(relation)
|
||||
let csum: Int = 0
|
||||
let i: Int = 0
|
||||
while i < rn {
|
||||
let cc: Int = str_char_code(relation, i)
|
||||
let csum: Int = csum + cc
|
||||
let i: Int = i + 1
|
||||
}
|
||||
let deg: Int = csum % 7
|
||||
let third: Int = 4
|
||||
if str_eq(polarity, "neg") { let third: Int = 3 }
|
||||
let sal_oct: Int = str_to_int_el(salience)
|
||||
let doff: Int = degree_offset(deg)
|
||||
let midi: Int = 60 + sal_oct * 12 + doff + third
|
||||
let conf_micro: Int = parse_micro(confidence)
|
||||
let dur_ms: Int = 200 + conf_micro / 1000
|
||||
let imp_micro: Int = parse_micro(importance)
|
||||
let amp_pm: Int = 400 + imp_micro / 2000
|
||||
let out: [Int] = native_list_empty()
|
||||
let out: [Int] = native_list_append(out, midi)
|
||||
let out: [Int] = native_list_append(out, dur_ms)
|
||||
let out: [Int] = native_list_append(out, amp_pm)
|
||||
return out
|
||||
}
|
||||
|
||||
fn realize_audio(frames: [[String]], sig_lines: [String], path: String, rate: Int, table: [Int]) -> Int {
|
||||
let nf: Int = native_list_len(frames)
|
||||
let all: [Int] = native_list_empty()
|
||||
let count: Int = 0
|
||||
let fi: Int = 0
|
||||
while fi < nf {
|
||||
let frame: [String] = native_list_get(frames, fi)
|
||||
let plan: [Int] = plan_note(frame)
|
||||
let midi: Int = native_list_get(plan, 0)
|
||||
let dur_ms: Int = native_list_get(plan, 1)
|
||||
let amp_pm: Int = native_list_get(plan, 2)
|
||||
let freq: Int = freq_of_midi(midi)
|
||||
let note: [Int] = synth_from_sig(sig_lines, freq, dur_ms, amp_pm, rate, table)
|
||||
let nn: Int = native_list_len(note)
|
||||
let j: Int = 0
|
||||
while j < nn {
|
||||
let all: [Int] = native_list_append(all, native_list_get(note, j))
|
||||
let j: Int = j + 1
|
||||
}
|
||||
let count: Int = count + nn
|
||||
let fi: Int = fi + 1
|
||||
}
|
||||
let ok: Int = wav_write(path, all, count, rate)
|
||||
return count
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import "language-profile.el"
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a_nodedup.el"
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -1 +0,0 @@
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -1,6 +0,0 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a.el"
|
||||
|
||||
fn main_fn(x: String) -> String {
|
||||
return x
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a.el"
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
import "dedup_test_a_notail.el"
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
// elp.el - Engram Language Protocol — public API.
|
||||
//
|
||||
// Output half of the ELP: Engram semantic form → natural language surface text.
|
||||
// 31 languages. Ties together language-profile, vocabulary, morphology,
|
||||
// grammar, realizer, and semantics into a single entry point.
|
||||
//
|
||||
// Import chain (mirrors manifest.el dependency order):
|
||||
// language-profile (no deps)
|
||||
// vocabulary (no deps)
|
||||
// morphology (depends on: language-profile)
|
||||
// morphology-XX (depends on: morphology) — all language engines
|
||||
// grammar (depends on: language-profile)
|
||||
// realizer (depends on: morphology, grammar, language-profile)
|
||||
// semantics (depends on: grammar, realizer, language-profile)
|
||||
//
|
||||
// When elc processes a source that imports this file, it resolves all
|
||||
// transitive imports via depth-first deduplication — each module is
|
||||
// inlined exactly once regardless of how many importers reference it.
|
||||
|
||||
// ── Base layers ───────────────────────────────────────────────────────────────
|
||||
import "language-profile.el"
|
||||
import "vocabulary.el"
|
||||
|
||||
// ── Morphology: base engine ───────────────────────────────────────────────────
|
||||
import "morphology.el"
|
||||
|
||||
// ── Morphology: living languages ──────────────────────────────────────────────
|
||||
import "morphology-es.el"
|
||||
import "morphology-fr.el"
|
||||
import "morphology-de.el"
|
||||
import "morphology-ru.el"
|
||||
import "morphology-ja.el"
|
||||
import "morphology-fi.el"
|
||||
import "morphology-ar.el"
|
||||
import "morphology-hi.el"
|
||||
import "morphology-sw.el"
|
||||
|
||||
// ── Morphology: ancient / classical ──────────────────────────────────────────
|
||||
import "morphology-la.el"
|
||||
import "morphology-he.el"
|
||||
|
||||
// ── Morphology: dead languages ────────────────────────────────────────────────
|
||||
import "morphology-grc.el"
|
||||
import "morphology-ang.el"
|
||||
import "morphology-sa.el"
|
||||
import "morphology-got.el"
|
||||
import "morphology-non.el"
|
||||
import "morphology-enm.el"
|
||||
import "morphology-pi.el"
|
||||
import "morphology-fro.el"
|
||||
import "morphology-goh.el"
|
||||
import "morphology-sga.el"
|
||||
import "morphology-txb.el"
|
||||
import "morphology-peo.el"
|
||||
import "morphology-akk.el"
|
||||
import "morphology-uga.el"
|
||||
import "morphology-egy.el"
|
||||
import "morphology-sux.el"
|
||||
import "morphology-gez.el"
|
||||
import "morphology-cop.el"
|
||||
|
||||
// ── Higher layers ─────────────────────────────────────────────────────────────
|
||||
import "grammar.el"
|
||||
import "realizer.el"
|
||||
import "semantics.el"
|
||||
//
|
||||
// Entry points:
|
||||
//
|
||||
// generate(semantic_form_json) -> String
|
||||
// Low-level JSON-based API, defaults to English. SemanticForm JSON fields:
|
||||
// intent - "assert" | "question" | "command"
|
||||
// agent - subject (pronoun or noun phrase, optional for commands)
|
||||
// predicate - verb base form
|
||||
// patient - object noun phrase (optional)
|
||||
// location - prepositional phrase e.g. "in the park" (optional)
|
||||
// tense - "present" | "past" | "future" (default: "present")
|
||||
// aspect - "simple" | "progressive" | "perfect" (default: "simple")
|
||||
// lang - ISO 639-1 code (default: "en")
|
||||
//
|
||||
// generate_lang(semantic_form_json, lang_code) -> String
|
||||
// JSON-based API with explicit language code (overrides any "lang" in JSON).
|
||||
//
|
||||
// generate_frame(frame: SemFrame) -> String
|
||||
// High-level SemFrame API. Language from frame's "lang" field (default "en").
|
||||
// Intents: "assert" | "query" | "describe" | "greet".
|
||||
//
|
||||
// generate_frame_lang(frame: SemFrame, lang_code: String) -> String
|
||||
// High-level SemFrame API with explicit language code override.
|
||||
|
||||
// ── JSON helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn sem_get(json: String, key: String) -> String {
|
||||
let val: String = json_get(json, key)
|
||||
return val
|
||||
}
|
||||
|
||||
// ── Public API: SemFrame ──────────────────────────────────────────────────────
|
||||
|
||||
// Generate text from a SemFrame in the language embedded in the frame (default "en").
|
||||
fn generate_frame(frame: [String]) -> String {
|
||||
return sem_realize(frame)
|
||||
}
|
||||
|
||||
// Generate text from a SemFrame in the specified language.
|
||||
fn generate_frame_lang(frame: [String], lang_code: String) -> String {
|
||||
return sem_realize_lang(frame, lang_code)
|
||||
}
|
||||
|
||||
// ── Public API: JSON ──────────────────────────────────────────────────────────
|
||||
|
||||
// Build a realizer slot map from JSON fields and an explicit lang code.
|
||||
fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String] {
|
||||
let intent: String = sem_get(semantic_form_json, "intent")
|
||||
let agent: String = sem_get(semantic_form_json, "agent")
|
||||
let predicate: String = sem_get(semantic_form_json, "predicate")
|
||||
let patient: String = sem_get(semantic_form_json, "patient")
|
||||
let location: String = sem_get(semantic_form_json, "location")
|
||||
let tense: String = sem_get(semantic_form_json, "tense")
|
||||
let aspect: String = sem_get(semantic_form_json, "aspect")
|
||||
|
||||
let form: [String] = native_list_empty()
|
||||
let form = native_list_append(form, "intent")
|
||||
let form = native_list_append(form, intent)
|
||||
let form = native_list_append(form, "agent")
|
||||
let form = native_list_append(form, agent)
|
||||
let form = native_list_append(form, "predicate")
|
||||
let form = native_list_append(form, predicate)
|
||||
let form = native_list_append(form, "patient")
|
||||
let form = native_list_append(form, patient)
|
||||
let form = native_list_append(form, "location")
|
||||
let form = native_list_append(form, location)
|
||||
let form = native_list_append(form, "tense")
|
||||
let form = native_list_append(form, tense)
|
||||
let form = native_list_append(form, "aspect")
|
||||
let form = native_list_append(form, aspect)
|
||||
let form = native_list_append(form, "lang")
|
||||
let form = native_list_append(form, lang_code)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// Generate text from a JSON semantic form. Language defaults to "en" unless
|
||||
// the JSON contains a "lang" field.
|
||||
fn generate(semantic_form_json: String) -> String {
|
||||
let lang_in_json: String = sem_get(semantic_form_json, "lang")
|
||||
let lang_code: String = lang_in_json
|
||||
if str_eq(lang_code, "") {
|
||||
let lang_code = "en"
|
||||
}
|
||||
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
|
||||
return realize(form)
|
||||
}
|
||||
|
||||
// Generate text from a JSON semantic form in the specified language.
|
||||
// lang_code overrides any "lang" field present in the JSON.
|
||||
fn generate_lang(semantic_form_json: String, lang_code: String) -> String {
|
||||
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
|
||||
return realize(form)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_get(json: String, key: String) -> String
|
||||
extern fn generate_frame(frame: [String]) -> String
|
||||
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
|
||||
extern fn generate(semantic_form_json: String) -> String
|
||||
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_a(x: String) -> String
|
||||
@@ -1,2 +0,0 @@
|
||||
import "language-profile.el"
|
||||
extern fn fn_b(x: String) -> String
|
||||
@@ -1,555 +0,0 @@
|
||||
// grammar.el - Grammar engine: syntactic structure, word order, phrase assembly.
|
||||
//
|
||||
// Language-specific word order and question strategy are driven by the language
|
||||
// profile, not hardcoded. The slot map format (GramSpec) is universal; a "lang"
|
||||
// key carries the ISO 639-1 code so every downstream function can resolve the
|
||||
// active profile.
|
||||
//
|
||||
// GramSpec slot keys:
|
||||
// intent - "assert" | "question" | "command"
|
||||
// agent - subject referent string
|
||||
// predicate - verb base form
|
||||
// patient - object noun phrase (optional)
|
||||
// location - prepositional phrase (optional)
|
||||
// tense - "present" | "past" | "future"
|
||||
// aspect - "simple" | "progressive" | "perfect"
|
||||
// lang - ISO 639-1 code (default "en")
|
||||
// verb_surf - conjugated verb surface form (computed)
|
||||
// aux_surf - auxiliary surface form (computed)
|
||||
//
|
||||
// Depends on: language-profile
|
||||
|
||||
// ── Slot map helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn slots_get(slots: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(slots)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(slots, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(slots, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn slots_set(slots: [String], key: String, val: String) -> [String] {
|
||||
let n: Int = native_list_len(slots)
|
||||
let result: [String] = native_list_empty()
|
||||
let found: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(slots, i)
|
||||
let v: String = native_list_get(slots, i + 1)
|
||||
if str_eq(k, key) {
|
||||
let result = native_list_append(result, k)
|
||||
let result = native_list_append(result, val)
|
||||
let found = true
|
||||
} else {
|
||||
let result = native_list_append(result, k)
|
||||
let result = native_list_append(result, v)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
if !found {
|
||||
let result = native_list_append(result, key)
|
||||
let result = native_list_append(result, val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn make_slots(k0: String, v0: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, k0)
|
||||
let r = native_list_append(r, v0)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] {
|
||||
let r: [String] = make_slots(k0, v0)
|
||||
let r = native_list_append(r, k1)
|
||||
let r = native_list_append(r, v1)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] {
|
||||
let r: [String] = make_slots2(k0, v0, k1, v1)
|
||||
let r = native_list_append(r, k2)
|
||||
let r = native_list_append(r, v2)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] {
|
||||
let r: [String] = make_slots3(k0, v0, k1, v1, k2, v2)
|
||||
let r = native_list_append(r, k3)
|
||||
let r = native_list_append(r, v3)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] {
|
||||
let r: [String] = make_slots4(k0, v0, k1, v1, k2, v2, k3, v3)
|
||||
let r = native_list_append(r, k4)
|
||||
let r = native_list_append(r, v4)
|
||||
return r
|
||||
}
|
||||
|
||||
// ── Grammar rule catalog ──────────────────────────────────────────────────────
|
||||
|
||||
fn rule_id(rule: [String]) -> String {
|
||||
return native_list_get(rule, 0)
|
||||
}
|
||||
|
||||
fn rule_lhs(rule: [String]) -> String {
|
||||
return native_list_get(rule, 1)
|
||||
}
|
||||
|
||||
fn rule_rhs_len(rule: [String]) -> Int {
|
||||
let n: Int = native_list_len(rule)
|
||||
return n - 2
|
||||
}
|
||||
|
||||
fn rule_rhs(rule: [String], idx: Int) -> String {
|
||||
return native_list_get(rule, idx + 2)
|
||||
}
|
||||
|
||||
fn make_rule(id: String, lhs: String, r0: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, id)
|
||||
let r = native_list_append(r, lhs)
|
||||
let r = native_list_append(r, r0)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] {
|
||||
let r: [String] = make_rule(id, lhs, r0)
|
||||
let r = native_list_append(r, r1)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] {
|
||||
let r: [String] = make_rule2(id, lhs, r0, r1)
|
||||
let r = native_list_append(r, r2)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] {
|
||||
let r: [String] = make_rule3(id, lhs, r0, r1, r2)
|
||||
let r = native_list_append(r, r3)
|
||||
return r
|
||||
}
|
||||
|
||||
fn build_rules() -> [[String]] {
|
||||
let rules: [[String]] = native_list_empty()
|
||||
|
||||
let rules = native_list_append(rules, make_rule2("S-DECL", "S", "NP", "VP"))
|
||||
let rules = native_list_append(rules, make_rule3("S-QUEST", "S", "Aux", "NP", "VP"))
|
||||
let rules = native_list_append(rules, make_rule("S-IMP", "S", "VP"))
|
||||
let rules = native_list_append(rules, make_rule2("NP-DET-N", "NP", "Det", "N"))
|
||||
let rules = native_list_append(rules, make_rule3("NP-DET-ADJ-N","NP", "Det", "Adj", "N"))
|
||||
let rules = native_list_append(rules, make_rule("NP-PRON", "NP", "Pron"))
|
||||
let rules = native_list_append(rules, make_rule("NP-N", "NP", "N"))
|
||||
let rules = native_list_append(rules, make_rule("VP-V", "VP", "V"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-V-NP", "VP", "V", "NP"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-V-PP", "VP", "V", "PP"))
|
||||
let rules = native_list_append(rules, make_rule3("VP-V-NP-PP", "VP", "V", "NP", "PP"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-AUX-V", "VP", "Aux", "V"))
|
||||
let rules = native_list_append(rules, make_rule3("VP-AUX-V-NP", "VP", "Aux", "V", "NP"))
|
||||
let rules = native_list_append(rules, make_rule2("PP-P-NP", "PP", "P", "NP"))
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
fn get_rules() -> [[String]] {
|
||||
return build_rules()
|
||||
}
|
||||
|
||||
fn find_rule(rule_id_str: String) -> [String] {
|
||||
let rules: [[String]] = get_rules()
|
||||
let n: Int = native_list_len(rules)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let rule: [String] = native_list_get(rules, i)
|
||||
let id: String = native_list_get(rule, 0)
|
||||
if str_eq(id, rule_id_str) {
|
||||
return rule
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let empty: [String] = native_list_empty()
|
||||
return empty
|
||||
}
|
||||
|
||||
// ── Tree node construction ────────────────────────────────────────────────────
|
||||
|
||||
fn make_leaf(label: String, word: String) -> String {
|
||||
return "(" + label + " " + word + ")"
|
||||
}
|
||||
|
||||
fn make_node1(label: String, child0: String) -> String {
|
||||
return "(" + label + " _ " + child0 + ")"
|
||||
}
|
||||
|
||||
fn make_node2(label: String, child0: String, child1: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + ")"
|
||||
}
|
||||
|
||||
fn make_node3(label: String, child0: String, child1: String, child2: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + ")"
|
||||
}
|
||||
|
||||
fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + " " + child3 + ")"
|
||||
}
|
||||
|
||||
// ── Tree rendering ────────────────────────────────────────────────────────────
|
||||
|
||||
fn nlg_is_ws(c: String) -> Bool {
|
||||
if str_eq(c, " ") { return true }
|
||||
if str_eq(c, "\t") { return true }
|
||||
if str_eq(c, "\n") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn skip_ws(s: String, pos: Int) -> Int {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = pos
|
||||
let running: Bool = true
|
||||
while running {
|
||||
if i >= n {
|
||||
let running = false
|
||||
} else {
|
||||
let c: String = str_slice(s, i, i + 1)
|
||||
if nlg_is_ws(c) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
fn scan_token(s: String, start: Int) -> [String] {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = start
|
||||
let running: Bool = true
|
||||
while running {
|
||||
if i >= n {
|
||||
let running = false
|
||||
} else {
|
||||
let c: String = str_slice(s, i, i + 1)
|
||||
if nlg_is_ws(c) {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(c, "(") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(c, ")") {
|
||||
let running = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tok: String = str_slice(s, start, i)
|
||||
let result: [String] = native_list_empty()
|
||||
let result = native_list_append(result, tok)
|
||||
let result = native_list_append(result, int_to_str(i))
|
||||
return result
|
||||
}
|
||||
|
||||
fn render_tree(tree: String) -> String {
|
||||
let words: [String] = native_list_empty()
|
||||
let n: Int = str_len(tree)
|
||||
let i: Int = 0
|
||||
let prev_was_open: Bool = false
|
||||
while i < n {
|
||||
let c: String = str_slice(tree, i, i + 1)
|
||||
if str_eq(c, "(") {
|
||||
let prev_was_open = true
|
||||
let i = i + 1
|
||||
} else {
|
||||
if str_eq(c, ")") {
|
||||
let prev_was_open = false
|
||||
let i = i + 1
|
||||
} else {
|
||||
if nlg_is_ws(c) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
let tok_info: [String] = scan_token(tree, i)
|
||||
let tok: String = native_list_get(tok_info, 0)
|
||||
let new_i: Int = str_to_int(native_list_get(tok_info, 1))
|
||||
let i = new_i
|
||||
if prev_was_open {
|
||||
let prev_was_open = false
|
||||
} else {
|
||||
if !str_eq(tok, "_") {
|
||||
let words = native_list_append(words, tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return str_join(words, " ")
|
||||
}
|
||||
|
||||
// ── Word-order engine ─────────────────────────────────────────────────────────
|
||||
|
||||
// gram_word_order: returns the word order string from a profile.
|
||||
fn gram_word_order(profile: [String]) -> String {
|
||||
return lang_word_order(profile)
|
||||
}
|
||||
|
||||
// gram_order_constituents: order Subject, Verb, Object tokens according to the
|
||||
// language profile's word_order.
|
||||
//
|
||||
// subj, verb, obj: surface strings (may be empty).
|
||||
// Returns a space-joined string in the correct order.
|
||||
//
|
||||
// Supported orders: SVO, SOV, VSO, VOS, OVS, OSV, free (defaults to SVO).
|
||||
|
||||
fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String {
|
||||
let order: String = gram_word_order(profile)
|
||||
let parts: [String] = native_list_empty()
|
||||
|
||||
if str_eq(order, "SVO") {
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
if str_eq(order, "SOV") {
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
if str_eq(order, "VSO") {
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
if str_eq(order, "VOS") {
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
if str_eq(order, "OVS") {
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
if str_eq(order, "OSV") {
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
// "free" and unknown: use SVO as the neutral citation order.
|
||||
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
|
||||
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
|
||||
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
|
||||
return str_join(parts, " ")
|
||||
}
|
||||
|
||||
// gram_build_vp: construct a verb phrase surface string.
|
||||
//
|
||||
// verb: main verb surface form.
|
||||
// aux: auxiliary surface form (empty if none).
|
||||
// profile: language profile.
|
||||
//
|
||||
// In SVO/VSO/VOS languages the auxiliary precedes the main verb.
|
||||
// In SOV languages the verb cluster appears at the end; we keep aux before V
|
||||
// as a reasonable default for the auxiliary-final constructions in those languages.
|
||||
|
||||
fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String {
|
||||
if str_eq(aux, "") {
|
||||
return verb
|
||||
}
|
||||
return aux + " " + verb
|
||||
}
|
||||
|
||||
// gram_question_strategy: returns the question formation strategy for a language.
|
||||
//
|
||||
// "do-support" - English: "Do you see?" — do-auxiliary inserted, verb stays base
|
||||
// "particle" - Japanese: sentence-final か appended
|
||||
// "intonation" - Mandarin, Spanish: rising intonation only, word order unchanged
|
||||
// "inversion" - French, German: subject-verb inversion
|
||||
|
||||
fn gram_question_strategy(profile: [String]) -> String {
|
||||
let code: String = lang_get(profile, "code")
|
||||
if str_eq(code, "en") { return "do-support" }
|
||||
if str_eq(code, "ja") { return "particle" }
|
||||
if str_eq(code, "zh") { return "intonation" }
|
||||
if str_eq(code, "es") { return "intonation" }
|
||||
if str_eq(code, "fr") { return "inversion" }
|
||||
if str_eq(code, "de") { return "inversion" }
|
||||
if str_eq(code, "ar") { return "intonation" }
|
||||
if str_eq(code, "hi") { return "particle" }
|
||||
if str_eq(code, "ru") { return "intonation" }
|
||||
if str_eq(code, "fi") { return "particle" }
|
||||
if str_eq(code, "sw") { return "intonation" }
|
||||
if str_eq(code, "la") { return "intonation" } // Latin: word order marks Q (VSO or -ne suffix)
|
||||
if str_eq(code, "he") { return "intonation" } // Modern Hebrew: rising intonation
|
||||
if str_eq(code, "grc") { return "intonation" } // Ancient Greek: ἆρα particle or intonation
|
||||
if str_eq(code, "ang") { return "intonation" } // Old English: hwæþer particle or intonation
|
||||
if str_eq(code, "sa") { return "intonation" } // Sanskrit: kim particle or intonation
|
||||
if str_eq(code, "got") { return "intonation" } // Gothic: ibai particle or intonation
|
||||
if str_eq(code, "non") { return "intonation" } // Old Norse: hvárr particle or intonation
|
||||
if str_eq(code, "enm") { return "do-support" } // Middle English: do-support emerging
|
||||
if str_eq(code, "pi") { return "intonation" } // Pali: kim particle or intonation
|
||||
// Unknown: default to intonation (safest — never wrong, just flat)
|
||||
return "intonation"
|
||||
}
|
||||
|
||||
// ── NP and PP assembly ────────────────────────────────────────────────────────
|
||||
//
|
||||
// These functions are profile-aware but the logic is the same across languages
|
||||
// because we work with pre-assembled strings (Engram vocabulary supplies
|
||||
// language-specific forms before these functions see them).
|
||||
|
||||
fn is_pronoun(word: String) -> Bool {
|
||||
if str_eq(word, "I") { return true }
|
||||
if str_eq(word, "you") { return true }
|
||||
if str_eq(word, "he") { return true }
|
||||
if str_eq(word, "she") { return true }
|
||||
if str_eq(word, "it") { return true }
|
||||
if str_eq(word, "we") { return true }
|
||||
if str_eq(word, "they") { return true }
|
||||
if str_eq(word, "me") { return true }
|
||||
if str_eq(word, "him") { return true }
|
||||
if str_eq(word, "her") { return true }
|
||||
if str_eq(word, "us") { return true }
|
||||
if str_eq(word, "them") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// build_np: assemble a noun phrase tree from a referent string.
|
||||
// profile parameter reserved for future case-marking / article agreement.
|
||||
fn build_np(referent: String, slots: [String]) -> String {
|
||||
if is_pronoun(referent) {
|
||||
return make_node1("NP", make_leaf("Pron", referent))
|
||||
}
|
||||
let parts: [String] = str_split(referent, " ")
|
||||
let np: Int = native_list_len(parts)
|
||||
if np == 1 {
|
||||
return make_node1("NP", make_leaf("N", referent))
|
||||
}
|
||||
if np == 2 {
|
||||
let det: String = native_list_get(parts, 0)
|
||||
let noun: String = native_list_get(parts, 1)
|
||||
return make_node2("NP", make_leaf("Det", det), make_leaf("N", noun))
|
||||
}
|
||||
if np == 3 {
|
||||
let det: String = native_list_get(parts, 0)
|
||||
let adj: String = native_list_get(parts, 1)
|
||||
let noun: String = native_list_get(parts, 2)
|
||||
return make_node3("NP", make_leaf("Det", det), make_leaf("Adj", adj), make_leaf("N", noun))
|
||||
}
|
||||
return make_node1("NP", make_leaf("N", referent))
|
||||
}
|
||||
|
||||
// build_pp: assemble a prepositional phrase tree from a "PREP NP" string.
|
||||
// For postpositional languages (ja, hi, ko) the slot value is expected to be
|
||||
// already pre-assembled with the postposition in the correct position by the
|
||||
// caller (vocabulary lookup from Engram supplies the right surface form).
|
||||
fn build_pp(loc: String) -> String {
|
||||
let parts: [String] = str_split(loc, " ")
|
||||
let n: Int = native_list_len(parts)
|
||||
if n < 2 {
|
||||
return make_leaf("PP", loc)
|
||||
}
|
||||
let prep: String = native_list_get(parts, 0)
|
||||
let np_parts: [String] = native_list_empty()
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
let np_parts = native_list_append(np_parts, native_list_get(parts, i))
|
||||
let i = i + 1
|
||||
}
|
||||
let np_str: String = str_join(np_parts, " ")
|
||||
let np_tree: String = build_np(np_str, native_list_empty())
|
||||
return make_node2("PP", make_leaf("P", prep), np_tree)
|
||||
}
|
||||
|
||||
// ── VP tree construction ──────────────────────────────────────────────────────
|
||||
|
||||
fn build_vp_body(slots: [String]) -> String {
|
||||
let verb_surf: String = slots_get(slots, "verb_surf")
|
||||
let patient: String = slots_get(slots, "patient")
|
||||
let loc: String = slots_get(slots, "location")
|
||||
if !str_eq(patient, "") {
|
||||
let obj_np: String = build_np(patient, slots)
|
||||
if !str_eq(loc, "") {
|
||||
let pp: String = build_pp(loc)
|
||||
return make_node3("VP", make_leaf("V", verb_surf), obj_np, pp)
|
||||
}
|
||||
return make_node2("VP", make_leaf("V", verb_surf), obj_np)
|
||||
}
|
||||
if !str_eq(loc, "") {
|
||||
let pp: String = build_pp(loc)
|
||||
return make_node2("VP", make_leaf("V", verb_surf), pp)
|
||||
}
|
||||
return make_node1("VP", make_leaf("V", verb_surf))
|
||||
}
|
||||
|
||||
fn build_vp_from_slots(slots: [String]) -> String {
|
||||
let aux_surf: String = slots_get(slots, "aux_surf")
|
||||
if !str_eq(aux_surf, "") {
|
||||
let verb_surf: String = slots_get(slots, "verb_surf")
|
||||
let patient: String = slots_get(slots, "patient")
|
||||
let loc: String = slots_get(slots, "location")
|
||||
if !str_eq(patient, "") {
|
||||
let obj_np: String = build_np(patient, slots)
|
||||
return make_node3("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf), obj_np)
|
||||
}
|
||||
return make_node2("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf))
|
||||
}
|
||||
return build_vp_body(slots)
|
||||
}
|
||||
|
||||
// ── Tree generator ────────────────────────────────────────────────────────────
|
||||
|
||||
fn generate_tree(rule_id_str: String, slots: [String]) -> String {
|
||||
let rule: [String] = find_rule(rule_id_str)
|
||||
let n: Int = native_list_len(rule)
|
||||
if n == 0 {
|
||||
return make_leaf("ERR", "unknown-rule")
|
||||
}
|
||||
|
||||
let lhs: String = native_list_get(rule, 1)
|
||||
|
||||
if str_eq(rule_id_str, "S-DECL") {
|
||||
let agent: String = slots_get(slots, "agent")
|
||||
let np_tree: String = build_np(agent, slots)
|
||||
let vp_tree: String = build_vp_from_slots(slots)
|
||||
return make_node2("S", np_tree, vp_tree)
|
||||
}
|
||||
|
||||
if str_eq(rule_id_str, "S-QUEST") {
|
||||
let agent: String = slots_get(slots, "agent")
|
||||
let np_tree: String = build_np(agent, slots)
|
||||
let vp_tree: String = build_vp_body(slots)
|
||||
let aux_surf: String = slots_get(slots, "aux_surf")
|
||||
return make_node3("S", make_leaf("Aux", aux_surf), np_tree, vp_tree)
|
||||
}
|
||||
|
||||
if str_eq(rule_id_str, "S-IMP") {
|
||||
let vp_tree: String = build_vp_from_slots(slots)
|
||||
return make_node1("S", vp_tree)
|
||||
}
|
||||
|
||||
return make_leaf(lhs, "?")
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn slots_get(slots: [String], key: String) -> String
|
||||
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
|
||||
extern fn make_slots(k0: String, v0: String) -> [String]
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
|
||||
extern fn rule_id(rule: [String]) -> String
|
||||
extern fn rule_lhs(rule: [String]) -> String
|
||||
extern fn rule_rhs_len(rule: [String]) -> Int
|
||||
extern fn rule_rhs(rule: [String], idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
|
||||
extern fn build_rules() -> [[String]]
|
||||
extern fn get_rules() -> [[String]]
|
||||
extern fn find_rule(rule_id_str: String) -> [String]
|
||||
extern fn make_leaf(label: String, word: String) -> String
|
||||
extern fn make_node1(label: String, child0: String) -> String
|
||||
extern fn make_node2(label: String, child0: String, child1: String) -> String
|
||||
extern fn make_node3(label: String, child0: String, child1: String, child2: String) -> String
|
||||
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
|
||||
extern fn nlg_is_ws(c: String) -> Bool
|
||||
extern fn skip_ws(s: String, pos: Int) -> Int
|
||||
extern fn scan_token(s: String, start: Int) -> [String]
|
||||
extern fn render_tree(tree: String) -> String
|
||||
extern fn gram_word_order(profile: [String]) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
|
||||
extern fn gram_question_strategy(profile: [String]) -> String
|
||||
extern fn is_pronoun(word: String) -> Bool
|
||||
extern fn build_np(referent: String, slots: [String]) -> String
|
||||
extern fn build_pp(loc: String) -> String
|
||||
extern fn build_vp_body(slots: [String]) -> String
|
||||
extern fn build_vp_from_slots(slots: [String]) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
|
||||
@@ -1,65 +0,0 @@
|
||||
// image-demo.el - Drive the native PNG surface: plan a scene from a small
|
||||
// meaning phrase (incl. a NEG frame) and emit a byte-valid 64x64 PNG whose
|
||||
// palette is read from elp/faculty/sig/scene.basis.
|
||||
|
||||
fn img_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
|
||||
let f: [String] = native_list_empty()
|
||||
let f: [String] = native_list_append(f, "relation")
|
||||
let f: [String] = native_list_append(f, relation)
|
||||
let f: [String] = native_list_append(f, "polarity")
|
||||
let f: [String] = native_list_append(f, polarity)
|
||||
let f: [String] = native_list_append(f, "confidence")
|
||||
let f: [String] = native_list_append(f, confidence)
|
||||
let f: [String] = native_list_append(f, "importance")
|
||||
let f: [String] = native_list_append(f, importance)
|
||||
let f: [String] = native_list_append(f, "salience")
|
||||
let f: [String] = native_list_append(f, salience)
|
||||
let f: [String] = native_list_append(f, "subj_id")
|
||||
let f: [String] = native_list_append(f, subj_id)
|
||||
return f
|
||||
}
|
||||
|
||||
fn rgb_str(c: [Int]) -> String {
|
||||
return int_to_str(native_list_get(c, 0)) + "," + int_to_str(native_list_get(c, 1)) + "," + int_to_str(native_list_get(c, 2))
|
||||
}
|
||||
|
||||
fn run_image() -> Int {
|
||||
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
|
||||
let table: [Int] = crc_table()
|
||||
println("crc_table[1]=" + int_to_str(native_list_get(table, 1)) + " (expect 1996959894 / 0x77073096)")
|
||||
|
||||
let basis: [String] = basis_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/scene.basis")
|
||||
let warm: [Int] = parse_rgb(basis_field(basis, "warm"))
|
||||
let cool: [Int] = parse_rgb(basis_field(basis, "cool"))
|
||||
let bg: [Int] = parse_rgb(basis_field(basis, "bg"))
|
||||
println("basis warm=" + rgb_str(warm) + " cool=" + rgb_str(cool) + " bg=" + rgb_str(bg) + " (read from scene.basis)")
|
||||
|
||||
let frames: [[String]] = native_list_empty()
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("theme", "aff", "0.7", "0.6", "1", "s2"))
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("cause", "aff", "0.8", "0.9", "0", "s3"))
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("negation", "neg", "0.85", "0.7", "1", "s4"))
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("goal", "aff", "0.6", "0.5", "0", "s5"))
|
||||
let frames: [[String]] = native_list_append(frames, img_frame("result", "aff", "0.95", "1.0", "1", "s6"))
|
||||
|
||||
let shapes: [[Int]] = plan_scene(frames, warm, cool)
|
||||
let ns: Int = native_list_len(shapes)
|
||||
println("planned " + int_to_str(ns) + " shapes:")
|
||||
let si: Int = 0
|
||||
while si < ns {
|
||||
let sh: [Int] = native_list_get(shapes, si)
|
||||
let pol: String = surface_get(native_list_get(frames, si), "polarity")
|
||||
println(" shape " + int_to_str(si) + " type=" + int_to_str(native_list_get(sh, 0)) + " x=" + int_to_str(native_list_get(sh, 1)) + " y=" + int_to_str(native_list_get(sh, 2)) + " size=" + int_to_str(native_list_get(sh, 3)) + " rgb=" + int_to_str(native_list_get(sh, 4)) + "," + int_to_str(native_list_get(sh, 5)) + "," + int_to_str(native_list_get(sh, 6)) + " polarity=" + pol)
|
||||
let si: Int = si + 1
|
||||
}
|
||||
|
||||
let raw: [Int] = rasterize(64, 64, shapes, bg)
|
||||
println("rasterized raw (filtered scanlines) bytes=" + int_to_str(native_list_len(raw)) + " (expect 12352)")
|
||||
let png: [Int] = png_build(64, 64, raw, table)
|
||||
let plen: Int = native_list_len(png)
|
||||
let ok: Int = png_write("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png", png)
|
||||
println("PNG bytes=" + int_to_str(plen) + " -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png (write_ok=" + int_to_str(ok) + ")")
|
||||
return plen
|
||||
}
|
||||
|
||||
println("image-demo returned png_bytes=" + int_to_str(run_image()))
|
||||
@@ -1,412 +0,0 @@
|
||||
// image-surface.el - Native own-core raster PNG surface (the image efferent
|
||||
// twin of audio). Renders a 64x64 RGB scene deterministically from a frame's
|
||||
// meaning-geometry, then serialises a byte-valid PNG entirely own-core:
|
||||
// 8-byte magic, IHDR, IDAT (zlib STORED/uncompressed DEFLATE + Adler32), IEND,
|
||||
// with a per-chunk CRC32 computed via software xor32 (EL has no bitwise ops).
|
||||
//
|
||||
// The RGB palette basis is read from elp/faculty/sig/scene.basis (data, not
|
||||
// literals) - the same read-from-learned discipline as the audio signatures.
|
||||
// Integer-only throughout; pixels are composed functionally (painter's order)
|
||||
// so no list mutation is needed.
|
||||
|
||||
// -- small int/parse helpers (self-contained) ----------------------------------
|
||||
|
||||
fn i_str_to_int(s: String) -> Int {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = 0
|
||||
let v: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c >= 48 {
|
||||
if c < 58 {
|
||||
let v: Int = v * 10 + (c - 48)
|
||||
}
|
||||
}
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
fn basis_load(path: String) -> [String] {
|
||||
return str_split(fs_read(path), "\n")
|
||||
}
|
||||
|
||||
fn basis_field(lines: [String], key: String) -> String {
|
||||
let pref: String = key + ": "
|
||||
let n: Int = native_list_len(lines)
|
||||
let plen: Int = str_len(pref)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ln: String = native_list_get(lines, i)
|
||||
if str_starts_with(ln, pref) {
|
||||
return str_slice(ln, plen, str_len(ln))
|
||||
}
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn parse_rgb(csv: String) -> [Int] {
|
||||
let parts: [String] = str_split(csv, ",")
|
||||
let out: [Int] = native_list_empty()
|
||||
let n: Int = native_list_len(parts)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: Int = i_str_to_int(native_list_get(parts, i))
|
||||
let out: [Int] = native_list_append(out, v)
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// -- software 32-bit XOR (no bitwise ops in EL) --------------------------------
|
||||
|
||||
fn xor32(a: Int, b: Int) -> Int {
|
||||
let r: Int = 0
|
||||
let bit: Int = 1
|
||||
let i: Int = 0
|
||||
while i < 32 {
|
||||
let abit: Int = (a / bit) % 2
|
||||
let bbit: Int = (b / bit) % 2
|
||||
if abit != bbit {
|
||||
let add: Int = bit
|
||||
let r: Int = r + add
|
||||
}
|
||||
let bit: Int = bit * 2
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// -- CRC32 (table-driven, table built with xor32) ------------------------------
|
||||
|
||||
fn crc_table() -> [Int] {
|
||||
let t: [Int] = native_list_empty()
|
||||
let n: Int = 0
|
||||
while n < 256 {
|
||||
let c: Int = n
|
||||
let k: Int = 0
|
||||
while k < 8 {
|
||||
if c % 2 == 1 {
|
||||
let h: Int = c / 2
|
||||
let c: Int = xor32(h, 3988292384)
|
||||
} else {
|
||||
let c: Int = c / 2
|
||||
}
|
||||
let k: Int = k + 1
|
||||
}
|
||||
let t: [Int] = native_list_append(t, c)
|
||||
let n: Int = n + 1
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
fn crc32_of(bytes: [Int], table: [Int]) -> Int {
|
||||
let crc: Int = 4294967295
|
||||
let n: Int = native_list_len(bytes)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let b: Int = native_list_get(bytes, i)
|
||||
let lo: Int = crc % 256
|
||||
let idx: Int = xor32(lo, b) % 256
|
||||
let tv: Int = native_list_get(table, idx)
|
||||
let hi: Int = crc / 256
|
||||
let crc: Int = xor32(hi, tv)
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return xor32(crc, 4294967295)
|
||||
}
|
||||
|
||||
// -- Adler32 (for the zlib trailer) --------------------------------------------
|
||||
|
||||
fn adler32_of(bytes: [Int]) -> Int {
|
||||
let a: Int = 1
|
||||
let b: Int = 0
|
||||
let n: Int = native_list_len(bytes)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let byte: Int = native_list_get(bytes, i)
|
||||
let a: Int = (a + byte) % 65521
|
||||
let b: Int = (b + a) % 65521
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return b * 65536 + a
|
||||
}
|
||||
|
||||
// -- byte-list append helpers --------------------------------------------------
|
||||
|
||||
fn app_u32be(dst: [Int], v: Int) -> [Int] {
|
||||
let dst: [Int] = native_list_append(dst, (v / 16777216) % 256)
|
||||
let dst: [Int] = native_list_append(dst, (v / 65536) % 256)
|
||||
let dst: [Int] = native_list_append(dst, (v / 256) % 256)
|
||||
let dst: [Int] = native_list_append(dst, v % 256)
|
||||
return dst
|
||||
}
|
||||
|
||||
fn app_tag(dst: [Int], s: String) -> [Int] {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let dst: [Int] = native_list_append(dst, str_char_code(s, i))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
fn app_all(dst: [Int], src: [Int]) -> [Int] {
|
||||
let n: Int = native_list_len(src)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let dst: [Int] = native_list_append(dst, native_list_get(src, i))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// -- plan: frame meaning-geometry -> shape atoms -------------------------------
|
||||
// shape = [type, x, y, size, r, g, b] (type 0=rect 1=disc 2=triangle)
|
||||
|
||||
fn charsum(s: String) -> Int {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = 0
|
||||
let acc: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
let acc: Int = acc + c
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
fn micro_of(s: String) -> Int {
|
||||
let dot: Int = str_index_of(s, ".")
|
||||
if dot < 0 { return i_str_to_int(s) * 1000000 }
|
||||
let n: Int = str_len(s)
|
||||
let fp: String = str_slice(s, dot + 1, n)
|
||||
let ip: String = str_slice(s, 0, dot)
|
||||
let iv: Int = i_str_to_int(ip)
|
||||
let fv: Int = 0
|
||||
let scale: Int = 100000
|
||||
let fl: Int = str_len(fp)
|
||||
let i: Int = 0
|
||||
while i < 6 {
|
||||
let d: Int = 0
|
||||
if i < fl { let d: Int = str_char_code(fp, i) - 48 }
|
||||
let fv: Int = fv + d * scale
|
||||
let scale: Int = scale / 10
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return iv * 1000000 + fv
|
||||
}
|
||||
|
||||
fn plan_scene(frames: [[String]], warm: [Int], cool: [Int]) -> [[Int]] {
|
||||
let shapes: [[Int]] = native_list_empty()
|
||||
let nf: Int = native_list_len(frames)
|
||||
let fi: Int = 0
|
||||
while fi < nf {
|
||||
let fr: [String] = native_list_get(frames, fi)
|
||||
let relation: String = surface_get(fr, "relation")
|
||||
let polarity: String = surface_get(fr, "polarity")
|
||||
let confidence: String = surface_get(fr, "confidence")
|
||||
let importance: String = surface_get(fr, "importance")
|
||||
let salience: String = surface_get(fr, "salience")
|
||||
// relation -> shape type
|
||||
let stype: Int = charsum(relation) % 3
|
||||
// confidence -> size (8..22)
|
||||
let cmi: Int = micro_of(confidence)
|
||||
let size: Int = 8 + cmi / 71428
|
||||
// salience -> y
|
||||
let sal: Int = i_str_to_int(salience)
|
||||
let y: Int = 6 + sal * 26
|
||||
// subj_id/index -> x
|
||||
let x: Int = 4 + (fi * 10) % 48
|
||||
// polarity -> warm/cool base color
|
||||
let br: Int = native_list_get(warm, 0)
|
||||
let bg2: Int = native_list_get(warm, 1)
|
||||
let bb: Int = native_list_get(warm, 2)
|
||||
if str_eq(polarity, "neg") {
|
||||
let br: Int = native_list_get(cool, 0)
|
||||
let bg2: Int = native_list_get(cool, 1)
|
||||
let bb: Int = native_list_get(cool, 2)
|
||||
}
|
||||
// importance -> brightness (500..1000 permille)
|
||||
let imi: Int = micro_of(importance)
|
||||
let bpm: Int = 500 + imi / 2000
|
||||
let r: Int = br * bpm / 1000
|
||||
let g: Int = bg2 * bpm / 1000
|
||||
let b: Int = bb * bpm / 1000
|
||||
let sh: [Int] = native_list_empty()
|
||||
let sh: [Int] = native_list_append(sh, stype)
|
||||
let sh: [Int] = native_list_append(sh, x)
|
||||
let sh: [Int] = native_list_append(sh, y)
|
||||
let sh: [Int] = native_list_append(sh, size)
|
||||
let sh: [Int] = native_list_append(sh, r)
|
||||
let sh: [Int] = native_list_append(sh, g)
|
||||
let sh: [Int] = native_list_append(sh, b)
|
||||
let shapes: [[Int]] = native_list_append(shapes, sh)
|
||||
let fi: Int = fi + 1
|
||||
}
|
||||
return shapes
|
||||
}
|
||||
|
||||
// covers: is (px,py) inside this shape?
|
||||
fn covers(sh: [Int], px: Int, py: Int) -> Bool {
|
||||
let stype: Int = native_list_get(sh, 0)
|
||||
let sx: Int = native_list_get(sh, 1)
|
||||
let sy: Int = native_list_get(sh, 2)
|
||||
let size: Int = native_list_get(sh, 3)
|
||||
let cx: Int = sx + size / 2
|
||||
if stype == 0 {
|
||||
if px >= sx {
|
||||
if px < sx + size {
|
||||
if py >= sy {
|
||||
if py < sy + size {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if stype == 1 {
|
||||
let rad: Int = size / 2
|
||||
let dx: Int = px - cx
|
||||
let dy: Int = py - (sy + rad)
|
||||
if dx * dx + dy * dy <= rad * rad {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// triangle: apex at top (sy), base at sy+size
|
||||
if py >= sy {
|
||||
if py < sy + size {
|
||||
let dyv: Int = py - sy
|
||||
let halfw: Int = dyv / 2
|
||||
let dxv: Int = px - cx
|
||||
let adx: Int = dxv
|
||||
if adx < 0 { let adx: Int = 0 - dxv }
|
||||
if adx <= halfw {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pixel_color: painter's algorithm - last covering shape wins. Returns [r,g,b].
|
||||
fn pixel_color(px: Int, py: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
|
||||
let r: Int = native_list_get(bg, 0)
|
||||
let g: Int = native_list_get(bg, 1)
|
||||
let b: Int = native_list_get(bg, 2)
|
||||
let n: Int = native_list_len(shapes)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let sh: [Int] = native_list_get(shapes, i)
|
||||
if covers(sh, px, py) {
|
||||
let r: Int = native_list_get(sh, 4)
|
||||
let g: Int = native_list_get(sh, 5)
|
||||
let b: Int = native_list_get(sh, 6)
|
||||
}
|
||||
let i: Int = i + 1
|
||||
}
|
||||
let out: [Int] = native_list_empty()
|
||||
let out: [Int] = native_list_append(out, r)
|
||||
let out: [Int] = native_list_append(out, g)
|
||||
let out: [Int] = native_list_append(out, b)
|
||||
return out
|
||||
}
|
||||
|
||||
// rasterize: build the raw (filtered) scanline byte stream, filter byte 0 / row.
|
||||
fn rasterize(w: Int, h: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
|
||||
let raw: [Int] = native_list_empty()
|
||||
let y: Int = 0
|
||||
while y < h {
|
||||
let raw: [Int] = native_list_append(raw, 0)
|
||||
let x: Int = 0
|
||||
while x < w {
|
||||
let col: [Int] = pixel_color(x, y, shapes, bg)
|
||||
let raw: [Int] = native_list_append(raw, native_list_get(col, 0))
|
||||
let raw: [Int] = native_list_append(raw, native_list_get(col, 1))
|
||||
let raw: [Int] = native_list_append(raw, native_list_get(col, 2))
|
||||
let x: Int = x + 1
|
||||
}
|
||||
let y: Int = y + 1
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// zlib stream with a single STORED (uncompressed) DEFLATE block + Adler32.
|
||||
fn zlib_store(raw: [Int]) -> [Int] {
|
||||
let z: [Int] = native_list_empty()
|
||||
let z: [Int] = native_list_append(z, 120)
|
||||
let z: [Int] = native_list_append(z, 1)
|
||||
let z: [Int] = native_list_append(z, 1)
|
||||
let len: Int = native_list_len(raw)
|
||||
let nlen: Int = 65535 - len
|
||||
let z: [Int] = native_list_append(z, len % 256)
|
||||
let z: [Int] = native_list_append(z, (len / 256) % 256)
|
||||
let z: [Int] = native_list_append(z, nlen % 256)
|
||||
let z: [Int] = native_list_append(z, (nlen / 256) % 256)
|
||||
let z: [Int] = app_all(z, raw)
|
||||
let ad: Int = adler32_of(raw)
|
||||
let z: [Int] = app_u32be(z, ad)
|
||||
return z
|
||||
}
|
||||
|
||||
// append a full PNG chunk: length + (type+data) + crc32(type+data).
|
||||
fn app_chunk(png: [Int], type_and_data: [Int], table: [Int]) -> [Int] {
|
||||
let total: Int = native_list_len(type_and_data)
|
||||
let dlen: Int = total - 4
|
||||
let png: [Int] = app_u32be(png, dlen)
|
||||
let png: [Int] = app_all(png, type_and_data)
|
||||
let crc: Int = crc32_of(type_and_data, table)
|
||||
let png: [Int] = app_u32be(png, crc)
|
||||
return png
|
||||
}
|
||||
|
||||
fn png_build(w: Int, h: Int, raw: [Int], table: [Int]) -> [Int] {
|
||||
let png: [Int] = native_list_empty()
|
||||
// 8-byte signature
|
||||
let png: [Int] = native_list_append(png, 137)
|
||||
let png: [Int] = native_list_append(png, 80)
|
||||
let png: [Int] = native_list_append(png, 78)
|
||||
let png: [Int] = native_list_append(png, 71)
|
||||
let png: [Int] = native_list_append(png, 13)
|
||||
let png: [Int] = native_list_append(png, 10)
|
||||
let png: [Int] = native_list_append(png, 26)
|
||||
let png: [Int] = native_list_append(png, 10)
|
||||
// IHDR
|
||||
let ihdr: [Int] = native_list_empty()
|
||||
let ihdr: [Int] = app_tag(ihdr, "IHDR")
|
||||
let ihdr: [Int] = app_u32be(ihdr, w)
|
||||
let ihdr: [Int] = app_u32be(ihdr, h)
|
||||
let ihdr: [Int] = native_list_append(ihdr, 8)
|
||||
let ihdr: [Int] = native_list_append(ihdr, 2)
|
||||
let ihdr: [Int] = native_list_append(ihdr, 0)
|
||||
let ihdr: [Int] = native_list_append(ihdr, 0)
|
||||
let ihdr: [Int] = native_list_append(ihdr, 0)
|
||||
let png: [Int] = app_chunk(png, ihdr, table)
|
||||
// IDAT
|
||||
let z: [Int] = zlib_store(raw)
|
||||
let idat: [Int] = native_list_empty()
|
||||
let idat: [Int] = app_tag(idat, "IDAT")
|
||||
let idat: [Int] = app_all(idat, z)
|
||||
let png: [Int] = app_chunk(png, idat, table)
|
||||
// IEND
|
||||
let iend: [Int] = native_list_empty()
|
||||
let iend: [Int] = app_tag(iend, "IEND")
|
||||
let png: [Int] = app_chunk(png, iend, table)
|
||||
return png
|
||||
}
|
||||
|
||||
fn png_write(path: String, png: [Int]) -> Int {
|
||||
let n: Int = native_list_len(png)
|
||||
let buf: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let buf: String = __str_set_char(buf, i, native_list_get(png, i))
|
||||
let i: Int = i + 1
|
||||
}
|
||||
let ok: Int = fs_write_bytes(path, buf, n)
|
||||
return ok
|
||||
}
|
||||
@@ -1,761 +0,0 @@
|
||||
// big language-profile for testing
|
||||
fn lang_profile_big0(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big0(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big0("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big1(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big1(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big1("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big2(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big2(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big2("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big3(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big3(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big3("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big4(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big4(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big4("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big5(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big5(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big5("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big6(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big6(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big6("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big7(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big7(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big7("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big8(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big8(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big8("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big9(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big9(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big9("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big10(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big10(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big10("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big11(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big11(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big11("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big12(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big12(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big12("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big13(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big13(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big13("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big14(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big14(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big14("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big15(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big15(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big15("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big16(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big16(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big16("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big17(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big17(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big17("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big18(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big18(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big18("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
fn lang_profile_big19(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
fn lang_get_big19(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile_big19("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
// language-profile.el - Language profile data and accessors.
|
||||
//
|
||||
// A language profile is a slot map ([String] key-value list) describing the
|
||||
// typological properties of a natural language. The engine reads these
|
||||
// properties to drive morphology, word-order, and question-formation without
|
||||
// any per-language code paths.
|
||||
//
|
||||
// Profile slot keys:
|
||||
// code - ISO 639-1 code: "en", "ja", "ar", "zh", "de", "fr", "es", "sw", "hi", "ru", etc.
|
||||
// word_order - "SVO" | "SOV" | "VSO" | "VOS" | "OVS" | "OSV" | "free"
|
||||
// morph_type - "isolating" | "agglutinative" | "fusional" | "polysynthetic"
|
||||
// has_case - "true" | "false"
|
||||
// has_gender - "true" | "false"
|
||||
// script_dir - "ltr" | "rtl" | "ttb"
|
||||
// agreement - semicolon-separated features: "number;person" | "number;person;gender;case" | "none"
|
||||
// null_subject - "true" | "false" (pro-drop: subject may be omitted)
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, "code")
|
||||
let r = native_list_append(r, code)
|
||||
let r = native_list_append(r, "word_order")
|
||||
let r = native_list_append(r, word_order)
|
||||
let r = native_list_append(r, "morph_type")
|
||||
let r = native_list_append(r, morph_type)
|
||||
let r = native_list_append(r, "has_case")
|
||||
let r = native_list_append(r, has_case)
|
||||
let r = native_list_append(r, "has_gender")
|
||||
let r = native_list_append(r, has_gender)
|
||||
let r = native_list_append(r, "script_dir")
|
||||
let r = native_list_append(r, script_dir)
|
||||
let r = native_list_append(r, "agreement")
|
||||
let r = native_list_append(r, agreement)
|
||||
let r = native_list_append(r, "null_subject")
|
||||
let r = native_list_append(r, null_subject)
|
||||
return r
|
||||
}
|
||||
|
||||
// ── Accessor ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn lang_get(profile: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(profile)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(profile, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(profile, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Built-in profiles ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Each profile encodes typological facts about one language. These are data,
|
||||
// not separate code paths. Adding a new language means adding a new profile
|
||||
// and loading its vocabulary/suffix tables into the Engram - no engine changes.
|
||||
|
||||
// English: SVO, fusional, no grammatical case (nominative/accusative collapsed),
|
||||
// no grammatical gender, left-to-right, agreement on number and person,
|
||||
// obligatory subject (no pro-drop).
|
||||
fn lang_profile_en() -> [String] {
|
||||
return lang_profile("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
// Japanese: SOV, agglutinative, grammatical relations marked by postpositions
|
||||
// (not inflectional case), no grammatical gender, left-to-right, no agreement
|
||||
// morphology on verbs, pro-drop (null subject frequent).
|
||||
fn lang_profile_ja() -> [String] {
|
||||
return lang_profile("ja", "SOV", "agglutinative", "false", "false", "ltr", "none", "true")
|
||||
}
|
||||
|
||||
// Arabic: VSO, fusional, full case system, grammatical gender (masc/fem),
|
||||
// right-to-left script, agreement on number, person, gender, and case,
|
||||
// pro-drop (subject agreement marking on verb allows subject omission).
|
||||
fn lang_profile_ar() -> [String] {
|
||||
return lang_profile("ar", "VSO", "fusional", "true", "true", "rtl", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Mandarin Chinese: SVO, isolating (no morphological inflection), no case,
|
||||
// no grammatical gender, left-to-right, no agreement (no morphological marking),
|
||||
// null subject allowed in discourse context.
|
||||
fn lang_profile_zh() -> [String] {
|
||||
return lang_profile("zh", "SVO", "isolating", "false", "false", "ltr", "none", "true")
|
||||
}
|
||||
|
||||
// German: V2 (second-position verb, base SOV in subordinate clauses), fusional,
|
||||
// four-case system, three grammatical genders, left-to-right, agreement on
|
||||
// number, person, gender, and case, obligatory subject.
|
||||
fn lang_profile_de() -> [String] {
|
||||
return lang_profile("de", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// Spanish: SVO, fusional, no morphological case (but object clitics exist),
|
||||
// grammatical gender (masc/fem), left-to-right, agreement on number, person,
|
||||
// and gender, pro-drop (rich verbal agreement allows subject omission).
|
||||
fn lang_profile_es() -> [String] {
|
||||
return lang_profile("es", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "true")
|
||||
}
|
||||
|
||||
// Finnish: SOV, agglutinative, fifteen grammatical cases, no grammatical gender,
|
||||
// left-to-right, agreement on number, person, and case, no pro-drop (subject
|
||||
// required in finite clauses).
|
||||
fn lang_profile_fi() -> [String] {
|
||||
return lang_profile("fi", "SOV", "agglutinative", "true", "false", "ltr", "number;person;case", "false")
|
||||
}
|
||||
|
||||
// Swahili: SVO, agglutinative, noun-class system (15+ classes replacing gender),
|
||||
// no case inflection, left-to-right, agreement driven by noun class and number,
|
||||
// pro-drop (subject prefix on verb can stand alone).
|
||||
fn lang_profile_sw() -> [String] {
|
||||
return lang_profile("sw", "SVO", "agglutinative", "false", "false", "ltr", "noun-class;number", "true")
|
||||
}
|
||||
|
||||
// Hindi: SOV, fusional, case-marked postpositional system, grammatical gender
|
||||
// (masc/fem), left-to-right (Devanagari script still ltr), agreement on number,
|
||||
// person, gender, and case, pro-drop (subject frequently dropped).
|
||||
fn lang_profile_hi() -> [String] {
|
||||
return lang_profile("hi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Russian: free word order (pragmatically determined), fusional, six-case system,
|
||||
// three grammatical genders, left-to-right (Cyrillic), agreement on number,
|
||||
// person, gender, and case, no pro-drop (subject required).
|
||||
fn lang_profile_ru() -> [String] {
|
||||
return lang_profile("ru", "free", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// French: SVO, fusional, no morphological case (but clitic object pronouns),
|
||||
// two grammatical genders (masc/fem), left-to-right, agreement on number,
|
||||
// person, and gender, no pro-drop.
|
||||
fn lang_profile_fr() -> [String] {
|
||||
return lang_profile("fr", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "false")
|
||||
}
|
||||
|
||||
// Latin: SOV (highly free word order), fusional, six-case system (nom/gen/dat/acc/abl/voc),
|
||||
// three genders (masc/fem/neut), left-to-right, rich agreement on number, person, gender,
|
||||
// and case, pro-drop (subject expressed in verb ending).
|
||||
fn lang_profile_la() -> [String] {
|
||||
return lang_profile("la", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Hebrew (Modern): SVO, Semitic trilateral root morphology, two genders (masc/fem),
|
||||
// two numbers (singular/plural; dual vestigial), right-to-left (Hebrew script),
|
||||
// agreement on number, person, gender; zero copula in present tense; no grammatical cases.
|
||||
fn lang_profile_he() -> [String] {
|
||||
return lang_profile("he", "SVO", "semitic", "true", "false", "rtl", "number;person;gender", "true")
|
||||
}
|
||||
|
||||
// Sanskrit: SOV/free, highly fusional, 3 genders, 8 cases, 3 numbers (sg/du/pl),
|
||||
// Devanagari script, rich verb system (10 classes, 9 tenses/moods), pro-drop.
|
||||
fn lang_profile_sa() -> [String] {
|
||||
return lang_profile("sa", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Gothic: SOV, fusional, 3 genders, 4 cases, singular/plural,
|
||||
// Gothic alphabet (romanized as þ/ƕ/ai/au/ei), strong and weak classes, pro-drop.
|
||||
fn lang_profile_got() -> [String] {
|
||||
return lang_profile("got", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Old Norse: free/SOV, fusional, 3 genders, 4 cases, singular/plural,
|
||||
// definite article as noun suffix (-inn/-in/-it), strong and weak classes, pro-drop.
|
||||
fn lang_profile_non() -> [String] {
|
||||
return lang_profile("non", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Middle English (ca. 1100–1500): SVO emerging, mostly lost case system,
|
||||
// -es plural/genitive, strong and weak verbs, no grammatical gender on nouns.
|
||||
fn lang_profile_enm() -> [String] {
|
||||
return lang_profile("enm", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
|
||||
}
|
||||
|
||||
// Pali: SOV, fusional (simplified Sanskrit), 3 genders, 8 cases, sg/pl,
|
||||
// Latin transliteration with IAST diacritics, Buddhist canonical language.
|
||||
fn lang_profile_pi() -> [String] {
|
||||
return lang_profile("pi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Ancient Greek: free/SOV word order, highly fusional, 3 genders, 5 cases (nom/acc/gen/dat/voc),
|
||||
// singular/dual/plural, polytonic Greek script (Unicode), complex verb system with aspect
|
||||
// (imperfective/perfective), augment in past tenses, pro-drop.
|
||||
fn lang_profile_grc() -> [String] {
|
||||
return lang_profile("grc", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case;aspect", "true")
|
||||
}
|
||||
|
||||
// Old English (Anglo-Saxon): SOV/V2, fusional, 3 genders, 4 cases (nom/acc/gen/dat),
|
||||
// singular/plural, Latin alphabet + þ/ð/ƿ/æ, strong and weak noun/verb classes, pro-drop.
|
||||
fn lang_profile_ang() -> [String] {
|
||||
return lang_profile("ang", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Old French (ca. 1000–1300 CE): SVO/V2, fusional, two-case system (nominative/oblique),
|
||||
// two genders (masculine/feminine), left-to-right, agreement on number, person, gender,
|
||||
// and case, no pro-drop (subject generally required).
|
||||
fn lang_profile_fro() -> [String] {
|
||||
return lang_profile("fro", "SVO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// Old High German (ca. 750–1050 CE): SOV/V2, fusional, four-case system, three genders,
|
||||
// left-to-right, agreement on number, person, gender, and case, pro-drop.
|
||||
fn lang_profile_goh() -> [String] {
|
||||
return lang_profile("goh", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Old Irish (ca. 600–900 CE): VSO, fusional, case system, three genders,
|
||||
// left-to-right, agreement on number, person, gender, and case, pro-drop.
|
||||
fn lang_profile_sga() -> [String] {
|
||||
return lang_profile("sga", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
|
||||
}
|
||||
|
||||
// Tocharian B (ca. 500–1000 CE): SOV, fusional, case system, two genders,
|
||||
// left-to-right, agreement on number, person, gender, and case, no pro-drop.
|
||||
fn lang_profile_txb() -> [String] {
|
||||
return lang_profile("txb", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// Old Persian (ca. 525–330 BCE): SOV, fusional, 8-case system, no grammatical gender,
|
||||
// left-to-right, agreement on number, person, and case, pro-drop.
|
||||
fn lang_profile_peo() -> [String] {
|
||||
return lang_profile("peo", "SOV", "fusional", "true", "false", "ltr", "number;person;case", "true")
|
||||
}
|
||||
|
||||
// Akkadian (Old Babylonian period, ca. 1900–1600 BCE): VSO, fusional, 3-case system
|
||||
// (nominative/accusative/genitive with mimation), two genders, left-to-right,
|
||||
// agreement on number, person, gender, and case, no pro-drop.
|
||||
fn lang_profile_akk() -> [String] {
|
||||
return lang_profile("akk", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// Ugaritic (ca. 1400–1200 BCE): VSO, Semitic trilateral root morphology, 3-case system,
|
||||
// two genders, left-to-right (cuneiform alphabetic script), agreement on number, person,
|
||||
// gender, and case, no pro-drop.
|
||||
fn lang_profile_uga() -> [String] {
|
||||
return lang_profile("uga", "VSO", "semitic", "true", "true", "ltr", "number;person;gender;case", "false")
|
||||
}
|
||||
|
||||
// Ancient Egyptian / Middle Egyptian (ca. 2100–1300 BCE): SVO, agglutinative,
|
||||
// no morphological case (word order + prepositions), two genders, left-to-right,
|
||||
// agreement on number, person, and gender, pro-drop (zero copula in present).
|
||||
fn lang_profile_egy() -> [String] {
|
||||
return lang_profile("egy", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "true")
|
||||
}
|
||||
|
||||
// Sumerian (ca. 3000–2000 BCE): SOV, agglutinative, ergative-absolutive case system,
|
||||
// no grammatical gender (animacy distinction instead), left-to-right, agreement on
|
||||
// number and person, pro-drop.
|
||||
fn lang_profile_sux() -> [String] {
|
||||
return lang_profile("sux", "SOV", "agglutinative", "true", "false", "ltr", "number;person", "true")
|
||||
}
|
||||
|
||||
// Ge'ez (Classical Ethiopic, ca. 4th–7th century CE): SOV, Semitic trilateral root
|
||||
// morphology, two genders (masc/fem), Ethiopic/Fidel script (ltr), agreement on
|
||||
// number, person, and gender, pro-drop (subject inflection on verb).
|
||||
fn lang_profile_gez() -> [String] {
|
||||
return lang_profile("gez", "SOV", "semitic", "true", "true", "ltr", "number;person;gender", "true")
|
||||
}
|
||||
|
||||
// Coptic (Sahidic dialect, ca. 3rd–11th century CE): SVO, agglutinative, no
|
||||
// morphological case, two genders (masc/fem), left-to-right (Coptic alphabet),
|
||||
// agreement on number and gender via bound subject pronouns, no pro-drop (explicit
|
||||
// subject prefix required on every verb).
|
||||
fn lang_profile_cop() -> [String] {
|
||||
return lang_profile("cop", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "false")
|
||||
}
|
||||
|
||||
// ── Dispatch: code -> profile ─────────────────────────────────────────────────
|
||||
|
||||
fn lang_from_code(code: String) -> [String] {
|
||||
if str_eq(code, "en") { return lang_profile_en() }
|
||||
if str_eq(code, "ja") { return lang_profile_ja() }
|
||||
if str_eq(code, "ar") { return lang_profile_ar() }
|
||||
if str_eq(code, "zh") { return lang_profile_zh() }
|
||||
if str_eq(code, "de") { return lang_profile_de() }
|
||||
if str_eq(code, "es") { return lang_profile_es() }
|
||||
if str_eq(code, "fi") { return lang_profile_fi() }
|
||||
if str_eq(code, "sw") { return lang_profile_sw() }
|
||||
if str_eq(code, "hi") { return lang_profile_hi() }
|
||||
if str_eq(code, "ru") { return lang_profile_ru() }
|
||||
if str_eq(code, "fr") { return lang_profile_fr() }
|
||||
if str_eq(code, "la") { return lang_profile_la() }
|
||||
if str_eq(code, "he") { return lang_profile_he() }
|
||||
if str_eq(code, "grc") { return lang_profile_grc() }
|
||||
if str_eq(code, "ang") { return lang_profile_ang() }
|
||||
if str_eq(code, "sa") { return lang_profile_sa() }
|
||||
if str_eq(code, "got") { return lang_profile_got() }
|
||||
if str_eq(code, "non") { return lang_profile_non() }
|
||||
if str_eq(code, "enm") { return lang_profile_enm() }
|
||||
if str_eq(code, "pi") { return lang_profile_pi() }
|
||||
if str_eq(code, "fro") { return lang_profile_fro() }
|
||||
if str_eq(code, "goh") { return lang_profile_goh() }
|
||||
if str_eq(code, "sga") { return lang_profile_sga() }
|
||||
if str_eq(code, "txb") { return lang_profile_txb() }
|
||||
if str_eq(code, "peo") { return lang_profile_peo() }
|
||||
if str_eq(code, "akk") { return lang_profile_akk() }
|
||||
if str_eq(code, "uga") { return lang_profile_uga() }
|
||||
if str_eq(code, "egy") { return lang_profile_egy() }
|
||||
if str_eq(code, "sux") { return lang_profile_sux() }
|
||||
if str_eq(code, "gez") { return lang_profile_gez() }
|
||||
if str_eq(code, "cop") { return lang_profile_cop() }
|
||||
// Unknown code: fall back to English profile
|
||||
return lang_profile_en()
|
||||
}
|
||||
|
||||
// English default - backward compatibility entry point.
|
||||
fn lang_default() -> [String] {
|
||||
return lang_profile_en()
|
||||
}
|
||||
|
||||
// ── Typed convenience predicates ──────────────────────────────────────────────
|
||||
|
||||
fn lang_is_isolating(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "morph_type"), "isolating")
|
||||
}
|
||||
|
||||
fn lang_is_agglutinative(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "morph_type"), "agglutinative")
|
||||
}
|
||||
|
||||
fn lang_is_fusional(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "morph_type"), "fusional")
|
||||
}
|
||||
|
||||
fn lang_is_polysynthetic(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "morph_type"), "polysynthetic")
|
||||
}
|
||||
|
||||
fn lang_is_rtl(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "script_dir"), "rtl")
|
||||
}
|
||||
|
||||
fn lang_has_null_subject(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "null_subject"), "true")
|
||||
}
|
||||
|
||||
fn lang_has_case(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "has_case"), "true")
|
||||
}
|
||||
|
||||
fn lang_has_gender(profile: [String]) -> Bool {
|
||||
return str_eq(lang_get(profile, "has_gender"), "true")
|
||||
}
|
||||
|
||||
fn lang_word_order(profile: [String]) -> String {
|
||||
return lang_get(profile, "word_order")
|
||||
}
|
||||
|
||||
fn lang_code(profile: [String]) -> String {
|
||||
return lang_get(profile, "code")
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
|
||||
extern fn lang_get(profile: [String], key: String) -> String
|
||||
extern fn lang_profile_en() -> [String]
|
||||
extern fn lang_profile_ja() -> [String]
|
||||
extern fn lang_profile_ar() -> [String]
|
||||
extern fn lang_profile_zh() -> [String]
|
||||
extern fn lang_profile_de() -> [String]
|
||||
extern fn lang_profile_es() -> [String]
|
||||
extern fn lang_profile_fi() -> [String]
|
||||
extern fn lang_profile_sw() -> [String]
|
||||
extern fn lang_profile_hi() -> [String]
|
||||
extern fn lang_profile_ru() -> [String]
|
||||
extern fn lang_profile_fr() -> [String]
|
||||
extern fn lang_profile_la() -> [String]
|
||||
extern fn lang_profile_he() -> [String]
|
||||
extern fn lang_profile_sa() -> [String]
|
||||
extern fn lang_profile_got() -> [String]
|
||||
extern fn lang_profile_non() -> [String]
|
||||
extern fn lang_profile_enm() -> [String]
|
||||
extern fn lang_profile_pi() -> [String]
|
||||
extern fn lang_profile_grc() -> [String]
|
||||
extern fn lang_profile_ang() -> [String]
|
||||
extern fn lang_profile_fro() -> [String]
|
||||
extern fn lang_profile_goh() -> [String]
|
||||
extern fn lang_profile_sga() -> [String]
|
||||
extern fn lang_profile_txb() -> [String]
|
||||
extern fn lang_profile_peo() -> [String]
|
||||
extern fn lang_profile_akk() -> [String]
|
||||
extern fn lang_profile_uga() -> [String]
|
||||
extern fn lang_profile_egy() -> [String]
|
||||
extern fn lang_profile_sux() -> [String]
|
||||
extern fn lang_profile_gez() -> [String]
|
||||
extern fn lang_profile_cop() -> [String]
|
||||
extern fn lang_from_code(code: String) -> [String]
|
||||
extern fn lang_default() -> [String]
|
||||
extern fn lang_is_isolating(profile: [String]) -> Bool
|
||||
extern fn lang_is_agglutinative(profile: [String]) -> Bool
|
||||
extern fn lang_is_fusional(profile: [String]) -> Bool
|
||||
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
|
||||
extern fn lang_is_rtl(profile: [String]) -> Bool
|
||||
extern fn lang_has_null_subject(profile: [String]) -> Bool
|
||||
extern fn lang_has_case(profile: [String]) -> Bool
|
||||
extern fn lang_has_gender(profile: [String]) -> Bool
|
||||
extern fn lang_word_order(profile: [String]) -> String
|
||||
extern fn lang_code(profile: [String]) -> String
|
||||
@@ -1,40 +0,0 @@
|
||||
import "language-profile.el"
|
||||
|
||||
extern fn es_pluralize(noun: String) -> String
|
||||
extern fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn fr_pluralize(noun: String) -> String
|
||||
extern fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn de_noun_plural(noun: String, gender: String) -> String
|
||||
extern fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
|
||||
extern fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String
|
||||
extern fn ja_conjugate(dict_form: String, form: String) -> String
|
||||
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
|
||||
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn ar_sound_plural(noun: String, gender: String) -> String
|
||||
extern fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
|
||||
extern fn hi_noun_direct(noun: String, gender: String, number: String) -> String
|
||||
extern fn hi_gender(noun: String) -> String
|
||||
extern fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
|
||||
extern fn sw_noun_plural(noun: String) -> String
|
||||
extern fn sw_conjugate(verb: String, person: String, number: String, noun_class: String, tense: String) -> String
|
||||
extern fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
|
||||
extern fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
@@ -1,3 +0,0 @@
|
||||
fn morph_tiny(x: String) -> String {
|
||||
return x
|
||||
}
|
||||
@@ -1,528 +0,0 @@
|
||||
// morphology-akk.el - Akkadian morphology for the NLG engine.
|
||||
// 𒀭𒂗𒍪 — Akkadian (akkadû), the language of Babylon and Assyria.
|
||||
//
|
||||
// Implements Old Babylonian Akkadian verb conjugation (G-stem / Grundstamm),
|
||||
// noun declension with mimation, and noun-phrase construction.
|
||||
//
|
||||
// Akkadian is the oldest attested Semitic language (ca. 2800–100 BCE).
|
||||
// It uses cuneiform script; we work in standard Latin transliteration
|
||||
// (Old Babylonian dialect — the classical prestige form).
|
||||
//
|
||||
// Language profile:
|
||||
// code=akk, name=Akkadian, morph_type=semitic, word_order=VSO/SOV,
|
||||
// script=cuneiform (transliterated), family=semitic/east-semitic
|
||||
//
|
||||
// Key grammatical facts:
|
||||
// - Semitic trilateral root system: words built from 3-consonant roots
|
||||
// by inserting vowel patterns (e.g. root p-r-s → iparras "he decides")
|
||||
// - Grammatical gender: masculine / feminine (no neuter)
|
||||
// - Cases: nominative (-um), accusative (-am), genitive (-im) — "mimation"
|
||||
// - Number: singular / plural (dual is vestigial in verbs)
|
||||
// - Verb stems: G (basic), D (intensive), Š (causative), N (passive);
|
||||
// this file implements G-stem throughout
|
||||
// - Two main tense/aspect systems:
|
||||
// Present-future (iparras pattern): action in progress or future
|
||||
// Perfect (iptaras pattern): completed action with present relevance
|
||||
// Stative (paris pattern): resultant state, often adjectival
|
||||
// - No definite or indefinite article; case endings convey
|
||||
// determination contextually
|
||||
// - Copula: bašû (to exist/be)
|
||||
//
|
||||
// Verb conjugation conventions:
|
||||
// person: "first" | "second" | "third"
|
||||
// gender: "m" | "f"
|
||||
// number: "singular" | "plural"
|
||||
// tense: "present" | "perfect" | "stative"
|
||||
//
|
||||
// Noun declension conventions:
|
||||
// gram_case: "nom" | "acc" | "gen"
|
||||
// number: "singular" | "plural"
|
||||
// gender: "m" | "f" (passed to akk_decline for gender-specific forms)
|
||||
//
|
||||
// Verbs covered (G-stem infinitive, transliterated):
|
||||
// "bašû" — to exist / be (copula)
|
||||
// "alāku" — to go
|
||||
// "amāru" — to see
|
||||
// "qabû" — to say
|
||||
// "epēšu" — to do / make
|
||||
//
|
||||
// Nouns covered with known mimation forms:
|
||||
// "šarrum" — king
|
||||
// "awīlum" — man / person
|
||||
// "bītum" — house
|
||||
// "ilum" — god
|
||||
//
|
||||
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn akk_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
fn akk_str_len(s: String) -> Int {
|
||||
return str_len(s)
|
||||
}
|
||||
|
||||
fn akk_str_drop_last(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len {
|
||||
return ""
|
||||
}
|
||||
return str_slice(s, 0, len - n)
|
||||
}
|
||||
|
||||
// ── Slot index ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Maps person × number to a 0-based slot for table lookups.
|
||||
// Akkadian verb agreement does not distinguish gender in 1st person,
|
||||
// and the 2nd person often conflates masc/fem in some paradigms.
|
||||
// We use a 6-cell paradigm matching the most common OB presentation:
|
||||
//
|
||||
// 0 = 1sg (I)
|
||||
// 1 = 2sg (you sg)
|
||||
// 2 = 3sg m (he)
|
||||
// 3 = 3sg f (she)
|
||||
// 4 = 1pl (we)
|
||||
// 5 = 3pl (they)
|
||||
//
|
||||
// Note: 2pl is rare / vestigial in attested OB texts; omitted here.
|
||||
|
||||
fn akk_slot(person: String, number: String) -> Int {
|
||||
if str_eq(person, "first") {
|
||||
if str_eq(number, "plural") { return 4 }
|
||||
return 0
|
||||
}
|
||||
if str_eq(person, "second") {
|
||||
return 1
|
||||
}
|
||||
// third
|
||||
if str_eq(number, "plural") { return 5 }
|
||||
return 2 // default: 3sg masc; caller may override with gender check below
|
||||
}
|
||||
|
||||
// akk_slot_g: gender-aware slot for third person singular.
|
||||
// Returns 3 (3sg fem) when person=third, number=singular, gender=f.
|
||||
fn akk_slot_g(person: String, gender: String, number: String) -> Int {
|
||||
let base: Int = akk_slot(person, number)
|
||||
if str_eq(person, "third") {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gender, "f") { return 3 }
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// ── Copula: bašû — to exist / be ──────────────────────────────────────────────
|
||||
//
|
||||
// bašû is suppletive and highly irregular.
|
||||
// Present: ibašši (3sg m/f), abašši (1sg), tabašši (2sg)
|
||||
// Stative: bašī (3sg m), bašiat (3sg f), bašāku (1sg)
|
||||
// Perfect: not commonly attested in G-stem; use present forms as fallback.
|
||||
|
||||
fn akk_copula_present(slot: Int) -> String {
|
||||
if slot == 0 { return "abašši" } // 1sg
|
||||
if slot == 1 { return "tabašši" } // 2sg
|
||||
if slot == 2 { return "ibašši" } // 3sg m
|
||||
if slot == 3 { return "ibašši" } // 3sg f (same form in attested OB)
|
||||
if slot == 4 { return "nibašši" } // 1pl
|
||||
return "ibaššū" // 3pl
|
||||
}
|
||||
|
||||
fn akk_copula_stative(slot: Int) -> String {
|
||||
if slot == 0 { return "bašāku" } // 1sg (stative 1sg: -āku suffix)
|
||||
if slot == 1 { return "bašāta" } // 2sg (-āta suffix)
|
||||
if slot == 2 { return "bašī" } // 3sg m (unmarked base)
|
||||
if slot == 3 { return "bašiat" } // 3sg f (-at suffix)
|
||||
if slot == 4 { return "bašānu" } // 1pl (-ānu suffix)
|
||||
return "bašū" // 3pl (-ū suffix)
|
||||
}
|
||||
|
||||
fn akk_is_copula(verb: String) -> Bool {
|
||||
if str_eq(verb, "bašû") { return true }
|
||||
if str_eq(verb, "bashu") { return true }
|
||||
if str_eq(verb, "be") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn akk_conjugate_copula(tense: String, slot: Int) -> String {
|
||||
if str_eq(tense, "stative") { return akk_copula_stative(slot) }
|
||||
// present and perfect both fall back to present forms for bašû
|
||||
return akk_copula_present(slot)
|
||||
}
|
||||
|
||||
// ── alāku — to go ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Irregular: present stem is illak- (not the expected alakk-).
|
||||
// Present: illak (3sg), allak (1sg), tallak (2sg), nillak (1pl), illaku (3pl)
|
||||
// Perfect: ittalk- forms (less common, use illak- + perf marker)
|
||||
// Stative: use present as proxy
|
||||
|
||||
fn akk_alaku_present(slot: Int) -> String {
|
||||
if slot == 0 { return "allak" } // 1sg
|
||||
if slot == 1 { return "tallak" } // 2sg
|
||||
if slot == 2 { return "illak" } // 3sg m
|
||||
if slot == 3 { return "tallak" } // 3sg f (same as 2sg — OB pattern)
|
||||
if slot == 4 { return "nillak" } // 1pl
|
||||
return "illaku" // 3pl
|
||||
}
|
||||
|
||||
fn akk_alaku_perfect(slot: Int) -> String {
|
||||
if slot == 0 { return "ittalak" } // 1sg
|
||||
if slot == 1 { return "tattalak" } // 2sg
|
||||
if slot == 2 { return "ittalak" } // 3sg m
|
||||
if slot == 3 { return "tattalak" } // 3sg f
|
||||
if slot == 4 { return "nittalak" } // 1pl
|
||||
return "ittalku" // 3pl
|
||||
}
|
||||
|
||||
// ── amāru — to see ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Present (immar-): immar (3sg), ammar (1sg), tammar (2sg)
|
||||
// Perfect (imtamar-): imtamar (3sg), amtamar (1sg), tamtamar (2sg)
|
||||
|
||||
fn akk_amaru_present(slot: Int) -> String {
|
||||
if slot == 0 { return "ammar" } // 1sg
|
||||
if slot == 1 { return "tammar" } // 2sg
|
||||
if slot == 2 { return "immar" } // 3sg m
|
||||
if slot == 3 { return "tammar" } // 3sg f
|
||||
if slot == 4 { return "nimmar" } // 1pl
|
||||
return "immaru" // 3pl
|
||||
}
|
||||
|
||||
fn akk_amaru_perfect(slot: Int) -> String {
|
||||
if slot == 0 { return "amtamar" } // 1sg
|
||||
if slot == 1 { return "tamtamar" } // 2sg
|
||||
if slot == 2 { return "imtamar" } // 3sg m
|
||||
if slot == 3 { return "tamtamar" } // 3sg f
|
||||
if slot == 4 { return "nimtamar" } // 1pl
|
||||
return "imtamaru" // 3pl
|
||||
}
|
||||
|
||||
fn akk_amaru_stative(slot: Int) -> String {
|
||||
// amāru stative: 3sg "amir" (the one who saw / he has seen)
|
||||
if slot == 0 { return "amrāku" }
|
||||
if slot == 1 { return "amrāta" }
|
||||
if slot == 2 { return "amir" }
|
||||
if slot == 3 { return "amrat" }
|
||||
if slot == 4 { return "amrānu" }
|
||||
return "amrū"
|
||||
}
|
||||
|
||||
// ── qabû — to say / speak ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Present: iqabbi (3sg), aqabbi (1sg), taqabbi (2sg)
|
||||
// Perfect: iqtabi (3sg), aqtabi (1sg), taqtabi (2sg)
|
||||
|
||||
fn akk_qabu_present(slot: Int) -> String {
|
||||
if slot == 0 { return "aqabbi" } // 1sg
|
||||
if slot == 1 { return "taqabbi" } // 2sg
|
||||
if slot == 2 { return "iqabbi" } // 3sg m
|
||||
if slot == 3 { return "taqabbi" } // 3sg f
|
||||
if slot == 4 { return "niqabbi" } // 1pl
|
||||
return "iqabbû" // 3pl
|
||||
}
|
||||
|
||||
fn akk_qabu_perfect(slot: Int) -> String {
|
||||
if slot == 0 { return "aqtabi" } // 1sg
|
||||
if slot == 1 { return "taqtabi" } // 2sg
|
||||
if slot == 2 { return "iqtabi" } // 3sg m
|
||||
if slot == 3 { return "taqtabi" } // 3sg f
|
||||
if slot == 4 { return "niqtabi" } // 1pl
|
||||
return "iqtabû" // 3pl
|
||||
}
|
||||
|
||||
fn akk_qabu_stative(slot: Int) -> String {
|
||||
if slot == 0 { return "qabāku" }
|
||||
if slot == 1 { return "qabāta" }
|
||||
if slot == 2 { return "qabi" }
|
||||
if slot == 3 { return "qabiat" }
|
||||
if slot == 4 { return "qabānu" }
|
||||
return "qabû"
|
||||
}
|
||||
|
||||
// ── epēšu — to do / make ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Present (ieppuš / eppuš): ieppuš (3sg), eppuš (1sg), teppuš (2sg)
|
||||
// Perfect: iptešu forms
|
||||
|
||||
fn akk_epesu_present(slot: Int) -> String {
|
||||
if slot == 0 { return "eppuš" } // 1sg
|
||||
if slot == 1 { return "teppuš" } // 2sg
|
||||
if slot == 2 { return "ieppuš" } // 3sg m
|
||||
if slot == 3 { return "teppuš" } // 3sg f
|
||||
if slot == 4 { return "neppuš" } // 1pl
|
||||
return "ieppušu" // 3pl
|
||||
}
|
||||
|
||||
fn akk_epesu_perfect(slot: Int) -> String {
|
||||
if slot == 0 { return "iptešu" } // 1sg (irregular: root ʿ-p-š)
|
||||
if slot == 1 { return "taptešu" } // 2sg
|
||||
if slot == 2 { return "iptešu" } // 3sg m
|
||||
if slot == 3 { return "taptešu" } // 3sg f
|
||||
if slot == 4 { return "niptešu" } // 1pl
|
||||
return "iptešū" // 3pl
|
||||
}
|
||||
|
||||
fn akk_epesu_stative(slot: Int) -> String {
|
||||
if slot == 0 { return "epšāku" }
|
||||
if slot == 1 { return "epšāta" }
|
||||
if slot == 2 { return "epuš" }
|
||||
if slot == 3 { return "epšat" }
|
||||
if slot == 4 { return "epšānu" }
|
||||
return "epšū"
|
||||
}
|
||||
|
||||
// ── Regular G-stem paradigms (iparras model) ──────────────────────────────────
|
||||
//
|
||||
// For regular verbs not in the irregular table, we apply the standard
|
||||
// OB G-stem paradigm using a caller-supplied present stem and perfect stem.
|
||||
// The stems must be pre-computed by the caller (or vocabulary layer).
|
||||
//
|
||||
// iparras (present) endings by slot:
|
||||
// 1sg: a- prefix
|
||||
// 2sg: ta- prefix
|
||||
// 3sg m: i- prefix
|
||||
// 3sg f: ta- prefix (same prefix as 2sg)
|
||||
// 1pl: ni- prefix
|
||||
// 3pl: i- prefix + -ū suffix
|
||||
//
|
||||
// For the generic fallback we use "iparras" as the model template.
|
||||
|
||||
fn akk_regular_present(stem: String, slot: Int) -> String {
|
||||
// stem is the 3sg m form (i-prefix already present in conventional citation)
|
||||
// We rebuild from the bare root portion by stripping/adding prefixes.
|
||||
// Simplification: return prefixed forms using the provided present-3sg string.
|
||||
if slot == 0 { return "a" + stem } // 1sg: a + stem (strip i-, add a-)
|
||||
if slot == 1 { return "ta" + stem } // 2sg
|
||||
if slot == 2 { return "i" + stem } // 3sg m
|
||||
if slot == 3 { return "ta" + stem } // 3sg f
|
||||
if slot == 4 { return "ni" + stem } // 1pl
|
||||
return "i" + stem + "u" // 3pl: i + stem + -ū
|
||||
}
|
||||
|
||||
fn akk_regular_perfect(stem: String, slot: Int) -> String {
|
||||
// Perfect (iptaras) — uses infix -ta- after first root consonant.
|
||||
// stem here is the 3sg perfect form; we apply person endings.
|
||||
if slot == 0 { return "a" + stem } // 1sg
|
||||
if slot == 1 { return "ta" + stem } // 2sg
|
||||
if slot == 2 { return "i" + stem } // 3sg m
|
||||
if slot == 3 { return "ta" + stem } // 3sg f
|
||||
if slot == 4 { return "ni" + stem } // 1pl
|
||||
return "i" + stem + "u" // 3pl
|
||||
}
|
||||
|
||||
fn akk_regular_stative(stem: String, slot: Int) -> String {
|
||||
// Stative (paris): 3sg m has zero ending; others take person suffixes.
|
||||
if slot == 0 { return stem + "āku" } // 1sg
|
||||
if slot == 1 { return stem + "āta" } // 2sg
|
||||
if slot == 2 { return stem } // 3sg m: bare stem
|
||||
if slot == 3 { return stem + "at" } // 3sg f
|
||||
if slot == 4 { return stem + "ānu" } // 1pl
|
||||
return stem + "ū" // 3pl
|
||||
}
|
||||
|
||||
// ── Known-verb dispatcher ─────────────────────────────────────────────────────
|
||||
|
||||
fn akk_known_verb(verb: String, tense: String, slot: Int) -> String {
|
||||
// bašû — to be / exist
|
||||
if str_eq(verb, "bašû") {
|
||||
return akk_conjugate_copula(tense, slot)
|
||||
}
|
||||
if str_eq(verb, "bashu") {
|
||||
return akk_conjugate_copula(tense, slot)
|
||||
}
|
||||
|
||||
// alāku — to go
|
||||
if str_eq(verb, "alāku") {
|
||||
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_alaku_present(slot) }
|
||||
return akk_alaku_present(slot)
|
||||
}
|
||||
if str_eq(verb, "alaku") {
|
||||
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
|
||||
return akk_alaku_present(slot)
|
||||
}
|
||||
|
||||
// amāru — to see
|
||||
if str_eq(verb, "amāru") {
|
||||
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
|
||||
return akk_amaru_present(slot)
|
||||
}
|
||||
if str_eq(verb, "amaru") {
|
||||
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
|
||||
return akk_amaru_present(slot)
|
||||
}
|
||||
|
||||
// qabû — to say
|
||||
if str_eq(verb, "qabû") {
|
||||
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
|
||||
return akk_qabu_present(slot)
|
||||
}
|
||||
if str_eq(verb, "qabu") {
|
||||
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
|
||||
return akk_qabu_present(slot)
|
||||
}
|
||||
|
||||
// epēšu — to do / make
|
||||
if str_eq(verb, "epēšu") {
|
||||
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
|
||||
return akk_epesu_present(slot)
|
||||
}
|
||||
if str_eq(verb, "epesu") {
|
||||
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
|
||||
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
|
||||
return akk_epesu_present(slot)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Main conjugation entry point ──────────────────────────────────────────────
|
||||
//
|
||||
// akk_conjugate: conjugate an Akkadian verb (G-stem).
|
||||
//
|
||||
// verb: G-stem infinitive (transliterated, e.g. "alāku", "amāru")
|
||||
// tense: "present" | "perfect" | "stative"
|
||||
// person: "first" | "second" | "third"
|
||||
// number: "singular" | "plural"
|
||||
//
|
||||
// Returns:
|
||||
// - Inflected form for known verbs
|
||||
// - verb unchanged as safe fallback for unknown verbs
|
||||
|
||||
fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String {
|
||||
let slot: Int = akk_slot(person, number)
|
||||
|
||||
// Copula shortcut
|
||||
if akk_is_copula(verb) {
|
||||
return akk_conjugate_copula(tense, slot)
|
||||
}
|
||||
|
||||
// Known-verb table
|
||||
let known: String = akk_known_verb(verb, tense, slot)
|
||||
if !str_eq(known, "") {
|
||||
return known
|
||||
}
|
||||
|
||||
// Unknown verb: safe fallback
|
||||
return verb
|
||||
}
|
||||
|
||||
// ── Noun declension ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// akk_decline: decline an Akkadian noun for gram_case and number.
|
||||
//
|
||||
// Mimation: OB nouns bear final -m in all case endings (mimation).
|
||||
// The base noun (dictionary form) is the nominative singular with mimation.
|
||||
// We strip the nominative -um ending (if present) to obtain the bare stem,
|
||||
// then apply the requested ending.
|
||||
//
|
||||
// Masculine case endings (singular):
|
||||
// Nominative: -um
|
||||
// Accusative: -am
|
||||
// Genitive: -im
|
||||
//
|
||||
// Masculine case endings (plural):
|
||||
// Nominative: -ūtum (or -ū in construct)
|
||||
// Accusative/Genitive: -ātim (or -ī in construct)
|
||||
//
|
||||
// Feminine nouns (identified by -tum nom sg ending):
|
||||
// Sg nominative: -tum, accusative: -tam, genitive: -tim
|
||||
// Pl nominative: -ātum, genitive/accusative: -ātim
|
||||
//
|
||||
// Known irregular stems (the vocabulary layer should pass dictionary forms):
|
||||
// šarrum → stem: šarr-
|
||||
// awīlum → stem: awīl-
|
||||
// bītum → stem: bīt-
|
||||
// ilum → stem: il-
|
||||
|
||||
fn akk_strip_nom(noun: String) -> String {
|
||||
// Strip -um (masc nom sg mimation ending) to get bare stem
|
||||
if akk_str_ends(noun, "um") {
|
||||
return akk_str_drop_last(noun, 2)
|
||||
}
|
||||
// Strip -tum (fem nom sg)
|
||||
if akk_str_ends(noun, "tum") {
|
||||
return akk_str_drop_last(noun, 3)
|
||||
}
|
||||
// Already a bare stem or unusual form: return as-is
|
||||
return noun
|
||||
}
|
||||
|
||||
fn akk_is_fem(noun: String) -> Bool {
|
||||
// Feminine nouns in OB typically end in -tum (nom sg)
|
||||
if akk_str_ends(noun, "tum") { return true }
|
||||
if akk_str_ends(noun, "tam") { return true }
|
||||
if akk_str_ends(noun, "tim") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn akk_decline(noun: String, gram_case: String, number: String) -> String {
|
||||
let fem: Bool = akk_is_fem(noun)
|
||||
let stem: String = akk_strip_nom(noun)
|
||||
|
||||
if str_eq(number, "singular") {
|
||||
if fem {
|
||||
if str_eq(gram_case, "nom") { return stem + "tum" }
|
||||
if str_eq(gram_case, "acc") { return stem + "tam" }
|
||||
if str_eq(gram_case, "gen") { return stem + "tim" }
|
||||
return stem + "tum"
|
||||
}
|
||||
// Masculine
|
||||
if str_eq(gram_case, "nom") { return stem + "um" }
|
||||
if str_eq(gram_case, "acc") { return stem + "am" }
|
||||
if str_eq(gram_case, "gen") { return stem + "im" }
|
||||
return stem + "um"
|
||||
}
|
||||
|
||||
// Plural
|
||||
if fem {
|
||||
if str_eq(gram_case, "nom") { return stem + "ātum" }
|
||||
// acc and gen merge in the oblique plural
|
||||
return stem + "ātim"
|
||||
}
|
||||
// Masculine plural
|
||||
if str_eq(gram_case, "nom") { return stem + "ūtum" }
|
||||
return stem + "ātim"
|
||||
}
|
||||
|
||||
// ── Noun phrase ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// akk_noun_phrase: produce the surface noun phrase.
|
||||
//
|
||||
// Akkadian has no definite or indefinite article. Determination is conveyed
|
||||
// by context, word order, and the genitive construct chain (status constructus).
|
||||
// The definite parameter is accepted but has no surface effect: the declined
|
||||
// noun is returned in either case.
|
||||
//
|
||||
// noun: dictionary form (nominative singular with mimation, e.g. "šarrum")
|
||||
// gram_case: "nom" | "acc" | "gen"
|
||||
// number: "singular" | "plural"
|
||||
// definite: "true" | "false" (no surface effect in Akkadian)
|
||||
|
||||
fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
|
||||
return akk_decline(noun, gram_case, number)
|
||||
}
|
||||
|
||||
// ── Canonical verb mapping ─────────────────────────────────────────────────────
|
||||
//
|
||||
// akk_map_canonical: map cross-lingual English canonical verb labels to
|
||||
// their Akkadian G-stem infinitive equivalents.
|
||||
|
||||
fn akk_map_canonical(verb: String) -> String {
|
||||
if str_eq(verb, "be") { return "bašû" }
|
||||
if str_eq(verb, "go") { return "alāku" }
|
||||
if str_eq(verb, "see") { return "amāru" }
|
||||
if str_eq(verb, "say") { return "qabû" }
|
||||
if str_eq(verb, "speak") { return "qabû" }
|
||||
if str_eq(verb, "do") { return "epēšu" }
|
||||
if str_eq(verb, "make") { return "epēšu" }
|
||||
return verb
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn akk_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn akk_str_len(s: String) -> Int
|
||||
extern fn akk_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn akk_slot(person: String, number: String) -> Int
|
||||
extern fn akk_slot_g(person: String, gender: String, number: String) -> Int
|
||||
extern fn akk_copula_present(slot: Int) -> String
|
||||
extern fn akk_copula_stative(slot: Int) -> String
|
||||
extern fn akk_is_copula(verb: String) -> Bool
|
||||
extern fn akk_conjugate_copula(tense: String, slot: Int) -> String
|
||||
extern fn akk_alaku_present(slot: Int) -> String
|
||||
extern fn akk_alaku_perfect(slot: Int) -> String
|
||||
extern fn akk_amaru_present(slot: Int) -> String
|
||||
extern fn akk_amaru_perfect(slot: Int) -> String
|
||||
extern fn akk_amaru_stative(slot: Int) -> String
|
||||
extern fn akk_qabu_present(slot: Int) -> String
|
||||
extern fn akk_qabu_perfect(slot: Int) -> String
|
||||
extern fn akk_qabu_stative(slot: Int) -> String
|
||||
extern fn akk_epesu_present(slot: Int) -> String
|
||||
extern fn akk_epesu_perfect(slot: Int) -> String
|
||||
extern fn akk_epesu_stative(slot: Int) -> String
|
||||
extern fn akk_regular_present(stem: String, slot: Int) -> String
|
||||
extern fn akk_regular_perfect(stem: String, slot: Int) -> String
|
||||
extern fn akk_regular_stative(stem: String, slot: Int) -> String
|
||||
extern fn akk_known_verb(verb: String, tense: String, slot: Int) -> String
|
||||
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn akk_strip_nom(noun: String) -> String
|
||||
extern fn akk_is_fem(noun: String) -> Bool
|
||||
extern fn akk_decline(noun: String, gram_case: String, number: String) -> String
|
||||
extern fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
|
||||
extern fn akk_map_canonical(verb: String) -> String
|
||||
@@ -1,752 +0,0 @@
|
||||
// morphology-ang.el - Old English (Anglo-Saxon) morphology for the NLG engine.
|
||||
//
|
||||
// Implements Old English verb conjugation, noun declension, and the definite
|
||||
// article/demonstrative pronoun. Designed as a companion to morphology.el and
|
||||
// called by the engine when the language profile code is "ang".
|
||||
//
|
||||
// Language profile: code=ang, name=Old English, morph_type=fusional,
|
||||
// word_order=SOV, question_strategy=intonation, script=latin, family=germanic.
|
||||
//
|
||||
// Typology note: Old English is a synthetic Germanic language with four
|
||||
// grammatical cases (nominative, accusative, genitive, dative), three genders,
|
||||
// and strong/weak noun and verb classes. Strong verbs form their past tense by
|
||||
// internal vowel change (ablaut); weak verbs use a dental (-de/-ode) suffix.
|
||||
// Long vowels are marked with a macron (ā ē ī ō ū) and are preserved in all
|
||||
// string literals; ǣ, æ, þ, ð, and ƿ (wynn) are used where historically
|
||||
// appropriate. V2 (verb-second) word order applies in main clauses but is not
|
||||
// enforced by this module — the realizer handles constituent ordering.
|
||||
//
|
||||
// Verb conjugation covered:
|
||||
// Tenses: present, past
|
||||
// Persons: first/second/third × singular/plural (slots 0-5)
|
||||
// Classes: weak (regular -ian), strong irregular table
|
||||
// Irregulars: wesan/beon (be), habban (have), gān (go), cuman (come),
|
||||
// secgan (say), sēon (see), dōn (do), willan (want), magan (can)
|
||||
// Canonical map: "be" -> "wesan" (past) / "beon" (present)
|
||||
//
|
||||
// Noun declension covered:
|
||||
// Strong masc a-stem (cyning pattern): nom/acc -∅, gen -es, dat -e; pl -as/-a/-um
|
||||
// Strong neut a-stem (word pattern): sg same as masc; pl nom/acc -∅
|
||||
// Weak n-stem (nama pattern): sg nom -a, obl -an; pl -an/-ena/-um
|
||||
//
|
||||
// Article: simplified demonstrative/article forms for masculine, feminine,
|
||||
// neuter (se/sēo/þæt), fully declined.
|
||||
//
|
||||
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn ang_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
fn ang_str_drop_last(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len {
|
||||
return ""
|
||||
}
|
||||
return str_slice(s, 0, len - n)
|
||||
}
|
||||
|
||||
fn ang_str_last_char(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
return str_slice(s, n - 1, n)
|
||||
}
|
||||
|
||||
fn ang_str_last2(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
if n < 2 {
|
||||
return s
|
||||
}
|
||||
return str_slice(s, n - 2, n)
|
||||
}
|
||||
|
||||
// ── Person/number slot ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Maps person × number to a 0-based index for paradigm tables.
|
||||
// 0 = 1st singular (ic)
|
||||
// 1 = 2nd singular (þū)
|
||||
// 2 = 3rd singular (hē/hēo/hit)
|
||||
// 3 = 1st plural (wē)
|
||||
// 4 = 2nd plural (gē)
|
||||
// 5 = 3rd plural (hīe)
|
||||
//
|
||||
// Old English also has a dual (wit, git) — not handled; dual falls through
|
||||
// to plural.
|
||||
|
||||
fn ang_slot(person: String, number: String) -> Int {
|
||||
if str_eq(person, "first") {
|
||||
if str_eq(number, "singular") { return 0 }
|
||||
return 3
|
||||
}
|
||||
if str_eq(person, "second") {
|
||||
if str_eq(number, "singular") { return 1 }
|
||||
return 4
|
||||
}
|
||||
// third
|
||||
if str_eq(number, "singular") { return 2 }
|
||||
return 5
|
||||
}
|
||||
|
||||
// ── Canonical verb mapping ─────────────────────────────────────────────────────
|
||||
//
|
||||
// The semantic layer may pass English canonical labels. Map to Old English
|
||||
// citation (infinitive) forms. "be" maps to "beon" for present and "wesan"
|
||||
// for past — the caller selects tense, so we map "be" to "beon" and handle
|
||||
// the past-tense wesan forms inside the conjugation function.
|
||||
|
||||
fn ang_map_canonical(verb: String) -> String {
|
||||
if str_eq(verb, "be") { return "beon" }
|
||||
if str_eq(verb, "have") { return "habban" }
|
||||
if str_eq(verb, "go") { return "gān" }
|
||||
if str_eq(verb, "come") { return "cuman" }
|
||||
if str_eq(verb, "say") { return "secgan" }
|
||||
if str_eq(verb, "see") { return "sēon" }
|
||||
if str_eq(verb, "do") { return "dōn" }
|
||||
if str_eq(verb, "want") { return "willan" }
|
||||
if str_eq(verb, "will") { return "willan" }
|
||||
if str_eq(verb, "can") { return "magan" }
|
||||
if str_eq(verb, "know") { return "witan" }
|
||||
if str_eq(verb, "give") { return "giefan" }
|
||||
if str_eq(verb, "take") { return "niman" }
|
||||
if str_eq(verb, "find") { return "findan" }
|
||||
if str_eq(verb, "make") { return "macian" }
|
||||
return verb
|
||||
}
|
||||
|
||||
// ── Irregular: wesan (to be — past tense forms) ───────────────────────────────
|
||||
//
|
||||
// Past: wæs wǣre wæs wǣron wǣron wǣron
|
||||
|
||||
fn ang_wesan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "wæs" }
|
||||
if slot == 1 { return "wǣre" }
|
||||
if slot == 2 { return "wæs" }
|
||||
if slot == 3 { return "wǣron" }
|
||||
if slot == 4 { return "wǣron" }
|
||||
return "wǣron"
|
||||
}
|
||||
|
||||
// ── Irregular: beon (to be — present / habitual / future) ────────────────────
|
||||
//
|
||||
// Present: bēo bist biþ bēoþ bēoþ bēoþ
|
||||
//
|
||||
// The present indicative of "wesan" is eom/eart/is/sind — that paradigm is
|
||||
// also provided below for completeness and for callers who specifically request
|
||||
// wesan present.
|
||||
|
||||
fn ang_beon_present(slot: Int) -> String {
|
||||
if slot == 0 { return "bēo" }
|
||||
if slot == 1 { return "bist" }
|
||||
if slot == 2 { return "biþ" }
|
||||
if slot == 3 { return "bēoþ" }
|
||||
if slot == 4 { return "bēoþ" }
|
||||
return "bēoþ"
|
||||
}
|
||||
|
||||
// ── Irregular: wesan present (eom/eart/is/sind) ───────────────────────────────
|
||||
//
|
||||
// Present: eom eart is sind/sindon sind sind
|
||||
|
||||
fn ang_wesan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "eom" }
|
||||
if slot == 1 { return "eart" }
|
||||
if slot == 2 { return "is" }
|
||||
if slot == 3 { return "sind" }
|
||||
if slot == 4 { return "sind" }
|
||||
return "sind"
|
||||
}
|
||||
|
||||
// ── Irregular: habban (to have) ───────────────────────────────────────────────
|
||||
//
|
||||
// Present: hæbbe hæfst hæfþ habbað habbað habbað
|
||||
// Past: hæfde hæfdest hæfde hæfdon hæfdon hæfdon
|
||||
|
||||
fn ang_habban_present(slot: Int) -> String {
|
||||
if slot == 0 { return "hæbbe" }
|
||||
if slot == 1 { return "hæfst" }
|
||||
if slot == 2 { return "hæfþ" }
|
||||
if slot == 3 { return "habbað" }
|
||||
if slot == 4 { return "habbað" }
|
||||
return "habbað"
|
||||
}
|
||||
|
||||
fn ang_habban_past(slot: Int) -> String {
|
||||
if slot == 0 { return "hæfde" }
|
||||
if slot == 1 { return "hæfdest" }
|
||||
if slot == 2 { return "hæfde" }
|
||||
if slot == 3 { return "hæfdon" }
|
||||
if slot == 4 { return "hæfdon" }
|
||||
return "hæfdon"
|
||||
}
|
||||
|
||||
// ── Irregular: gān (to go) ────────────────────────────────────────────────────
|
||||
//
|
||||
// Present: gā gǣst gǣþ gāð gāð gāð
|
||||
// Past: ēode ēodest ēode ēodon ēodon ēodon
|
||||
|
||||
fn ang_gan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "gā" }
|
||||
if slot == 1 { return "gǣst" }
|
||||
if slot == 2 { return "gǣþ" }
|
||||
if slot == 3 { return "gāð" }
|
||||
if slot == 4 { return "gāð" }
|
||||
return "gāð"
|
||||
}
|
||||
|
||||
fn ang_gan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "ēode" }
|
||||
if slot == 1 { return "ēodest" }
|
||||
if slot == 2 { return "ēode" }
|
||||
if slot == 3 { return "ēodon" }
|
||||
if slot == 4 { return "ēodon" }
|
||||
return "ēodon"
|
||||
}
|
||||
|
||||
// ── Irregular: cuman (to come) ────────────────────────────────────────────────
|
||||
//
|
||||
// Present: cume cymst cymþ cumað cumað cumað
|
||||
// Past: cōm cōme cōm cōmon cōmon cōmon
|
||||
|
||||
fn ang_cuman_present(slot: Int) -> String {
|
||||
if slot == 0 { return "cume" }
|
||||
if slot == 1 { return "cymst" }
|
||||
if slot == 2 { return "cymþ" }
|
||||
if slot == 3 { return "cumað" }
|
||||
if slot == 4 { return "cumað" }
|
||||
return "cumað"
|
||||
}
|
||||
|
||||
fn ang_cuman_past(slot: Int) -> String {
|
||||
if slot == 0 { return "cōm" }
|
||||
if slot == 1 { return "cōme" }
|
||||
if slot == 2 { return "cōm" }
|
||||
if slot == 3 { return "cōmon" }
|
||||
if slot == 4 { return "cōmon" }
|
||||
return "cōmon"
|
||||
}
|
||||
|
||||
// ── Irregular: secgan (to say) ────────────────────────────────────────────────
|
||||
//
|
||||
// Present: secge sagast sagað secgað secgað secgað
|
||||
// Past: sægde sægdest sægde sægdon sægdon sægdon
|
||||
|
||||
fn ang_secgan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "secge" }
|
||||
if slot == 1 { return "sagast" }
|
||||
if slot == 2 { return "sagað" }
|
||||
if slot == 3 { return "secgað" }
|
||||
if slot == 4 { return "secgað" }
|
||||
return "secgað"
|
||||
}
|
||||
|
||||
fn ang_secgan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "sægde" }
|
||||
if slot == 1 { return "sægdest" }
|
||||
if slot == 2 { return "sægde" }
|
||||
if slot == 3 { return "sægdon" }
|
||||
if slot == 4 { return "sægdon" }
|
||||
return "sægdon"
|
||||
}
|
||||
|
||||
// ── Irregular: sēon (to see) ──────────────────────────────────────────────────
|
||||
//
|
||||
// Present: sēo siehst siehþ sēoð sēoð sēoð
|
||||
// Past: seah sāwe seah sāwon sāwon sāwon
|
||||
|
||||
fn ang_seon_present(slot: Int) -> String {
|
||||
if slot == 0 { return "sēo" }
|
||||
if slot == 1 { return "siehst" }
|
||||
if slot == 2 { return "siehþ" }
|
||||
if slot == 3 { return "sēoð" }
|
||||
if slot == 4 { return "sēoð" }
|
||||
return "sēoð"
|
||||
}
|
||||
|
||||
fn ang_seon_past(slot: Int) -> String {
|
||||
if slot == 0 { return "seah" }
|
||||
if slot == 1 { return "sāwe" }
|
||||
if slot == 2 { return "seah" }
|
||||
if slot == 3 { return "sāwon" }
|
||||
if slot == 4 { return "sāwon" }
|
||||
return "sāwon"
|
||||
}
|
||||
|
||||
// ── Irregular: dōn (to do) ────────────────────────────────────────────────────
|
||||
//
|
||||
// Present: dō dēst dēþ dōð dōð dōð
|
||||
// Past: dyde dydest dyde dydon dydon dydon
|
||||
|
||||
fn ang_don_present(slot: Int) -> String {
|
||||
if slot == 0 { return "dō" }
|
||||
if slot == 1 { return "dēst" }
|
||||
if slot == 2 { return "dēþ" }
|
||||
if slot == 3 { return "dōð" }
|
||||
if slot == 4 { return "dōð" }
|
||||
return "dōð"
|
||||
}
|
||||
|
||||
fn ang_don_past(slot: Int) -> String {
|
||||
if slot == 0 { return "dyde" }
|
||||
if slot == 1 { return "dydest" }
|
||||
if slot == 2 { return "dyde" }
|
||||
if slot == 3 { return "dydon" }
|
||||
if slot == 4 { return "dydon" }
|
||||
return "dydon"
|
||||
}
|
||||
|
||||
// ── Irregular: willan (to want / will) ────────────────────────────────────────
|
||||
//
|
||||
// Present: wille wilt wile willað willað willað
|
||||
// Past: wolde woldest wolde woldon woldon woldon
|
||||
|
||||
fn ang_willan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "wille" }
|
||||
if slot == 1 { return "wilt" }
|
||||
if slot == 2 { return "wile" }
|
||||
if slot == 3 { return "willað" }
|
||||
if slot == 4 { return "willað" }
|
||||
return "willað"
|
||||
}
|
||||
|
||||
fn ang_willan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "wolde" }
|
||||
if slot == 1 { return "woldest" }
|
||||
if slot == 2 { return "wolde" }
|
||||
if slot == 3 { return "woldon" }
|
||||
if slot == 4 { return "woldon" }
|
||||
return "woldon"
|
||||
}
|
||||
|
||||
// ── Irregular: magan (to be able / can) ──────────────────────────────────────
|
||||
//
|
||||
// Present: mæg meaht mæg magon magon magon
|
||||
// Past: meahte meahtest meahte meahton meahton meahton
|
||||
|
||||
fn ang_magan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "mæg" }
|
||||
if slot == 1 { return "meaht" }
|
||||
if slot == 2 { return "mæg" }
|
||||
if slot == 3 { return "magon" }
|
||||
if slot == 4 { return "magon" }
|
||||
return "magon"
|
||||
}
|
||||
|
||||
fn ang_magan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "meahte" }
|
||||
if slot == 1 { return "meahtest" }
|
||||
if slot == 2 { return "meahte" }
|
||||
if slot == 3 { return "meahton" }
|
||||
if slot == 4 { return "meahton" }
|
||||
return "meahton"
|
||||
}
|
||||
|
||||
// ── Irregular: witan (to know) ────────────────────────────────────────────────
|
||||
//
|
||||
// Present: wāt wāst wāt witon witon witon
|
||||
// Past: wisse/wiste wissest wisse wisson wisson wisson
|
||||
|
||||
fn ang_witan_present(slot: Int) -> String {
|
||||
if slot == 0 { return "wāt" }
|
||||
if slot == 1 { return "wāst" }
|
||||
if slot == 2 { return "wāt" }
|
||||
if slot == 3 { return "witon" }
|
||||
if slot == 4 { return "witon" }
|
||||
return "witon"
|
||||
}
|
||||
|
||||
fn ang_witan_past(slot: Int) -> String {
|
||||
if slot == 0 { return "wisse" }
|
||||
if slot == 1 { return "wissest" }
|
||||
if slot == 2 { return "wisse" }
|
||||
if slot == 3 { return "wisson" }
|
||||
if slot == 4 { return "wisson" }
|
||||
return "wisson"
|
||||
}
|
||||
|
||||
// ── Weak verb: present-tense endings ─────────────────────────────────────────
|
||||
//
|
||||
// Weak verbs with -ian infinitives form their present tense as:
|
||||
// stem + -e, -est, -eþ, -aþ, -aþ, -aþ
|
||||
//
|
||||
// The stem is the infinitive with -ian stripped (or -an for class-2 verbs).
|
||||
|
||||
fn ang_weak_present_ending(slot: Int) -> String {
|
||||
if slot == 0 { return "e" }
|
||||
if slot == 1 { return "est" }
|
||||
if slot == 2 { return "eþ" }
|
||||
if slot == 3 { return "aþ" }
|
||||
if slot == 4 { return "aþ" }
|
||||
return "aþ"
|
||||
}
|
||||
|
||||
// ── Weak verb: past-tense ending selection ────────────────────────────────────
|
||||
//
|
||||
// Class 1 (-ian with short stem): past -ede (e.g. nerian -> nerede)
|
||||
// Class 2 (-ian with long/heavy stem): past -ode (e.g. macian -> macode)
|
||||
// Class 3 (-ian, small group): past -de (e.g. habban -> hæfde — irregular)
|
||||
//
|
||||
// Heuristic: if the stem length is 1 char, use -ede; otherwise use -ode.
|
||||
// This is a simplification; correct assignment requires lexical class marking.
|
||||
//
|
||||
// For the past, all persons in the plural share -on, and all singulars share
|
||||
// the same dental-suffixed stem.
|
||||
|
||||
fn ang_weak_past_stem(stem: String) -> String {
|
||||
let slen: Int = str_len(stem)
|
||||
if slen <= 2 {
|
||||
return stem + "ede"
|
||||
}
|
||||
return stem + "ode"
|
||||
}
|
||||
|
||||
fn ang_weak_past(stem: String, slot: Int) -> String {
|
||||
let pstem: String = ang_weak_past_stem(stem)
|
||||
if slot == 0 { return pstem }
|
||||
if slot == 1 { return pstem + "st" }
|
||||
if slot == 2 { return pstem }
|
||||
if slot == 3 { return ang_str_drop_last(pstem, 1) + "on" }
|
||||
if slot == 4 { return ang_str_drop_last(pstem, 1) + "on" }
|
||||
return ang_str_drop_last(pstem, 1) + "on"
|
||||
}
|
||||
|
||||
// ── Stem extraction for weak verbs ────────────────────────────────────────────
|
||||
//
|
||||
// Strip the infinitive ending to recover the stem:
|
||||
// -ian -> strip 3 chars (nerian -> ner-, macian -> mac-)
|
||||
// -an -> strip 2 chars (habban -> habb-; fallback for non -ian)
|
||||
// otherwise: return as-is
|
||||
|
||||
fn ang_weak_stem(verb: String) -> String {
|
||||
if ang_str_ends(verb, "ian") {
|
||||
return ang_str_drop_last(verb, 3)
|
||||
}
|
||||
if ang_str_ends(verb, "an") {
|
||||
return ang_str_drop_last(verb, 2)
|
||||
}
|
||||
return verb
|
||||
}
|
||||
|
||||
// ── ang_conjugate: main conjugation entry point ───────────────────────────────
|
||||
//
|
||||
// verb: Old English infinitive or English canonical label
|
||||
// tense: "present" | "past"
|
||||
// person: "first" | "second" | "third"
|
||||
// number: "singular" | "plural"
|
||||
//
|
||||
// Strategy:
|
||||
// 1. Map canonical English labels to OE verbs.
|
||||
// 2. Check the full irregular table.
|
||||
// 3. Fall back to weak conjugation for unknown -ian/-an verbs.
|
||||
// 4. Return the base form if nothing matches.
|
||||
|
||||
fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String {
|
||||
let v: String = ang_map_canonical(verb)
|
||||
let slot: Int = ang_slot(person, number)
|
||||
|
||||
// ── Irregulars ────────────────────────────────────────────────────────────
|
||||
|
||||
// beon: present-tense "be" (habitual/future/general)
|
||||
if str_eq(v, "beon") {
|
||||
if str_eq(tense, "present") { return ang_beon_present(slot) }
|
||||
// past: use wesan past forms
|
||||
return ang_wesan_past(slot)
|
||||
}
|
||||
|
||||
// wesan: past "be" and present "be" (existential/stative)
|
||||
if str_eq(v, "wesan") {
|
||||
if str_eq(tense, "present") { return ang_wesan_present(slot) }
|
||||
return ang_wesan_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "habban") {
|
||||
if str_eq(tense, "present") { return ang_habban_present(slot) }
|
||||
return ang_habban_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "gān") {
|
||||
if str_eq(tense, "present") { return ang_gan_present(slot) }
|
||||
return ang_gan_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "cuman") {
|
||||
if str_eq(tense, "present") { return ang_cuman_present(slot) }
|
||||
return ang_cuman_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "secgan") {
|
||||
if str_eq(tense, "present") { return ang_secgan_present(slot) }
|
||||
return ang_secgan_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "sēon") {
|
||||
if str_eq(tense, "present") { return ang_seon_present(slot) }
|
||||
return ang_seon_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "dōn") {
|
||||
if str_eq(tense, "present") { return ang_don_present(slot) }
|
||||
return ang_don_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "willan") {
|
||||
if str_eq(tense, "present") { return ang_willan_present(slot) }
|
||||
return ang_willan_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "magan") {
|
||||
if str_eq(tense, "present") { return ang_magan_present(slot) }
|
||||
return ang_magan_past(slot)
|
||||
}
|
||||
|
||||
if str_eq(v, "witan") {
|
||||
if str_eq(tense, "present") { return ang_witan_present(slot) }
|
||||
return ang_witan_past(slot)
|
||||
}
|
||||
|
||||
// ── Regular weak conjugation ──────────────────────────────────────────────
|
||||
|
||||
let stem: String = ang_weak_stem(v)
|
||||
|
||||
if str_eq(tense, "present") {
|
||||
return stem + ang_weak_present_ending(slot)
|
||||
}
|
||||
|
||||
if str_eq(tense, "past") {
|
||||
return ang_weak_past(stem, slot)
|
||||
}
|
||||
|
||||
// Unknown tense: return infinitive
|
||||
return v
|
||||
}
|
||||
|
||||
// ── Noun declension class detection ───────────────────────────────────────────
|
||||
//
|
||||
// Infer the declension class from the nominative singular form and an optional
|
||||
// gender hint. Without a full lexicon, ending-based heuristics are used:
|
||||
//
|
||||
// ends in -a -> weak n-stem (nama pattern)
|
||||
// ends in -e (long) -> may be various; default to strong masc a-stem
|
||||
// any other ending -> strong a-stem; gender distinguishes masc vs neut
|
||||
//
|
||||
// The caller may pass gender as a hint:
|
||||
// "masculine" | "feminine" | "neuter" | "" (empty = infer)
|
||||
//
|
||||
// For simplicity this module handles three paradigms:
|
||||
// "strong_masc" — a-stem masculine (cyning, mann)
|
||||
// "strong_neut" — a-stem neuter (word, scip)
|
||||
// "weak" — n-stem (nama, ēage)
|
||||
|
||||
fn ang_declension(noun: String, gender: String) -> String {
|
||||
if ang_str_ends(noun, "a") { return "weak" }
|
||||
if str_eq(gender, "neuter") { return "strong_neut" }
|
||||
return "strong_masc"
|
||||
}
|
||||
|
||||
// ── Strong masculine a-stem (cyning pattern) ──────────────────────────────────
|
||||
//
|
||||
// Stem: the noun as given (nom sg lacks an inflectional ending in this class).
|
||||
//
|
||||
// Singular: nom -∅ acc -∅ gen -es dat -e
|
||||
// Plural: nom -as acc -as gen -a dat -um
|
||||
|
||||
fn ang_decline_strong_masc(noun: String, gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return noun }
|
||||
if str_eq(gram_case, "accusative") { return noun }
|
||||
if str_eq(gram_case, "genitive") { return noun + "es" }
|
||||
if str_eq(gram_case, "dative") { return noun + "e" }
|
||||
return noun
|
||||
}
|
||||
// plural
|
||||
if str_eq(gram_case, "nominative") { return noun + "as" }
|
||||
if str_eq(gram_case, "accusative") { return noun + "as" }
|
||||
if str_eq(gram_case, "genitive") { return noun + "a" }
|
||||
if str_eq(gram_case, "dative") { return noun + "um" }
|
||||
return noun + "as"
|
||||
}
|
||||
|
||||
// ── Strong neuter a-stem (word pattern) ───────────────────────────────────────
|
||||
//
|
||||
// Singular: same as strong masc
|
||||
// Plural: nom/acc -∅ gen -a dat -um
|
||||
|
||||
fn ang_decline_strong_neut(noun: String, gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return noun }
|
||||
if str_eq(gram_case, "accusative") { return noun }
|
||||
if str_eq(gram_case, "genitive") { return noun + "es" }
|
||||
if str_eq(gram_case, "dative") { return noun + "e" }
|
||||
return noun
|
||||
}
|
||||
// plural: neuters have zero ending in nom/acc
|
||||
if str_eq(gram_case, "nominative") { return noun }
|
||||
if str_eq(gram_case, "accusative") { return noun }
|
||||
if str_eq(gram_case, "genitive") { return noun + "a" }
|
||||
if str_eq(gram_case, "dative") { return noun + "um" }
|
||||
return noun
|
||||
}
|
||||
|
||||
// ── Weak n-stem (nama pattern) ────────────────────────────────────────────────
|
||||
//
|
||||
// The nom sg ends in -a; the oblique stem is formed by stripping -a and adding
|
||||
// -an. Plural genitive is -ena.
|
||||
//
|
||||
// Singular: nom -a acc -an gen -an dat -an
|
||||
// Plural: nom -an acc -an gen -ena dat -um
|
||||
|
||||
fn ang_decline_weak(noun: String, gram_case: String, number: String) -> String {
|
||||
// Oblique stem: strip the final -a
|
||||
let stem: String = ang_str_drop_last(noun, 1)
|
||||
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return noun }
|
||||
if str_eq(gram_case, "accusative") { return stem + "an" }
|
||||
if str_eq(gram_case, "genitive") { return stem + "an" }
|
||||
if str_eq(gram_case, "dative") { return stem + "an" }
|
||||
return noun
|
||||
}
|
||||
// plural
|
||||
if str_eq(gram_case, "nominative") { return stem + "an" }
|
||||
if str_eq(gram_case, "accusative") { return stem + "an" }
|
||||
if str_eq(gram_case, "genitive") { return stem + "ena" }
|
||||
if str_eq(gram_case, "dative") { return stem + "um" }
|
||||
return stem + "an"
|
||||
}
|
||||
|
||||
// ── ang_decline: main declension entry point ──────────────────────────────────
|
||||
//
|
||||
// noun: nominative singular Old English noun (e.g. "cyning", "word", "nama")
|
||||
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
|
||||
// number: "singular" | "plural"
|
||||
// gender: "masculine" | "neuter" | "feminine" | "" (empty triggers inference)
|
||||
//
|
||||
// Returns the inflected form. Falls back to the nominative singular for any
|
||||
// unrecognised combination.
|
||||
|
||||
fn ang_decline(noun: String, gram_case: String, number: String, gender: String) -> String {
|
||||
let decl: String = ang_declension(noun, gender)
|
||||
|
||||
if str_eq(decl, "strong_masc") {
|
||||
return ang_decline_strong_masc(noun, gram_case, number)
|
||||
}
|
||||
|
||||
if str_eq(decl, "strong_neut") {
|
||||
return ang_decline_strong_neut(noun, gram_case, number)
|
||||
}
|
||||
|
||||
if str_eq(decl, "weak") {
|
||||
return ang_decline_weak(noun, gram_case, number)
|
||||
}
|
||||
|
||||
// Unknown: return nominative unchanged
|
||||
return noun
|
||||
}
|
||||
|
||||
// ── Definite article / demonstrative: se/sēo/þæt ─────────────────────────────
|
||||
//
|
||||
// Old English used the demonstrative pronoun se/sēo/þæt as a definite article.
|
||||
// The full paradigm (gender × case × number) is given below.
|
||||
//
|
||||
// Masculine:
|
||||
// sg: nom se acc þone gen þæs dat þǣm
|
||||
// pl: nom þā acc þā gen þāra dat þǣm
|
||||
//
|
||||
// Feminine:
|
||||
// sg: nom sēo acc þā gen þǣre dat þǣre
|
||||
// pl: nom þā acc þā gen þāra dat þǣm
|
||||
//
|
||||
// Neuter:
|
||||
// sg: nom þæt acc þæt gen þæs dat þǣm
|
||||
// pl: nom þā acc þā gen þāra dat þǣm
|
||||
|
||||
fn ang_article_masculine(gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return "se" }
|
||||
if str_eq(gram_case, "accusative") { return "þone" }
|
||||
if str_eq(gram_case, "genitive") { return "þæs" }
|
||||
if str_eq(gram_case, "dative") { return "þǣm" }
|
||||
return "se"
|
||||
}
|
||||
// plural
|
||||
if str_eq(gram_case, "nominative") { return "þā" }
|
||||
if str_eq(gram_case, "accusative") { return "þā" }
|
||||
if str_eq(gram_case, "genitive") { return "þāra" }
|
||||
if str_eq(gram_case, "dative") { return "þǣm" }
|
||||
return "þā"
|
||||
}
|
||||
|
||||
fn ang_article_feminine(gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return "sēo" }
|
||||
if str_eq(gram_case, "accusative") { return "þā" }
|
||||
if str_eq(gram_case, "genitive") { return "þǣre" }
|
||||
if str_eq(gram_case, "dative") { return "þǣre" }
|
||||
return "sēo"
|
||||
}
|
||||
// plural
|
||||
if str_eq(gram_case, "nominative") { return "þā" }
|
||||
if str_eq(gram_case, "accusative") { return "þā" }
|
||||
if str_eq(gram_case, "genitive") { return "þāra" }
|
||||
if str_eq(gram_case, "dative") { return "þǣm" }
|
||||
return "þā"
|
||||
}
|
||||
|
||||
fn ang_article_neuter(gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "singular") {
|
||||
if str_eq(gram_case, "nominative") { return "þæt" }
|
||||
if str_eq(gram_case, "accusative") { return "þæt" }
|
||||
if str_eq(gram_case, "genitive") { return "þæs" }
|
||||
if str_eq(gram_case, "dative") { return "þǣm" }
|
||||
return "þæt"
|
||||
}
|
||||
// plural
|
||||
if str_eq(gram_case, "nominative") { return "þā" }
|
||||
if str_eq(gram_case, "accusative") { return "þā" }
|
||||
if str_eq(gram_case, "genitive") { return "þāra" }
|
||||
if str_eq(gram_case, "dative") { return "þǣm" }
|
||||
return "þā"
|
||||
}
|
||||
|
||||
fn ang_article(gender: String, gram_case: String, number: String) -> String {
|
||||
if str_eq(gender, "masculine") { return ang_article_masculine(gram_case, number) }
|
||||
if str_eq(gender, "feminine") { return ang_article_feminine(gram_case, number) }
|
||||
// neuter
|
||||
return ang_article_neuter(gram_case, number)
|
||||
}
|
||||
|
||||
// ── Gender inference from noun form ───────────────────────────────────────────
|
||||
//
|
||||
// A last-resort heuristic when the caller provides no gender hint.
|
||||
// -a ending strongly suggests weak masculine or neuter (but most -a nouns are
|
||||
// masculine weak). Without a full lexicon, masculine is the safe default.
|
||||
|
||||
fn ang_infer_gender(noun: String) -> String {
|
||||
if ang_str_ends(noun, "u") { return "feminine" }
|
||||
if ang_str_ends(noun, "e") { return "feminine" }
|
||||
return "masculine"
|
||||
}
|
||||
|
||||
// ── ang_noun_phrase: noun phrase builder ──────────────────────────────────────
|
||||
//
|
||||
// Produces a declined noun with optional definite article (demonstrative)
|
||||
// prepended. When gender is empty ("") it is inferred from the noun form.
|
||||
//
|
||||
// noun: nominative singular Old English noun
|
||||
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
|
||||
// number: "singular" | "plural"
|
||||
// definite: "true" | "false"
|
||||
|
||||
fn ang_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
|
||||
let gender: String = ang_infer_gender(noun)
|
||||
let declined: String = ang_decline(noun, gram_case, number, gender)
|
||||
if str_eq(definite, "true") {
|
||||
let art: String = ang_article(gender, gram_case, number)
|
||||
return art + " " + declined
|
||||
}
|
||||
return declined
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user