Compare commits

..

1 Commits

Author SHA1 Message Date
will.anderson 5baab050e2 ci: test pr->dev hook
El SDK CI - dev / build-and-test (pull_request) Failing after 30s
2026-05-06 17:19:56 -05:00
195 changed files with 18954 additions and 12050 deletions
+50 -148
View File
@@ -22,46 +22,39 @@ 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
- 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
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
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: |
ELC="$(pwd)/dist/platform/elc" \
@@ -87,23 +80,14 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
@@ -112,8 +96,8 @@ jobs:
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)
@@ -122,8 +106,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
@@ -132,8 +116,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
@@ -142,8 +126,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
@@ -152,8 +136,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
@@ -162,8 +146,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
@@ -172,8 +156,8 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
@@ -182,32 +166,10 @@ jobs:
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
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
-lcurl -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'
@@ -215,16 +177,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
VERSION="${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
@@ -232,15 +198,7 @@ jobs:
--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 \
--package=el/el_runtime.c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
@@ -248,65 +206,9 @@ jobs:
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--package=el/el_runtime.h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-dev"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (dev)
# Patches ci-base:dev in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:dev (or fall back to :latest on first run)
BASE_TAG="dev"
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:dev" \
-t "${CI_BASE}:dev-${SHA}" \
.
docker push "${CI_BASE}:dev"
docker push "${CI_BASE}:dev-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
rm -f /tmp/gcp-key.json
+39 -111
View File
@@ -34,25 +34,35 @@ jobs:
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
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
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
- name: Run tests - text
@@ -87,7 +97,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
@@ -97,7 +107,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
@@ -107,7 +117,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
@@ -117,7 +127,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
@@ -127,7 +137,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
@@ -137,7 +147,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
@@ -147,7 +157,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
@@ -157,7 +167,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
@@ -167,45 +177,9 @@ jobs:
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
-lcurl -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'
@@ -213,19 +187,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates 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
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
@@ -233,7 +208,7 @@ jobs:
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--package=el/el_runtime.c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
@@ -241,56 +216,9 @@ jobs:
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--package=el/el_runtime.h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
echo "Published El SDK version=${VERSION} to foundation-stage"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (stage)
# Patches ci-base:stage in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:stage (system deps stay cached in the base layer)
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:stage" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:stage" \
-t "${CI_BASE}:stage-${SHA}" \
.
docker push "${CI_BASE}:stage"
docker push "${CI_BASE}:stage-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
rm -f /tmp/gcp-key.json
+59 -108
View File
@@ -34,26 +34,35 @@ jobs:
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: |
mkdir -p dist/platform
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
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 binary
@@ -65,30 +74,34 @@ jobs:
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-lcurl -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/)
# Build epm binary (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")
dist/platform/elc ../epm/src/epm.el > dist/epm.c
gcc -O2 \
-I el-compiler/runtime \
dist/epm.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-o dist/bin/epm
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
# Build el-install binary
- 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")
dist/platform/elc tools/install/el-install.el > dist/el-install.c
gcc -O2 \
-I el-compiler/runtime \
dist/el-install.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-o dist/bin/el-install
chmod +x dist/bin/el-install
echo "el-install built"
@@ -124,7 +137,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
@@ -134,7 +147,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
@@ -144,7 +157,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
@@ -154,7 +167,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
@@ -164,7 +177,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
@@ -174,7 +187,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
@@ -184,7 +197,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
@@ -194,7 +207,7 @@ jobs:
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
-lcurl -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
@@ -204,7 +217,7 @@ jobs:
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
-lcurl -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
@@ -228,7 +241,7 @@ jobs:
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: |
@@ -289,19 +302,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates 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
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
@@ -309,15 +323,7 @@ jobs:
--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 \
--package=el/el_runtime.c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
@@ -325,75 +331,20 @@ jobs:
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--package=el/el_runtime.h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-prod"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK
# Patches ci-base:latest in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base (system deps stay cached in the base layer)
docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:latest" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:latest" \
-t "${CI_BASE}:${SHA}" \
.
docker push "${CI_BASE}:latest"
docker push "${CI_BASE}:${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
rm -f /tmp/gcp-key.json
- name: Dispatch el-sdk-updated to downstream repos
if: github.event_name == 'push'
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
for repo in neuron-technologies/forge; do
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
+46 -46
View File
@@ -1,46 +1,46 @@
// auto-generated by elc --emit-header do not edit
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
extern fn lang_get(profile: [String], key: String) -> String
extern fn lang_profile_en() -> [String]
extern fn lang_profile_ja() -> [String]
extern fn lang_profile_ar() -> [String]
extern fn lang_profile_zh() -> [String]
extern fn lang_profile_de() -> [String]
extern fn lang_profile_es() -> [String]
extern fn lang_profile_fi() -> [String]
extern fn lang_profile_sw() -> [String]
extern fn lang_profile_hi() -> [String]
extern fn lang_profile_ru() -> [String]
extern fn lang_profile_fr() -> [String]
extern fn lang_profile_la() -> [String]
extern fn lang_profile_he() -> [String]
extern fn lang_profile_sa() -> [String]
extern fn lang_profile_got() -> [String]
extern fn lang_profile_non() -> [String]
extern fn lang_profile_enm() -> [String]
extern fn lang_profile_pi() -> [String]
extern fn lang_profile_grc() -> [String]
extern fn lang_profile_ang() -> [String]
extern fn lang_profile_fro() -> [String]
extern fn lang_profile_goh() -> [String]
extern fn lang_profile_sga() -> [String]
extern fn lang_profile_txb() -> [String]
extern fn lang_profile_peo() -> [String]
extern fn lang_profile_akk() -> [String]
extern fn lang_profile_uga() -> [String]
extern fn lang_profile_egy() -> [String]
extern fn lang_profile_sux() -> [String]
extern fn lang_profile_gez() -> [String]
extern fn lang_profile_cop() -> [String]
extern fn lang_from_code(code: String) -> [String]
extern fn lang_default() -> [String]
extern fn lang_is_isolating(profile: [String]) -> Bool
extern fn lang_is_agglutinative(profile: [String]) -> Bool
extern fn lang_is_fusional(profile: [String]) -> Bool
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
extern fn lang_is_rtl(profile: [String]) -> Bool
extern fn lang_has_null_subject(profile: [String]) -> Bool
extern fn lang_has_case(profile: [String]) -> Bool
extern fn lang_has_gender(profile: [String]) -> Bool
extern fn lang_word_order(profile: [String]) -> String
extern fn lang_code(profile: [String]) -> String
// 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) -> Any
extern fn lang_get(profile: Any, key: String) -> String
extern fn lang_profile_en() -> Any
extern fn lang_profile_ja() -> Any
extern fn lang_profile_ar() -> Any
extern fn lang_profile_zh() -> Any
extern fn lang_profile_de() -> Any
extern fn lang_profile_es() -> Any
extern fn lang_profile_fi() -> Any
extern fn lang_profile_sw() -> Any
extern fn lang_profile_hi() -> Any
extern fn lang_profile_ru() -> Any
extern fn lang_profile_fr() -> Any
extern fn lang_profile_la() -> Any
extern fn lang_profile_he() -> Any
extern fn lang_profile_sa() -> Any
extern fn lang_profile_got() -> Any
extern fn lang_profile_non() -> Any
extern fn lang_profile_enm() -> Any
extern fn lang_profile_pi() -> Any
extern fn lang_profile_grc() -> Any
extern fn lang_profile_ang() -> Any
extern fn lang_profile_fro() -> Any
extern fn lang_profile_goh() -> Any
extern fn lang_profile_sga() -> Any
extern fn lang_profile_txb() -> Any
extern fn lang_profile_peo() -> Any
extern fn lang_profile_akk() -> Any
extern fn lang_profile_uga() -> Any
extern fn lang_profile_egy() -> Any
extern fn lang_profile_sux() -> Any
extern fn lang_profile_gez() -> Any
extern fn lang_profile_cop() -> Any
extern fn lang_from_code(code: String) -> Any
extern fn lang_default() -> Any
extern fn lang_is_isolating(profile: Any) -> Bool
extern fn lang_is_agglutinative(profile: Any) -> Bool
extern fn lang_is_fusional(profile: Any) -> Bool
extern fn lang_is_polysynthetic(profile: Any) -> Bool
extern fn lang_is_rtl(profile: Any) -> Bool
extern fn lang_has_null_subject(profile: Any) -> Bool
extern fn lang_has_case(profile: Any) -> Bool
extern fn lang_has_gender(profile: Any) -> Bool
extern fn lang_word_order(profile: Any) -> String
extern fn lang_code(profile: Any) -> String
-1
View File
@@ -56,7 +56,6 @@
// String helpers
import "morphology.el"
fn akk_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// 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
-1
View File
@@ -36,7 +36,6 @@
// String helpers
import "morphology.el"
fn ang_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn ang_str_ends(s: String, suf: String) -> Bool
extern fn ang_str_drop_last(s: String, n: Int) -> String
extern fn ang_str_last_char(s: String) -> String
-1
View File
@@ -21,7 +21,6 @@
// String helpers
import "morphology.el"
fn ar_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn ar_str_ends(s: String, suf: String) -> Bool
extern fn ar_str_len(s: String) -> Int
extern fn ar_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -54,7 +54,6 @@
// String helpers
import "morphology.el"
fn cop_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn cop_str_ends(s: String, suf: String) -> Bool
extern fn cop_str_len(s: String) -> Int
extern fn cop_drop(s: String, n: Int) -> String
-1
View File
@@ -26,7 +26,6 @@
// Dat: dem der dem den
// Gen: des der des der
import "morphology.el"
fn de_article_def(gender: String, gram_case: String, number: String) -> String {
if str_eq(number, "pl") {
if str_eq(gram_case, "nom") { return "die" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn de_article_def(gender: String, gram_case: String, number: String) -> String
extern fn de_article_indef(gender: String, gram_case: String, number: String) -> String
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
-1
View File
@@ -52,7 +52,6 @@
// String helpers
import "morphology.el"
fn egy_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn egy_str_ends(s: String, suf: String) -> Bool
extern fn egy_str_len(s: String) -> Int
extern fn egy_drop(s: String, n: Int) -> String
-1
View File
@@ -31,7 +31,6 @@
// String helpers
import "morphology.el"
fn enm_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn enm_str_ends(s: String, suf: String) -> Bool
extern fn enm_drop(s: String, n: Int) -> String
extern fn enm_first_char(s: String) -> String
-1
View File
@@ -12,7 +12,6 @@
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn es_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn es_str_ends(s: String, suf: String) -> Bool
extern fn es_str_drop_last(s: String, n: Int) -> String
extern fn es_str_last_char(s: String) -> String
-1
View File
@@ -25,7 +25,6 @@
// If only neutral vowels are found, default to "front" (the conservative choice
// for borrowed words and those without clear back vowels).
import "morphology.el"
fn fi_harmony(word: String) -> String {
let n: Int = str_len(word)
let i: Int = n - 1
+3 -3
View File
@@ -1,11 +1,11 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn fi_harmony(word: String) -> String
extern fn fi_suffix(base: String, harmony: String) -> String
extern fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
extern fn fi_str_last_char(s: String) -> String
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_verb_stem(dict_form: String) -> String
extern fn fi_irregular_verb(dict_form: String) -> [String]
extern fn fi_irregular_verb(dict_form: String) -> Any
extern fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String
extern fn fi_past_stem(stem: String) -> String
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String
@@ -14,4 +14,4 @@ extern fn fi_negative(verb: String, person: String, number: String) -> String
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fi_question_suffix(harmony: String) -> String
extern fn fi_make_question(verb_form: String, harmony: String) -> String
extern fn fi_full_paradigm(noun: String) -> [String]
extern fn fi_full_paradigm(noun: String) -> Any
-1
View File
@@ -19,7 +19,6 @@
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn fr_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn fr_str_ends(s: String, suf: String) -> Bool
extern fn fr_str_drop_last(s: String, n: Int) -> String
extern fn fr_str_last_char(s: String) -> String
-1
View File
@@ -53,7 +53,6 @@
// String helpers
import "morphology.el"
fn fro_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn fro_str_ends(s: String, suf: String) -> Bool
extern fn fro_drop(s: String, n: Int) -> String
extern fn fro_slot(person: String, number: String) -> Int
-1
View File
@@ -64,7 +64,6 @@
// String helpers
import "morphology.el"
fn gez_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn gez_str_ends(s: String, suf: String) -> Bool
extern fn gez_str_len(s: String) -> Int
extern fn gez_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -48,7 +48,6 @@
// String helpers
import "morphology.el"
fn goh_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn goh_str_ends(s: String, suf: String) -> Bool
extern fn goh_drop(s: String, n: Int) -> String
extern fn goh_slot(person: String, number: String) -> Int
-1
View File
@@ -49,7 +49,6 @@
// String helpers
import "morphology.el"
fn got_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn got_str_ends(s: String, suf: String) -> Bool
extern fn got_str_drop_last(s: String, n: Int) -> String
extern fn got_slot(person: String, number: String) -> Int
-1
View File
@@ -31,7 +31,6 @@
// String helpers
import "morphology.el"
fn grc_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn grc_str_ends(s: String, suf: String) -> Bool
extern fn grc_str_drop_last(s: String, n: Int) -> String
extern fn grc_str_last_char(s: String) -> String
-1
View File
@@ -51,7 +51,6 @@
// String helpers
import "morphology.el"
fn he_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn he_str_ends(s: String, suf: String) -> Bool
extern fn he_str_len(s: String) -> Int
extern fn he_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -24,7 +24,6 @@
// String helpers
import "morphology.el"
fn hi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn hi_str_ends(s: String, suf: String) -> Bool
extern fn hi_str_drop_last(s: String, n: Int) -> String
extern fn hi_str_last_char(s: String) -> String
-1
View File
@@ -23,7 +23,6 @@
// Note: this is a heuristic classifier for romanized input. For production use
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
import "morphology.el"
fn ja_verb_group(dict_form: String) -> String {
// Irregular verbs (exact match on dictionary form)
if str_eq(dict_form, "する") { return "irregular" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn ja_verb_group(dict_form: String) -> String
extern fn ja_ichidan_stem(dict_form: String) -> String
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
-1
View File
@@ -25,7 +25,6 @@
// String helpers
import "morphology.el"
fn la_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn la_str_ends(s: String, suf: String) -> Bool
extern fn la_str_drop_last(s: String, n: Int) -> String
extern fn la_str_last_char(s: String) -> String
-1
View File
@@ -27,7 +27,6 @@
// String helpers
import "morphology.el"
fn non_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn non_str_ends(s: String, suf: String) -> Bool
extern fn non_drop(s: String, n: Int) -> String
extern fn non_last(s: String) -> String
-1
View File
@@ -31,7 +31,6 @@
// String helpers
import "morphology.el"
fn peo_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn peo_drop(s: String, n: Int) -> String
extern fn peo_ends(s: String, suf: String) -> Bool
extern fn peo_slot(person: String, number: String) -> Int
-1
View File
@@ -30,7 +30,6 @@
// String helpers
import "morphology.el"
fn pi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn pi_str_ends(s: String, suf: String) -> Bool
extern fn pi_drop(s: String, n: Int) -> String
extern fn pi_last_char(s: String) -> String
-1
View File
@@ -35,7 +35,6 @@
// The heuristic returns the most probable gender. Caller should override
// for known exceptions (путь, рубль are masc despite ).
import "morphology.el"
fn ru_gender(noun: String) -> String {
let n: Int = str_len(noun)
if n == 0 { return "m" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn ru_gender(noun: String) -> String
extern fn ru_stem_type(noun: String, gender: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
-1
View File
@@ -42,7 +42,6 @@
// String helpers
import "morphology.el"
fn sa_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn sa_str_ends(s: String, suf: String) -> Bool
extern fn sa_str_drop_last(s: String, n: Int) -> String
extern fn sa_slot(person: String, number: String) -> Int
-1
View File
@@ -31,7 +31,6 @@
// String helpers
import "morphology.el"
fn sga_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn sga_drop(s: String, n: Int) -> String
extern fn sga_first(s: String) -> String
extern fn sga_rest(s: String) -> String
-1
View File
@@ -53,7 +53,6 @@
// String helpers
import "morphology.el"
fn sux_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn sux_str_ends(s: String, suf: String) -> Bool
extern fn sux_str_drop_last(s: String, n: Int) -> String
extern fn sux_str_last_char(s: String) -> String
-1
View File
@@ -24,7 +24,6 @@
// String helpers
import "morphology.el"
fn sw_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn sw_str_ends(s: String, suf: String) -> Bool
extern fn sw_str_drop_last(s: String, n: Int) -> String
extern fn sw_str_first_char(s: String) -> String
-1
View File
@@ -30,7 +30,6 @@
// String helpers
import "morphology.el"
fn txb_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn txb_drop(s: String, n: Int) -> String
extern fn txb_ends(s: String, suf: String) -> Bool
extern fn txb_slot(person: String, number: String) -> Int
-1
View File
@@ -48,7 +48,6 @@
// String helpers
import "morphology.el"
fn uga_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn uga_str_ends(s: String, suf: String) -> Bool
extern fn uga_str_len(s: String) -> Int
extern fn uga_str_drop_last(s: String, n: Int) -> String
-11
View File
@@ -33,17 +33,6 @@
// String helpers
import "language-profile.el"
import "morphology-es.el"
import "morphology-fr.el"
import "morphology-de.el"
import "morphology-ru.el"
import "morphology-fi.el"
import "morphology-ar.el"
import "morphology-hi.el"
import "morphology-sw.el"
import "morphology-la.el"
import "morphology-ja.el"
fn str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+5 -5
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header do not edit
// auto-generated by elc --emit-header - do not edit
extern fn str_ends(s: String, suf: String) -> Bool
extern fn str_last_char(s: String) -> String
extern fn str_last2(s: String) -> String
@@ -8,7 +8,7 @@ extern fn is_vowel(c: String) -> Bool
extern fn morph_apply_suffix(base: String, suffix: String) -> String
extern fn en_irregular_plural(word: String) -> String
extern fn en_irregular_singular(word: String) -> String
extern fn en_irregular_verb(base: String) -> [String]
extern fn en_irregular_verb(base: String) -> Any
extern fn en_verb_3sg(base: String) -> String
extern fn en_should_double_final(base: String) -> Bool
extern fn en_verb_past(base: String) -> String
@@ -16,10 +16,10 @@ extern fn en_verb_gerund(base: String) -> String
extern fn en_pluralize_regular(singular: String) -> String
extern fn en_verb_form(base: String, tense: String, person: String, number: String) -> String
extern fn agree_determiner(det: String, noun: String) -> String
extern fn morph_pluralize(noun: String, profile: [String]) -> String
extern fn morph_pluralize(noun: String, profile: Any) -> String
extern fn morph_map_canonical(verb: String, code: String) -> String
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: [String]) -> String
extern fn morph_inflect(word: String, features: String, profile: [String]) -> String
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: Any) -> String
extern fn morph_inflect(word: String, features: String, profile: Any) -> String
extern fn pluralize(singular: String) -> String
extern fn singularize(plural: String) -> String
extern fn verb_form(base: String, tense: String, person: String, number: String) -> String
+19 -19
View File
@@ -1,20 +1,20 @@
// auto-generated by elc --emit-header do not edit
extern fn lex_word(entry: [String]) -> String
extern fn lex_pos(entry: [String]) -> String
extern fn lex_form(entry: [String], idx: Int) -> String
extern fn lex_class(entry: [String]) -> String
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> [String]
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> [String]
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> [String]
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> [String]
extern fn build_vocab() -> [[String]]
extern fn get_vocab() -> [[String]]
extern fn vocab_lookup(word: String, lang_code: String) -> [String]
extern fn vocab_lookup_en(word: String) -> [String]
// auto-generated by elc --emit-header - do not edit
extern fn lex_word(entry: Any) -> String
extern fn lex_pos(entry: Any) -> String
extern fn lex_form(entry: Any, idx: Int) -> String
extern fn lex_class(entry: Any) -> String
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> Any
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> Any
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> Any
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> Any
extern fn build_vocab() -> Any
extern fn get_vocab() -> Any
extern fn vocab_lookup(word: String, lang_code: String) -> Any
extern fn vocab_lookup_en(word: String) -> Any
extern fn vocab_synonym(word: String, lang_register: String, lang_code: String) -> String
extern fn vocab_by_pos(pos: String) -> [[String]]
extern fn vocab_by_class(cls: String) -> [[String]]
extern fn entry_found(entry: [String]) -> Bool
extern fn entry_word(entry: [String]) -> String
extern fn entry_pos(entry: [String]) -> String
extern fn entry_form(entry: [String], n: Int) -> String
extern fn vocab_by_pos(pos: String) -> Any
extern fn vocab_by_class(cls: String) -> Any
extern fn entry_found(entry: Any) -> Bool
extern fn entry_word(entry: Any) -> String
extern fn entry_pos(entry: Any) -> String
extern fn entry_form(entry: Any, n: Int) -> String
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
+32 -711
View File
@@ -42,7 +42,6 @@
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include <sys/resource.h> /* getrusage — memory guard */
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
@@ -178,17 +177,15 @@ static el_val_t el_wrap_str(char* s) {
/* ── I/O ──────────────────────────────────────────────────────────────────── */
el_val_t println(el_val_t s) {
void println(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) puts(str);
else puts("");
return 0;
}
el_val_t print(el_val_t s) {
void print(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) fputs(str, stdout);
return 0;
}
el_val_t readline(void) {
@@ -295,10 +292,6 @@ el_val_t str_to_int(el_val_t sv) {
return (el_val_t)atoll(s);
}
/* native_str_to_int — El compiler-generated alias for str_to_int.
* Converts a string el_val_t to its integer representation. */
el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); }
el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
@@ -914,33 +907,6 @@ el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_ma
return r;
}
/* http_post_json_with_headers — POST with Content-Type: application/json plus
* any additional headers supplied as an El map. Combines http_post_json and
* http_post_with_headers: the Content-Type header is always prepended so
* callers do not have to include it in their map. */
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) {
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
/* Append caller-supplied headers from the map */
ElMap* m = as_map(headers_map);
if (m) {
for (int64_t i = 0; i < m->count; i++) {
const char* k = EL_CSTR(m->keys[i]);
const char* v = EL_CSTR(m->values[i]);
if (!k || !v) continue;
size_t n = strlen(k) + strlen(v) + 4;
char* line = malloc(n);
if (!line) continue;
snprintf(line, n, "%s: %s", k, v);
h = curl_slist_append(h, line);
free(line);
}
}
el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h);
curl_slist_free_all(h);
return r;
}
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) {
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded");
@@ -1093,7 +1059,7 @@ void el_runtime_register_handler(const char* name, http_handler_fn fn) {
pthread_mutex_unlock(&_http_handler_mu);
}
el_val_t http_set_handler(el_val_t name) {
void http_set_handler(el_val_t name) {
const char* n = EL_CSTR(name);
pthread_mutex_lock(&_http_handler_mu);
free(_http_active_handler);
@@ -1117,7 +1083,6 @@ el_val_t http_set_handler(el_val_t name) {
}
}
pthread_mutex_unlock(&_http_handler_mu);
return 0;
}
static http_handler_fn http_lookup_active(void) {
@@ -1575,18 +1540,18 @@ static void* http_worker(void* arg) {
return NULL;
}
el_val_t http_serve(el_val_t port, el_val_t handler) {
void http_serve(el_val_t port, el_val_t handler) {
/* If `handler` looks like a string name, register it as the active handler. */
const char* hname = EL_CSTR(handler);
if (hname && looks_like_string(handler)) {
http_set_handler(handler);
}
int p = (int)port;
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; }
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; }
/* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6.
* This makes `localhost` work in browsers that resolve it to ::1 first. */
int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 0; }
if (sock < 0) { perror("socket"); return; }
int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
@@ -1596,9 +1561,9 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return 0;
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
@@ -1629,7 +1594,6 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
pthread_detach(tid);
}
close(sock);
return 0;
}
/* ── HTTP server v2 — request headers + structured response ──────────────── */
@@ -1681,7 +1645,7 @@ void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) {
pthread_mutex_unlock(&_http_handler_mu);
}
el_val_t http_set_handler_v2(el_val_t name) {
void http_set_handler_v2(el_val_t name) {
const char* n = EL_CSTR(name);
pthread_mutex_lock(&_http_handler_mu);
free(_http_active_handler4);
@@ -1703,7 +1667,6 @@ el_val_t http_set_handler_v2(el_val_t name) {
}
}
pthread_mutex_unlock(&_http_handler_mu);
return 0;
}
static http_handler4_fn http_lookup_active_v2(void) {
@@ -1824,7 +1787,7 @@ static void* http_worker_v2(void* arg) {
return NULL;
}
el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
void http_serve_v2(el_val_t port, el_val_t handler) {
const char* hname = EL_CSTR(handler);
if (hname && looks_like_string(handler)) {
http_set_handler_v2(handler);
@@ -1832,11 +1795,11 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
int p = (int)port;
if (p <= 0 || p > 65535) {
fprintf(stderr, "http_serve_v2: invalid port %d\n", p);
return 0;
return;
}
/* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */
int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 0; }
if (sock < 0) { perror("socket"); return; }
int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
@@ -1846,9 +1809,9 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return 0;
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
@@ -1879,84 +1842,6 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
pthread_detach(tid);
}
close(sock);
return 0;
}
/* ── http_serve_async — non-blocking HTTP server ─────────────────────────── */
/* Runs the accept loop in a background pthread, returns immediately so the
* calling EL script can continue (e.g. to run an awareness loop).
*
* El signature: http_serve_async(port, handler) -> Void */
typedef struct { int sock; } HttpServeAsyncArg;
static void* _http_serve_async_loop(void* raw) {
HttpServeAsyncArg* a = (HttpServeAsyncArg*)raw;
int sock = a->sock;
free(a);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
if (cfd < 0) {
if (errno == EINTR) continue;
perror("accept"); break;
}
pthread_mutex_lock(&_http_conn_mu);
while (_http_conn_active >= HTTP_MAX_CONNS) {
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
}
_http_conn_active++;
pthread_mutex_unlock(&_http_conn_mu);
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
if (!arg) { close(cfd); continue; }
arg->fd = cfd;
pthread_t tid;
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
close(cfd); free(arg);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
pthread_mutex_unlock(&_http_conn_mu);
continue;
}
pthread_detach(tid);
}
close(sock);
return NULL;
}
void http_serve_async(el_val_t port, el_val_t handler) {
const char* hname = EL_CSTR(handler);
if (hname && looks_like_string(handler)) {
http_set_handler(handler);
}
int p = (int)port;
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve_async: invalid port %d\n", p); return; }
int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return; }
int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
if (!a) { close(sock); return; }
a->sock = sock;
pthread_t tid;
if (pthread_create(&tid, NULL, _http_serve_async_loop, a) != 0) {
perror("pthread_create"); free(a); close(sock); return;
}
pthread_detach(tid);
/* Returns immediately — caller can now run awareness_run() or any loop. */
}
/* Build the response envelope a 4-arg handler can return. We hand-write
@@ -2319,43 +2204,6 @@ el_val_t url_decode(el_val_t sv) {
return el_wrap_str(out);
}
/* ── html_raw ────────────────────────────────────────────────────────────────
* Identity passthrough for raw HTML template interpolation.
* El's {raw(expr)} compiles to html_raw(expr) the value is output as-is
* without any escaping. The caller is responsible for safety.
*/
el_val_t html_raw(el_val_t s) {
return s;
}
/* ── html_escape ─────────────────────────────────────────────────────────────
* Escape < > " ' & for safe HTML text interpolation.
* El's {expr} in HTML templates compiles to html_escape(expr).
*/
el_val_t html_escape(el_val_t sv) {
const char* src = EL_CSTR(sv);
if (!src) return EL_STR("");
size_t len = strlen(src);
/* Worst case: every byte → 6 chars (&quot;) */
char* out = (char*)malloc(len * 6 + 1);
if (!out) return sv;
el_arena_track(out);
char* p = out;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)src[i];
switch (c) {
case '&': memcpy(p, "&amp;", 5); p += 5; break;
case '<': memcpy(p, "&lt;", 4); p += 4; break;
case '>': memcpy(p, "&gt;", 4); p += 4; break;
case '"': memcpy(p, "&quot;", 6); p += 6; break;
case '\'': memcpy(p, "&#39;", 5); p += 5; break;
default: *p++ = (char)c; break;
}
}
*p = '\0';
return el_wrap_str(out);
}
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
* el_html_sanitize(input, allowlist_json)
*
@@ -3250,49 +3098,23 @@ static void jb_puts(JsonBuf* b, const char* s) {
static void jb_emit_escaped(JsonBuf* b, const char* s) {
jb_putc(b, '"');
const unsigned char* p = (const unsigned char*)s;
while (*p) {
unsigned char c = *p;
for (; *s; s++) {
unsigned char c = (unsigned char)*s;
switch (c) {
case '"': jb_puts(b, "\\\""); p++; break;
case '\\': jb_puts(b, "\\\\"); p++; break;
case '\b': jb_puts(b, "\\b"); p++; break;
case '\f': jb_puts(b, "\\f"); p++; break;
case '\n': jb_puts(b, "\\n"); p++; break;
case '\r': jb_puts(b, "\\r"); p++; break;
case '\t': jb_puts(b, "\\t"); p++; break;
case '"': jb_puts(b, "\\\""); break;
case '\\': jb_puts(b, "\\\\"); break;
case '\b': jb_puts(b, "\\b"); break;
case '\f': jb_puts(b, "\\f"); break;
case '\n': jb_puts(b, "\\n"); break;
case '\r': jb_puts(b, "\\r"); break;
case '\t': jb_puts(b, "\\t"); break;
default:
if (c < 0x20) {
char tmp[8];
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
jb_puts(b, tmp);
p++;
} else if (c < 0x80) {
jb_putc(b, (char)c);
p++;
} else {
/* Multi-byte UTF-8: validate sequence, pass through if valid,
* escape as \u00xx if the start byte is invalid/orphaned. */
int seq_len = 0;
if ((c & 0xE0) == 0xC0) seq_len = 2;
else if ((c & 0xF0) == 0xE0) seq_len = 3;
else if ((c & 0xF8) == 0xF0) seq_len = 4;
if (seq_len >= 2) {
int valid = 1;
for (int i = 1; i < seq_len; i++) {
if ((p[i] & 0xC0) != 0x80) { valid = 0; break; }
}
if (valid) {
for (int i = 0; i < seq_len; i++) jb_putc(b, (char)p[i]);
p += seq_len;
break;
}
}
/* Invalid start byte or truncated sequence — escape it */
char tmp[8];
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
jb_puts(b, tmp);
p++;
jb_putc(b, (char)c);
}
break;
}
@@ -5177,13 +4999,7 @@ el_val_t state_get_or(el_val_t key, el_val_t default_val) {
el_val_t float_to_str(el_val_t f) {
char buf[64];
double v = el_to_float(f);
/* Normalize NaN to "nan" regardless of sign — platform-independent. */
if (isnan(v)) {
snprintf(buf, sizeof(buf), "nan");
} else {
snprintf(buf, sizeof(buf), "%g", v);
}
snprintf(buf, sizeof(buf), "%g", el_to_float(f));
return el_wrap_str(el_strdup(buf));
}
@@ -5803,9 +5619,8 @@ el_val_t parse_int(el_val_t sv, el_val_t default_val) {
/* ── Process ─────────────────────────────────────────────────────────────── */
el_val_t exit_program(el_val_t code) {
void exit_program(el_val_t code) {
exit((int)code);
return 0; /* unreachable */
}
/* getpid_now — current process id. Named with the _now suffix to avoid
@@ -5815,50 +5630,6 @@ el_val_t getpid_now(void) {
return (el_val_t)getpid();
}
/* el_mem_check — self-terminating memory guard for long-running compiler runs.
*
* Call this periodically (e.g. after each function compiled) to detect runaway
* memory growth before the OS OOM-killer fires. Reads the limit from the env
* var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit,
* prints a diagnostic to stderr and exits with code 1 so the caller (elb or a
* CI script) can handle the failure gracefully instead of having the whole
* machine go down.
*
* Platform notes:
* macOS ru_maxrss is in bytes.
* Linux ru_maxrss is in kilobytes.
* We normalise to MB before comparing.
*
* Returns 0 always (the only non-return path is the exit() branch).
*/
el_val_t el_mem_check(void) {
/* Read limit from env; default 512 MB. */
long limit_mb = 512;
const char *env_val = getenv("ELC_MAX_MEM_MB");
if (env_val && *env_val) {
long v = atol(env_val);
if (v > 0) limit_mb = v;
}
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */
long rss_mb;
#if defined(__APPLE__) || defined(__MACH__)
/* macOS: ru_maxrss is bytes */
rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L));
#else
/* Linux: ru_maxrss is kilobytes */
rss_mb = (long)(ru.ru_maxrss / 1024L);
#endif
if (rss_mb >= limit_mb) {
fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb);
exit(1);
}
return 0;
}
/* ── args() — command-line argument access ──────────────────────────────────
* Compiled El programs call args() to get a list of CLI arguments.
* Call el_runtime_init_args(argc, argv) at the start of C main() to populate.
@@ -6031,14 +5802,6 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
#define ENGRAM_LAYER_DOMAIN 2u
#define ENGRAM_LAYER_IMPRINT 3u
#define ENGRAM_LAYER_SUIT 4u
#define ENGRAM_LAYER_ACCUMULATION 5u
/* New user-facing nodes (memories, knowledge, conversations) are created in the
* accumulation layer the top of the consciousness stack, the engram the user
* sees; every layer below shapes behavior but is hidden from the user (Layered
* Consciousness architecture, app 64/064,262). ENGRAM_LAYER_DEFAULT stays
* core-identity ON PURPOSE: it is the fallback home for LEGACY nodes loaded from
* snapshots without a layer_id, so existing data (the originator corpus) is
* never migrated out of its established layer. New != legacy. */
#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY
/* Pass 3 override floor. Layer 0 nodes that received any background
@@ -6216,20 +5979,6 @@ static void engram_init_layers(EngramStore* g) {
.transparent = 0,
.injectable = 1
};
/* Layer 5 — accumulation. The TOP of the consciousness stack: the default
* home for all new user-facing nodes. This is the engram the user sees;
* every layer below shapes behavior but is hidden from the user. Not
* injectable it is the persistent user accumulation, not a swappable
* overlay. transparent=0: its content is surfaced to introspection (it is
* the user's own knowledge/memory), unlike the lower behavioral layers. */
g->layers[g->layer_count++] = (EngramLayer){
.layer_id = ENGRAM_LAYER_ACCUMULATION,
.name = el_strdup_persist("accumulation"),
.activation_priority = 50,
.suppressible = 1,
.transparent = 0,
.injectable = 0
};
}
static EngramStore* engram_get(void) {
@@ -6421,7 +6170,7 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
n->layer_id = ENGRAM_LAYER_DEFAULT;
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -6457,7 +6206,7 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
n->layer_id = ENGRAM_LAYER_DEFAULT;
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -8014,257 +7763,6 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) {
return el_wrap_str(b.buf);
}
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
* Reads a JSON snapshot from `path` and adds any nodes/edges not already
* present in the in-memory graph. Dedup is by node id (for nodes) and by
* (from_id, to_id, relation) tuple (for edges).
*
* Returns (as an EL int) the count of new nodes added. Embeddings are
* intentionally skipped on merged nodes to avoid Ollama delays at runtime;
* auto_link_semantic will handle them when nodes are next activated.
*
* Does not merge layers the in-process layer registry is authoritative. */
el_val_t engram_load_merge(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p || !*p) return 0;
FILE* f = fopen(p, "rb");
if (!f) return 0;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
if (sz <= 0) { fclose(f); return 0; }
char* data = malloc((size_t)sz + 1);
if (!data) { fclose(f); return 0; }
size_t got = fread(data, 1, (size_t)sz, f);
fclose(f);
data[got] = '\0';
EngramStore* g = engram_get();
int64_t added_nodes = 0;
/* Walk nodes array — skip any node whose id already exists */
const char* nodes_p = json_find_key(data, "nodes");
if (nodes_p) {
nodes_p = eg_skip_ws(nodes_p);
if (*nodes_p == '[') {
nodes_p++;
nodes_p = eg_skip_ws(nodes_p);
while (*nodes_p && *nodes_p != ']') {
if (*nodes_p != '{') { nodes_p++; continue; }
const char* end = json_skip_value(nodes_p);
size_t n = (size_t)(end - nodes_p);
char* obj = malloc(n + 1);
memcpy(obj, nodes_p, n); obj[n] = '\0';
char* nid = eg_get_str_field(obj, "id");
int already = (nid && *nid && engram_find_node(nid) != NULL);
free(nid);
if (!already) {
engram_grow_nodes();
EngramNode* nn = &g->nodes[g->node_count];
memset(nn, 0, sizeof(*nn));
nn->id = eg_get_str_field(obj, "id");
nn->content = eg_get_str_field(obj, "content");
nn->node_type = eg_get_str_field(obj, "node_type");
nn->label = eg_get_str_field(obj, "label");
nn->tier = eg_get_str_field(obj, "tier");
nn->tags = eg_get_str_field(obj, "tags");
nn->metadata = eg_get_str_field(obj, "metadata");
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = strdup("{}"); }
nn->salience = eg_get_num_field(obj, "salience");
nn->importance = eg_get_num_field(obj, "importance");
nn->confidence = eg_get_num_field(obj, "confidence");
nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate");
nn->activation_count = eg_get_int_field(obj, "activation_count");
nn->last_activated = eg_get_int_field(obj, "last_activated");
nn->created_at = eg_get_int_field(obj, "created_at");
nn->updated_at = eg_get_int_field(obj, "updated_at");
nn->background_activation = eg_get_num_field(obj, "background_activation");
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
nn->working_memory_weight = 0.0; /* clamp corrupt snapshot values */
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
if (json_find_key(obj, "layer_id")) {
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
} else {
nn->layer_id = ENGRAM_LAYER_DEFAULT;
}
g->node_count++;
added_nodes++;
}
free(obj);
nodes_p = end;
nodes_p = eg_skip_ws(nodes_p);
if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); }
}
}
}
/* Walk edges array — skip if (from_id, to_id, relation) already present */
const char* edges_p = json_find_key(data, "edges");
if (edges_p) {
edges_p = eg_skip_ws(edges_p);
if (*edges_p == '[') {
edges_p++;
edges_p = eg_skip_ws(edges_p);
while (*edges_p && *edges_p != ']') {
if (*edges_p != '{') { edges_p++; continue; }
const char* end = json_skip_value(edges_p);
size_t n = (size_t)(end - edges_p);
char* obj = malloc(n + 1);
memcpy(obj, edges_p, n); obj[n] = '\0';
char* efrom = eg_get_str_field(obj, "from_id");
char* eto = eg_get_str_field(obj, "to_id");
char* erel = eg_get_str_field(obj, "relation");
/* Check for duplicate by scanning existing edges */
int dup = 0;
if (efrom && eto && erel) {
for (int64_t ei = 0; ei < g->edge_count; ei++) {
EngramEdge* ex = &g->edges[ei];
if (ex->from_id && ex->to_id && ex->relation &&
strcmp(ex->from_id, efrom) == 0 &&
strcmp(ex->to_id, eto) == 0 &&
strcmp(ex->relation, erel) == 0) {
dup = 1; break;
}
}
}
if (!dup) {
engram_grow_edges();
EngramEdge* ee = &g->edges[g->edge_count];
memset(ee, 0, sizeof(*ee));
ee->id = eg_get_str_field(obj, "id");
ee->from_id = efrom ? efrom : strdup("");
ee->to_id = eto ? eto : strdup("");
ee->relation = erel ? erel : strdup("");
ee->metadata = eg_get_str_field(obj, "metadata");
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = strdup("{}"); }
ee->weight = eg_get_num_field(obj, "weight");
ee->confidence = eg_get_num_field(obj, "confidence");
ee->created_at = eg_get_int_field(obj, "created_at");
ee->updated_at = eg_get_int_field(obj, "updated_at");
ee->last_fired = eg_get_int_field(obj, "last_fired");
ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory");
if (json_find_key(obj, "layer_id")) {
ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
} else {
ee->layer_id = ENGRAM_LAYER_DEFAULT;
}
g->edge_count++;
/* NOTE: efrom/eto/erel ownership transferred to ee above */
efrom = NULL; eto = NULL; erel = NULL;
} else {
free(efrom); free(eto); free(erel);
}
free(obj);
edges_p = end;
edges_p = eg_skip_ws(edges_p);
if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); }
}
}
}
free(data);
return (el_val_t)added_nodes;
}
el_val_t engram_wm_count(void) {
EngramStore* g = engram_get();
int64_t count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) count++;
}
return (el_val_t)count;
}
/* Average working_memory_weight across all promoted nodes (wm > 0).
* Returns the float bit-pattern via el_from_float so EL can use it with
* float_to_str / float_gt. Returns 0.0 when no nodes are promoted.
* Useful in heartbeat ISEs to distinguish "many weak activations" (sparse
* graph, low avg) from "few strong activations" (dense subgraph, high avg).
* Added 2026-06-04 self-review for graph health observability. */
el_val_t engram_wm_avg_weight(void) {
EngramStore* g = engram_get();
double sum = 0.0;
int64_t count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
double w = g->nodes[i].working_memory_weight;
/* Defensive guard: skip any corrupt/out-of-range values so a single
* bad snapshot node doesn't produce a garbage average (e.g. 1.77e+234). */
if (w > 0.0 && w <= 1.0 && isfinite(w)) { sum += w; count++; }
}
double avg = (count > 0) ? (sum / (double)count) : 0.0;
return el_from_float(avg);
}
/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a
* compact JSON array for ISE heartbeat reporting.
*
* Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42}
*
* Purpose: the heartbeat ISE reports wm_active (count) and wm_avg_weight but
* gives zero visibility into WM *composition* which types/tiers are active.
* After long uptime every WM slot is in steady-state decay+re-promotion so
* wm_promotion ISEs never fire (they only fire on 0>0.1 transitions).
* This function fills the observability gap by snapshotting the current top-N
* WM nodes on every heartbeat. Inserted 2026-06-05 self-review. */
el_val_t engram_wm_top_json(el_val_t n_v) {
int64_t top_n = (int64_t)n_v;
if (top_n <= 0) top_n = 10;
if (top_n > 50) top_n = 50;
EngramStore* g = engram_get();
/* Collect indices of promoted nodes, excluding monitoring noise.
* InternalStateEvent nodes are system-observation artifacts they reflect
* what the daemon is doing, not what it knows. Including them in wm_top
* buries real knowledge (Memory, Knowledge, Belief nodes) under a wall of
* heartbeat/curiosity ISEs, making the heartbeat ISE useless for diagnosing
* WM composition. Filter them out here so wm_top always shows substantive
* content. (2026-06-07 self-review) */
int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t));
if (!idx) return el_wrap_str(el_strdup("[]"));
int64_t mc = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) {
const char* nt = g->nodes[i].node_type;
if (nt && strcmp(nt, "InternalStateEvent") == 0) continue;
idx[mc++] = i;
}
}
/* Insertion-sort descending by wm weight (mc is typically small). */
for (int64_t i = 1; i < mc; i++) {
int64_t key = idx[i];
double kw = g->nodes[key].working_memory_weight;
int64_t j = i;
while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) {
idx[j] = idx[j-1]; j--;
}
idx[j] = key;
}
int64_t emit = mc < top_n ? mc : top_n;
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
for (int64_t k = 0; k < emit; k++) {
EngramNode* n = &g->nodes[idx[k]];
if (k > 0) jb_putc(&b, ',');
jb_putc(&b, '{');
jb_puts(&b, "\"label\":");
jb_emit_escaped(&b, n->label ? n->label : "");
jb_puts(&b, ",\"node_type\":");
jb_emit_escaped(&b, n->node_type ? n->node_type : "");
jb_puts(&b, ",\"tier\":");
jb_emit_escaped(&b, n->tier ? n->tier : "");
char tmp[48];
snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight);
jb_puts(&b, tmp);
jb_putc(&b, '}');
}
free(idx);
jb_putc(&b, ']');
return el_wrap_str(b.buf);
}
#ifdef HAVE_CURL
/* ── DHARMA network ─────────────────────────────────────────────────────────
* Real implementation. Peers are addressed by `dharma_id` either bare
@@ -8905,7 +8403,7 @@ static el_val_t llm_provider_request(const char* url, const char* key,
}
}
static el_val_t llm_chain_call(const char* model_pref, const char* system_str, const char* user_str) {
static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
char url_key[64], key_key[64], fmt_key[64], model_key[64];
for (int i = 0; i < LLM_MAX_PROVIDERS; i++) {
snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i);
@@ -8918,7 +8416,6 @@ static el_val_t llm_chain_call(const char* model_pref, const char* system_str, c
const char* fmt_s = getenv(fmt_key);
int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0;
const char* model = getenv(model_key);
if (!model || !*model) model = model_pref; /* fall back to the caller-requested model */
fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url);
el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str);
const char* t = EL_CSTR(result);
@@ -8929,7 +8426,7 @@ static el_val_t llm_chain_call(const char* model_pref, const char* system_str, c
const char* api_key = getenv("ANTHROPIC_API_KEY");
if (!api_key || !*api_key) return http_error_json("no LLM providers configured");
fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n");
return llm_provider_request(LLM_API_URL, api_key, 1, model_pref, system_str, user_str);
return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str);
}
/* Legacy llm_request — kept for backward compat with agentic loop internals */
@@ -8993,16 +8490,14 @@ static el_val_t llm_extract_text(el_val_t resp_val) {
}
el_val_t llm_call(el_val_t model, el_val_t prompt) {
const char* m = EL_CSTR(model);
const char* u = EL_CSTR(prompt); if (!u) u = "";
return llm_chain_call(m, NULL, u);
return llm_chain_call(NULL, u);
}
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) {
const char* m = EL_CSTR(model);
const char* s = EL_CSTR(system_prompt); if (!s) s = "";
const char* u = EL_CSTR(user_prompt); if (!u) u = "";
return llm_chain_call(m, s, u);
return llm_chain_call(s, u);
}
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
@@ -11682,175 +11177,6 @@ el_val_t hash_sha256(el_val_t sv) {
return el_hex_encode(digest, 32);
}
/* ── __ prefixed aliases — public boundary for compiled El programs ──────────
*
* The El compiler's self-hosting back-end emits calls to __-prefixed function
* names (e.g. __println, __str_len). These wrappers forward to the existing
* el_runtime implementations so both naming conventions resolve at link time.
*
* Note: __thread_create and __thread_join are already defined above in the
* threading section; they are not repeated here.
* */
/* I/O */
el_val_t __println(el_val_t s) { return println(s); }
el_val_t __print(el_val_t s) { return print(s); }
el_val_t __readline(void) { return readline(); }
/* String */
el_val_t __int_to_str(el_val_t n) { return int_to_str(n); }
el_val_t __str_to_int(el_val_t s) { return str_to_int(s); }
el_val_t __float_to_str(el_val_t f) { return float_to_str(f); }
el_val_t __str_to_float(el_val_t s) { return str_to_float(s); }
el_val_t __str_len(el_val_t s) { return str_len(s); }
el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); }
el_val_t __str_cmp(el_val_t a, el_val_t b) {
const char* ca = EL_CSTR(a);
const char* cb = EL_CSTR(b);
if (!ca) ca = "";
if (!cb) cb = "";
return (el_val_t)strcmp(ca, cb);
}
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) {
const char* ca = EL_CSTR(a);
const char* cb = EL_CSTR(b);
if (!ca) ca = "";
if (!cb) cb = "";
return (el_val_t)strncmp(ca, cb, (size_t)n);
}
el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); }
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); }
el_val_t __str_alloc(el_val_t n) {
if (n <= 0) n = 0;
char* buf = el_strbuf((size_t)n + 1);
memset(buf, 0, (size_t)n + 1);
return el_wrap_str(buf);
}
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
char* buf = (char*)(uintptr_t)s;
if (buf) buf[(size_t)i] = (char)c;
return s;
}
/* URL encoding */
el_val_t __url_encode(el_val_t s) { return url_encode(s); }
el_val_t __url_decode(el_val_t s) { return url_decode(s); }
/* Environment */
el_val_t __env_get(el_val_t key) { return env(key); }
/* Subprocess */
el_val_t __exec(el_val_t cmd) { return exec(cmd); }
el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); }
/* Process */
el_val_t __exit_program(el_val_t code) { return exit_program(code); }
/* Filesystem */
el_val_t __fs_exists(el_val_t path) { return fs_exists(path); }
el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); }
el_val_t __fs_read(el_val_t path) { return fs_read(path); }
el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); }
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); }
el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); }
/* HTTP server (no curl dependency) */
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); }
el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); }
el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); }
/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here
* so el_runtime.c compiles standalone. When both translation units are linked
* the el_seed.c definitions win via their non-static linkage (strong symbols).
* These stubs are marked weak so they are silently overridden. */
__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); }
__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; }
__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; }
__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; }
/* JSON */
el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); }
el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); }
el_val_t __json_array_len(el_val_t json) { return json_array_len(json); }
el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); }
el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); }
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); }
el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); }
el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); }
/* Hashing */
el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); }
/* State K/V */
el_val_t __state_del(el_val_t key) { return state_del(key); }
el_val_t __state_get(el_val_t key) { return state_get(key); }
el_val_t __state_keys(void) { return state_keys(); }
el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); }
/* UUID */
el_val_t __uuid_v4(void) { return uuid_v4(); }
/* Args */
el_val_t __args_json(void) { return args(); }
/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below
* with a matching stub in the #ifndef HAVE_CURL block. */
#ifdef HAVE_CURL
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_map, el_val_t timeout_ms) {
/* timeout_ms is accepted for API compatibility but ignored here;
* el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */
(void)timeout_ms;
struct curl_slist* h = headers_from_map(headers_map);
el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h);
if (h) curl_slist_free_all(h);
return r;
}
/* __http_do_map — same as __http_do but headers_map arg is a JSON-string
* rather than an ElMap. Parse it first, then delegate. */
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t timeout_ms) {
(void)timeout_ms;
/* Build a curl_slist from a JSON object {"Header":"value",...}. */
const char* hj = EL_CSTR(headers_json);
struct curl_slist* h = NULL;
if (hj && *hj && *hj == '{') {
/* Walk the JSON pairs with a simple parser reusing json_get_string logic. */
/* For correctness we just call the existing json_get iteration path.
* We duplicate the key-extraction loop from headers_from_map but driven
* by JSON rather than ElMap. Use json_get_raw to iterate is not easy
* without knowing keys, so accept the JSON string and build a tmp map. */
el_val_t map = json_parse(EL_STR(hj));
h = headers_from_map(map);
}
el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h);
if (h) curl_slist_free_all(h);
return r;
}
/* __http_do_map_to_file — same as __http_do_map but streams response body
* to a local file path rather than returning it as a string. */
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t output_path) {
const char* hj = EL_CSTR(headers_json);
struct curl_slist* h = NULL;
if (hj && *hj && *hj == '{') {
el_val_t map = json_parse(EL_STR(hj));
h = headers_from_map(map);
}
el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body),
h, EL_CSTR(output_path));
if (h) curl_slist_free_all(h);
return r;
}
#endif /* HAVE_CURL */
#ifndef HAVE_CURL
/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── *
* These return a JSON error string so El programs get a clear message if they
@@ -11863,7 +11189,6 @@ el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body
el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); }
el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); }
el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); }
el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); }
el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); }
el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
@@ -11877,8 +11202,4 @@ el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (voi
el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); }
el_val_t llm_models(void) { return el_list_empty(); }
void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; }
/* __ HTTP stubs (no-curl build) */
el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); }
el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); }
el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); }
#endif /* !HAVE_CURL */
+7 -113
View File
@@ -81,8 +81,8 @@ extern "C" {
/* ── I/O ──────────────────────────────────────────────────────────────────── */
el_val_t println(el_val_t s);
el_val_t print(el_val_t s);
void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
/* ── String builtins ─────────────────────────────────────────────────────── */
@@ -95,7 +95,6 @@ el_val_t str_len(el_val_t s);
el_val_t str_concat(el_val_t a, el_val_t b);
el_val_t int_to_str(el_val_t n);
el_val_t str_to_int(el_val_t s);
el_val_t native_str_to_int(el_val_t s);
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
el_val_t str_contains(el_val_t s, el_val_t sub);
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
@@ -150,11 +149,10 @@ el_val_t http_post(el_val_t url, el_val_t body);
el_val_t http_post_json(el_val_t url, el_val_t json_body);
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_serve(el_val_t port, el_val_t handler);
el_val_t http_set_handler(el_val_t name);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
/* HTTP server v2 ─────────────────────────────────────────────────────────────
* Same dispatch model as http_serve, but the handler signature is widened:
@@ -175,9 +173,8 @@ el_val_t http_set_handler(el_val_t name);
* The 3-arg http_serve(port, handler) remains supported unchanged for
* existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
void http_serve_async(el_val_t port, el_val_t handler);
el_val_t http_set_handler_v2(el_val_t name);
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
@@ -228,8 +225,6 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
* {"p":[],"a":["href","title"],"strong":[],...}
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
el_val_t html_raw(el_val_t s);
el_val_t html_escape(el_val_t s);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
@@ -531,15 +526,9 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
/* ── Process ─────────────────────────────────────────────────────────────── */
el_val_t exit_program(el_val_t code);
void exit_program(el_val_t code);
el_val_t getpid_now(void);
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
* exits with code 1 if resident memory exceeds the limit. Call periodically
* during long compilation loops (e.g. after each function is compiled).
* Returns 0 when memory is within bounds. */
el_val_t el_mem_check(void);
/* ── CGI identity ─────────────────────────────────────────────────────────────
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
* Records the program's DHARMA identity before any other code executes. */
@@ -639,12 +628,6 @@ el_val_t engram_list_layers_json(void);
* no nodes promoted to working memory. */
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
/* ── Working memory ──────────────────────────────────────────────────────────*/
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
el_val_t engram_load_merge(el_val_t path);
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
* All functions call https://api.anthropic.com/v1/messages with the API key
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
@@ -796,95 +779,6 @@ el_val_t emit_event(el_val_t name, el_val_t duration_ms);
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
el_val_t __thread_join(el_val_t tid_v);
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
* The El self-hosting compiler emits calls to __-prefixed names. These are
* forwarding wrappers around the existing el_runtime functions above. */
/* I/O */
el_val_t __println(el_val_t s);
el_val_t __print(el_val_t s);
el_val_t __readline(void);
/* String */
el_val_t __int_to_str(el_val_t n);
el_val_t __str_to_int(el_val_t s);
el_val_t __float_to_str(el_val_t f);
el_val_t __str_to_float(el_val_t s);
el_val_t __str_len(el_val_t s);
el_val_t __str_char_at(el_val_t s, el_val_t i);
el_val_t __str_cmp(el_val_t a, el_val_t b);
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
el_val_t __str_alloc(el_val_t n);
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
/* URL encoding */
el_val_t __url_encode(el_val_t s);
el_val_t __url_decode(el_val_t s);
/* Environment */
el_val_t __env_get(el_val_t key);
/* Subprocess */
el_val_t __exec(el_val_t cmd);
el_val_t __exec_bg(el_val_t cmd);
/* Process */
el_val_t __exit_program(el_val_t code);
/* Filesystem */
el_val_t __fs_exists(el_val_t path);
el_val_t __fs_mkdir(el_val_t path);
el_val_t __fs_read(el_val_t path);
el_val_t __fs_write(el_val_t path, el_val_t content);
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
el_val_t __fs_list_raw(el_val_t path);
/* HTTP server */
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
el_val_t __http_serve(el_val_t port, el_val_t handler);
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
el_val_t __http_conn_fd(void);
el_val_t __http_sse_open(el_val_t conn_id);
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
el_val_t __http_sse_close(el_val_t conn_id);
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_map, el_val_t timeout_ms);
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t timeout_ms);
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t output_path);
/* JSON */
el_val_t __json_array_get(el_val_t json, el_val_t index);
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
el_val_t __json_array_len(el_val_t json);
el_val_t __json_get(el_val_t json, el_val_t key);
el_val_t __json_get_raw(el_val_t json, el_val_t key);
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
el_val_t __json_parse_map(el_val_t json_str);
el_val_t __json_stringify_val(el_val_t val);
/* Hashing */
el_val_t __sha256_hex(el_val_t s);
/* State K/V */
el_val_t __state_del(el_val_t key);
el_val_t __state_get(el_val_t key);
el_val_t __state_keys(void);
el_val_t __state_set(el_val_t key, el_val_t val);
/* UUID */
el_val_t __uuid_v4(void);
/* Args */
el_val_t __args_json(void);
#ifdef __cplusplus
}
#endif
-2
View File
@@ -990,8 +990,6 @@ el_val_t __json_get(el_val_t json, el_val_t key) { return j
el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); }
el_val_t __json_parse(el_val_t s) { return json_parse(s); }
el_val_t __json_stringify(el_val_t v) { return json_stringify(v); }
el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); }
el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); }
el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); }
el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); }
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); }
-2
View File
@@ -174,8 +174,6 @@ el_val_t __json_get(el_val_t json, el_val_t key);
el_val_t __json_get_raw(el_val_t json_str, el_val_t key);
el_val_t __json_parse(el_val_t s);
el_val_t __json_stringify(el_val_t v);
el_val_t __json_parse_map(el_val_t json_str); /* alias for __json_parse */
el_val_t __json_stringify_val(el_val_t val); /* alias for __json_stringify */
el_val_t __json_array_len(el_val_t json_str);
el_val_t __json_array_get(el_val_t json_str, el_val_t index);
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index);
-16
View File
@@ -324,10 +324,6 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let each_c: String = cg_html_each(child, acc_var)
let parts = native_list_append(parts, each_c)
}
if str_eq(html_kind, "HtmlIf") {
let if_c: String = cg_html_if(child, acc_var)
let parts = native_list_append(parts, if_c)
}
let i = i + 1
}
str_join(parts, "")
@@ -417,17 +413,6 @@ fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
// Generate code for {#if cond} ... {/if} (with optional {#else}).
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
let cond_expr = node["cond"]
let then_children: [Map<String, Any>] = node["then"]
let else_children: [Map<String, Any>] = node["else"]
let cond_c: String = cg_expr(cond_expr)
let then_c: String = cg_html_parts(then_children, acc_var)
let else_c: String = cg_html_parts(else_children, acc_var)
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
}
// Top-level HTML template codegen returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
@@ -3730,7 +3715,6 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
cg_fn(stmt)
el_release(stmt)
el_arena_pop(fn_arena_mark)
el_mem_check()
}
} else {
if is_top_level_decl(stmt) {
+5 -38
View File
@@ -287,9 +287,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 +325,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:
@@ -565,20 +536,16 @@ 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_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)
+94 -497
View File
@@ -293,48 +293,6 @@ fn is_void_element(name: String) -> Bool {
false
}
// Collect all tokens as raw text until </tag_name> is encountered.
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
// Returns { "text": "...", "pos": p_after_closing_tag }
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> Map<String, Any> {
let parts: [String] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// Check if this is </tag_name>
let close_name = tok_value(tokens, p + 2)
if str_eq(close_name, tag_name) {
// consume </tag_name>
let p = p + 3
let p = expect(tokens, p, "Gt")
let running = false
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
}
}
{ "text": str_join(parts, ""), "pos": p }
}
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
@@ -362,7 +320,7 @@ fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
}
}
}
{ "text": str_join(parts, ""), "pos": p }
{ "text": str_join(parts, " "), "pos": p }
}
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
@@ -477,125 +435,77 @@ fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<Strin
}
} else {
if str_eq(k, "LBrace") {
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
// Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
// LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
// {#else} lexes as LBrace Else RBrace.
// Interpolation: {expr} or {#each ...} or {/each}
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// {/each} or {/if} end of block, stop
// skip { /
let p = p + 2
// skip the close-tag name token (each, if, etc.)
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
if str_eq(k2, "If") {
// {#if condition} ... {/if}
// Skip { if (2 tokens; '#' was silently skipped by lexer)
let p = p + 2
// Parse condition expression (no block expr)
if str_eq(k2, "Hash") {
// {#each list as item}
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "each") {
let p = p + 3
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_cond = parse_expr(tokens, p)
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let cond_expr = r_cond["node"]
let p = r_cond["pos"]
el_release(r_cond)
let list_expr = r_list["node"]
let p = r_list["pos"]
// r_list result map fully consumed release to free peak heap.
el_release(r_list)
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse then-children until {#else} or {/if}
let r_then = parse_html_children(tokens, p, "__if_then__")
let then_children = r_then["children"]
let p = r_then["pos"]
el_release(r_then)
// check for {#else} lexed as LBrace Else RBrace
let else_children: [Map<String, Any>] = native_list_empty()
let ck = tok_kind(tokens, p)
if str_eq(ck, "LBrace") {
let ck2 = tok_kind(tokens, p + 1)
if str_eq(ck2, "Else") {
// consume { else }
let p = p + 2
let p = expect(tokens, p, "RBrace")
// parse else-children until {/if}
let r_else = parse_html_children(tokens, p, "__if_else__")
let else_children = r_else["children"]
let p = r_else["pos"]
el_release(r_else)
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
// r_body result map fully consumed release to free peak heap.
el_release(r_body)
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
let p = p + 1
}
} else {
if str_eq(k2, "Slash") {
// {/each} end of each block, stop
// skip {/each}
let p = p + 2
// skip "each"
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
// regular {expr}
let r = parse_expr(tokens, p + 1)
let interp_val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
let children = native_list_append(children, if_node)
} else {
if str_eq(k2, "Else") {
// {#else} sentinel lexed as LBrace Else RBrace
// Do NOT consume leave position for caller ({#if} handler checks for it)
let running = false
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
// Check for {#each list as item} lexed as LBrace Ident:"each" ...
let k2_v = tok_value(tokens, p + 1)
if str_eq(k2_v, "each") {
let p = p + 2
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let list_expr = r_list["node"]
let p = r_list["pos"]
// r_list result map fully consumed release to free peak heap.
el_release(r_list)
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
// r_body result map fully consumed release to free peak heap.
el_release(r_body)
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
// regular {expr} disable map-literal parsing so {fn(a,b)}
// does not trigger the LBracemap path inside parse_primary
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r = parse_expr(tokens, p + 1)
state_set("__no_block_expr", prev_no_block)
let interp_val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
}
@@ -655,27 +565,6 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
if is_void_element(tag_name) {
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// raw-text mode for style/script collect content as plain text without parsing CSS/JS as El
if str_eq(tag_name, "style") {
let r_raw = parse_raw_text_content(tokens, p, "style")
let raw_text: String = r_raw["text"]
let p = r_raw["pos"]
el_release(r_raw)
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
let raw_children: [Map<String, Any>] = native_list_empty()
let raw_children = native_list_append(raw_children, raw_child)
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
}
if str_eq(tag_name, "script") {
let r_raw = parse_raw_text_content(tokens, p, "script")
let raw_text: String = r_raw["text"]
let p = r_raw["pos"]
el_release(r_raw)
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
let raw_children: [Map<String, Any>] = native_list_empty()
let raw_children = native_list_append(raw_children, raw_child)
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
}
// parse children
let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"]
@@ -829,123 +718,44 @@ fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
// as the start of the block they're expecting.
return make_result({ "expr": "Nil" }, pos)
}
// Distinguish map literal from interpolation chain.
// A map literal requires { key: value } the second token inside { must be Colon.
// An empty {} is a map literal. Everything else is an interpolation chain.
let first_k: String = tok_kind(tokens, pos + 1)
let second_k: String = tok_kind(tokens, pos + 2)
if str_eq(first_k, "RBrace") {
// Empty map literal {}
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
}
if str_eq(second_k, "Colon") {
// MAP LITERAL: { key: value, ... }
let p = pos + 1
let pairs: [Map<String, Any>] = native_list_empty()
let running = true
while running {
let k2 = tok_kind(tokens, p)
if k2 == "RBrace" {
let p = pos + 1
let pairs: [Map<String, Any>] = native_list_empty()
let running = true
while running {
let k2 = tok_kind(tokens, p)
if k2 == "RBrace" {
let running = false
} else {
if k2 == "Eof" {
let running = false
} else {
if k2 == "Eof" {
let running = false
// key: Str token
let key = tok_value(tokens, p)
let new_p: Int = p + 1
let new_p = expect(tokens, new_p, "Colon")
let r = parse_expr(tokens, new_p)
let val_node = r["node"]
let new_p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let pair = { "key": key, "value": val_node }
let pairs = native_list_append(pairs, pair)
let k3 = tok_kind(tokens, new_p)
if k3 == "Comma" {
let new_p = new_p + 1
}
// Non-progress guard: malformed map content can leave
// parse_expr returning the same pos. Force advance.
if new_p <= p {
let p = p + 1
} else {
// key: Str or Ident token
let key = tok_value(tokens, p)
let new_p: Int = p + 1
let new_p = expect(tokens, new_p, "Colon")
let r = parse_expr(tokens, new_p)
let val_node = r["node"]
let new_p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let pair = { "key": key, "value": val_node }
let pairs = native_list_append(pairs, pair)
let k3 = tok_kind(tokens, new_p)
if k3 == "Comma" {
let new_p = new_p + 1
}
// Non-progress guard: malformed map content can leave
// parse_expr returning the same pos. Force advance.
if new_p <= p {
let p = p + 1
} else {
let p = new_p
}
let p = new_p
}
}
}
let p = expect(tokens, p, "RBrace")
return make_result({ "expr": "Map", "pairs": pairs }, p)
}
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
// Build a BinOp(Plus, ...) concatenation chain.
let p = pos
let chain_node: Map<String, Any> = { "expr": "Nil" }
let chain_started = false
let chain_running = true
while chain_running {
let ck: String = tok_kind(tokens, p)
if str_eq(ck, "LBrace") {
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r = parse_expr(tokens, p + 1)
state_set("__no_block_expr", prev_no_block)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
if str_eq(ck, "Lt") {
let ck2: String = tok_kind(tokens, p + 1)
if str_eq(ck2, "Not") {
let r = parse_html_template(tokens, p)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
if str_eq(ck2, "Ident") {
let tag_candidate: String = tok_value(tokens, p + 1)
if is_html_tag_name(tag_candidate) {
let r = parse_html_template(tokens, p)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
let chain_running = false
}
} else {
let chain_running = false
}
}
} else {
let chain_running = false
}
}
}
return make_result(chain_node, p)
let p = expect(tokens, p, "RBrace")
return make_result({ "expr": "Map", "pairs": pairs }, p)
}
// if expression
@@ -2065,219 +1875,6 @@ fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
p
}
// scan_type_el read a type annotation starting at pos and return its El
// source representation as a string, plus the new position.
// Returns { "el": String, "pos": Int }.
// Handles: Ident, [Type], Map<K,V>, Type?, Type<T,...> (same shapes as skip_type).
fn scan_type_el(tokens: [Any], pos: Int) -> Map<String, Any> {
let k: String = tok_kind(tokens, pos)
// Array type: [Type]
if str_eq(k, "LBracket") {
let p: Int = pos + 1
let inner = scan_type_el(tokens, p)
let inner_str: String = inner["el"]
let p = inner["pos"]
el_release(inner)
let p = expect(tokens, p, "RBracket")
return { "el": "[" + inner_str + "]", "pos": p }
}
// Named type (possibly generic or optional)
if str_eq(k, "Ident") {
let name: String = tok_value(tokens, pos)
let p: Int = pos + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Lt") {
// Generic params: collect until matching >
let p = p + 1
let depth: Int = 1
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, name + "<")
let running: Bool = true
while running {
let kk: String = tok_kind(tokens, p)
if str_eq(kk, "Eof") {
let running = false
} else {
if str_eq(kk, "Lt") {
let depth = depth + 1
let parts = native_list_append(parts, "<")
let p = p + 1
} else {
if str_eq(kk, "Gt") {
let depth = depth - 1
let p = p + 1
if depth <= 0 {
let parts = native_list_append(parts, ">")
let running = false
} else {
let parts = native_list_append(parts, ">")
}
} else {
if str_eq(kk, "Comma") {
let parts = native_list_append(parts, ", ")
let p = p + 1
} else {
let parts = native_list_append(parts, tok_value(tokens, p))
let p = p + 1
}
}
}
}
}
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "QuestionMark") { let p = p + 1 }
let result: String = str_join(parts, "")
el_release(parts)
return { "el": result, "pos": p }
}
// Optional marker
if str_eq(k2, "QuestionMark") {
return { "el": name + "?", "pos": p + 1 }
}
return { "el": name, "pos": p }
}
// Fallback: unknown token, treat as Any
{ "el": "Any", "pos": pos + 1 }
}
// scan_params_el scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the El parameter
// declaration string (e.g. "a: String, b: Int") along with the new position.
// Returns { "el": String, "pos": Int }.
// Used by scan_fn_sigs_el for --emit-header without building full AST.
fn scan_params_el(tokens: [Any], pos: Int) -> Map<String, Any> {
let p: Int = expect(tokens, pos, "LParen")
let parts: [String] = native_list_empty()
let going: Bool = true
while going {
let kk: String = tok_kind(tokens, p)
if str_eq(kk, "RParen") {
let going = false
} else {
if str_eq(kk, "Eof") {
let going = false
} else {
let pname: String = tok_value(tokens, p)
let p = p + 1
let p = expect(tokens, p, "Colon")
let tr = scan_type_el(tokens, p)
let ptype: String = tr["el"]
let p = tr["pos"]
el_release(tr)
let parts = native_list_append(parts, pname + ": " + ptype)
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Comma") {
let p = p + 1
}
}
}
}
let p = expect(tokens, p, "RParen")
let el_str: String = str_join(parts, ", ")
el_release(parts)
{ "el": el_str, "pos": p }
}
// scan_fn_sigs_el lightweight token-level pre-scan for --emit-header.
//
// Like scan_fn_sigs but captures El-style type strings instead of C types.
// Only records fn/extern_fn entries (header generation ignores lets/blocks).
//
// Descriptor shape:
// { "kind": "fn"|"extern_fn", "name": String,
// "params_el": String, <- El param list, e.g. "a: String, b: Int"
// "ret_el": String } <- El return type, e.g. "String" or "Void"
//
// Peak memory: O(tokens) with no expression AST allocation.
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
let total: Int = native_list_len(tokens) / 2
let sigs: [Map<String, Any>] = native_list_empty()
let pos: Int = 0
let going: Bool = true
while going {
if pos >= total {
let going = false
} else {
let k: String = tok_kind(tokens, pos)
if str_eq(k, "Eof") {
let going = false
} else {
// --- fn definition ---
if str_eq(k, "Fn") {
let p: Int = pos + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let pr = scan_params_el(tokens, p)
let params_el: String = pr["el"]
let p = pr["pos"]
el_release(pr)
// read return type
let ret_el: String = "Any"
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Arrow") {
let p = p + 1
let tr = scan_type_el(tokens, p)
let ret_el = tr["el"]
let p = tr["pos"]
el_release(tr)
}
// skip body
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "LBrace") {
let p = skip_to_rbrace(tokens, p)
}
if !str_eq(name, "main") {
let sigs = native_list_append(sigs, {
"kind": "fn",
"name": name,
"params_el": params_el,
"ret_el": ret_el
})
}
let pos = p
} else {
// --- extern fn ---
if str_eq(k, "Extern") {
let p: Int = pos + 1
let k2: String = tok_kind(tokens, p)
if str_eq(k2, "Fn") {
let p = p + 1
let name: String = tok_value(tokens, p)
let p = p + 1
let pr = scan_params_el(tokens, p)
let params_el: String = pr["el"]
let p = pr["pos"]
el_release(pr)
let ret_el: String = "Any"
let k3: String = tok_kind(tokens, p)
if str_eq(k3, "Arrow") {
let p = p + 1
let tr = scan_type_el(tokens, p)
let ret_el = tr["el"]
let p = tr["pos"]
el_release(tr)
}
let sigs = native_list_append(sigs, {
"kind": "extern_fn",
"name": name,
"params_el": params_el,
"ret_el": ret_el
})
let pos = p
} else {
let pos = pos + 1
}
} else {
// Let, Cgi, Service, Import, Type, Enum, From skip to boundary.
let p: Int = pos + 1
let p = skip_expr_to_stmt_boundary(tokens, p)
let pos = p
}}}
}
}
sigs
}
// scan_params_c scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the C parameter
// declaration string along with the new position.
+2 -53
View File
@@ -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)
@@ -298,21 +271,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 -fbracket-depth=1024 -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 +279,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 -lm")
let parts = native_list_append(parts, "-o " + out_bin)
let cmd: String = str_join(parts, " ")
println(" link " + out_bin)
@@ -353,7 +312,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)
@@ -432,15 +390,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)
-10062
View File
File diff suppressed because it is too large Load Diff
+3 -43
View File
@@ -6,55 +6,15 @@
//
// Dependencies: runtime/string.el, runtime/json.el
// --- Validation (defense in depth) ---
// el_val_t is an untyped machine word, so a wrong TYPE can't be caught here but a
// wrong VALUE can (a tier in the node_type slot, an empty/garbage string, an int, a
// path, a model name, a cgi id). Reject loudly instead of silently writing junk.
fn engram_valid_node_type(t: String) -> Bool {
return str_eq(t, "Memory") || str_eq(t, "Knowledge") || str_eq(t, "Belief")
|| str_eq(t, "Project") || str_eq(t, "Tag") || str_eq(t, "BacklogItem")
|| str_eq(t, "Artifact") || str_eq(t, "Conversation") || str_eq(t, "ExecutionContext")
|| str_eq(t, "InternalStateEvent") || str_eq(t, "Self") || str_eq(t, "Entity")
|| str_eq(t, "Process") || str_eq(t, "ConfigEntry") || str_eq(t, "Concept") || str_eq(t, "Imprint")
|| str_eq(t, "SessionSummary")
}
fn engram_valid_tier(t: String) -> Bool {
return str_eq(t, "Semantic") || str_eq(t, "Episodic") || str_eq(t, "Working")
|| str_eq(t, "Procedural") || str_eq(t, "Canonical") || str_eq(t, "Note") || str_eq(t, "Lesson")
}
// --- Node creation ---
fn engram_node(content: String, node_type: String, salience: Float) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "'")
return ""
}
return __engram_node(content, node_type, salience)
}
// Signature MUST match the C primitive __engram_node_full exactly (el_seed.h):
// (content, node_type, label, salience, importance, confidence, tier, tags)
// The previous wrapper declared a stale 8-arg schema with wrong names AND types
// (sal:Float at the label slot, ts:Int at the tier slot). Because el_val_t is an
// untyped machine word, the EL compiler coerced caller args to those wrong param
// types and then forwarded them BY POSITION into the C function so tier received
// an int, importance/confidence received strings, label received a float, etc.
// That is the field-corruption bug. Match the contract 1:1 no coercion, no reorder.
fn engram_node_full(content: String, node_type: String, label: String,
salience: Float, importance: Float, confidence: Float,
tier: String, tags: String) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "' (label=" + label + ")")
return ""
}
if !engram_valid_tier(tier) {
__println("[engram] REJECTED node write — invalid tier '" + tier + "' (node_type=" + node_type + ", label=" + label + ")")
return ""
}
return __engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
source: String, lang: String, ts: Int, tags: String) -> String {
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
}
// --- Node retrieval ---
+1 -1
View File
@@ -44,7 +44,7 @@ run_runtime_case() {
fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
-lcurl -lpthread -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
echo "FAIL ${name} — cc failed:"
cat /tmp/cal_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1))
+1 -1
View File
@@ -26,7 +26,7 @@ echo "==> Compiling runner.el via ${ELC}"
echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o "${OUT_BIN}"
-lcurl -lpthread -o "${OUT_BIN}"
echo "==> Running"
"${OUT_BIN}"
+1 -1
View File
@@ -42,7 +42,7 @@ run_runtime_case() {
fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/text_test.cc.err; then
-lcurl -lpthread -o "${out_bin}" 2>/tmp/text_test.cc.err; then
echo "FAIL ${name} — cc failed:"
cat /tmp/text_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1))
+2 -2
View File
@@ -55,7 +55,7 @@ run_runtime_case() {
fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/time_test.cc.err; then
-lcurl -lpthread -o "${out_bin}" 2>/tmp/time_test.cc.err; then
echo "FAIL ${name} — cc failed:"
cat /tmp/time_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1))
@@ -116,7 +116,7 @@ run_typeerror_case() {
fi
if cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then
-lcurl -lpthread -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then
echo "FAIL ${name} — cc unexpectedly succeeded; type rule did not fire"
FAIL=$((FAIL+1))
FAILED_NAMES+=("${name}")
+9
View File
@@ -10,6 +10,15 @@
// export PATH="$HOME/.el/bin:$PATH"
// export EL_HOME="$HOME/.el"
// Imports
import "../../runtime/string.el"
import "../../runtime/env.el"
import "../../runtime/fs.el"
import "../../runtime/exec.el"
import "../../runtime/json.el"
import "../../runtime/http.el"
// Constants
fn gitea_releases_url() -> String {
Generated
+897
View File
@@ -0,0 +1,897 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cc"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "el-aop"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "el-auth"
version = "0.1.0"
dependencies = [
"base64",
"el-identity",
"hmac",
"sha2",
"thiserror",
"uuid",
]
[[package]]
name = "el-config"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
"toml",
]
[[package]]
name = "el-i18n"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
"toml",
]
[[package]]
name = "el-identity"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"hmac",
"serde",
"serde_json",
"sha2",
"thiserror",
"uuid",
]
[[package]]
name = "el-layout"
version = "0.1.0"
dependencies = [
"el-style",
"thiserror",
]
[[package]]
name = "el-platform"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "el-publish"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "el-secrets"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "el-services"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "el-style"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "el-ui-compiler"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.0",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "profile-card"
version = "0.1.0"
dependencies = [
"el-config",
"el-i18n",
"el-layout",
"el-secrets",
"el-style",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom",
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+18
View File
@@ -0,0 +1,18 @@
[workspace]
members = [
"vessels/el-ui-compiler",
"vessels/el-platform",
"vessels/el-services",
"vessels/el-aop",
"vessels/el-auth",
"vessels/el-publish",
"vessels/el-identity",
"vessels/el-style",
"vessels/el-layout",
"vessels/el-i18n",
"vessels/el-config",
"vessels/el-secrets",
"vessels/el-html",
"examples/profile-card",
]
resolver = "2"
+350
View File
@@ -0,0 +1,350 @@
//! Profile card example — demonstrates el-ui styling, layout, i18n, config, and secrets.
//!
//! This example shows what building with el-ui looks like:
//!
//! - Styling via semantic tokens and the StyleModifier trait
//! - Responsive layout: VStack/HStack that wrap automatically
//! - Localization via LocaleContext and t()/t_plural()
//! - Configuration from el.toml / env vars
//! - Secrets that never appear in logs
use std::collections::HashMap;
use el_config::prelude::*;
use el_i18n::prelude::*;
use el_layout::prelude::*;
use el_secrets::prelude::*;
use el_style::prelude::*;
// --- Domain model ---
struct UserProfile {
handle: String,
display_name: String,
bio: String,
follower_count: i64,
following_count: i64,
is_verified: bool,
is_following: bool,
}
// --- Profile card component ---
/// A profile card component.
///
/// In a real el-ui app, this would be a .el component file compiled by
/// el-ui-compiler. Here we show the same patterns in pure Rust so the
/// example is self-contained and runnable.
struct ProfileCard {
profile: UserProfile,
theme: Theme,
style: StyleSet,
locale: LocaleContext,
}
impl StyleModifier for ProfileCard {
fn style_mut(&mut self) -> &mut StyleSet {
&mut self.style
}
}
impl ProfileCard {
fn new(profile: UserProfile, theme: Theme, locale: LocaleContext) -> Self {
Self {
profile,
theme,
style: StyleSet::default(),
locale,
}
}
/// "Render" the card — in a real app the backend converts this to
/// native views. Here we produce a human-readable description.
fn render(&self) -> String {
let t = &self.locale;
let theme = &self.theme;
// --- Header HStack (avatar + name + verified badge) ---
// HStack wraps automatically if the container is narrow (mobile-first)
let header = HStack::new()
.spacing(12)
.alignment(VAlign::Center)
.wrap(true);
// --- Name text: Title style ---
let name_style = theme.typography.resolve(&TextStyle::Title);
// --- Body text: Body style ---
let body_style = theme.typography.resolve(&TextStyle::Body);
// --- Stats HStack (followers / following) ---
let _stats_layout = HStack::new().spacing(24).wrap(true);
// --- Follow button ---
// VStack wraps children that don't fit, so this works on any screen width
let _card_layout = VStack::new()
.spacing(16)
.alignment(HAlign::Leading)
.wrap(true);
// --- Localized strings ---
let follow_label = if self.profile.is_following {
t.t("profile.following")
} else {
t.t("profile.follow")
};
let followers_label =
t.t_plural("profile.followers", self.profile.follower_count);
let following_label =
t.t_plural("profile.following_count", self.profile.following_count);
// --- Color resolution ---
let (bg_r, bg_g, bg_b, _) = theme.colors.resolve(&Color::Surface);
let (text_r, text_g, text_b, _) = theme.colors.resolve(&Color::OnSurface);
let (primary_r, primary_g, primary_b, _) = theme.colors.resolve(&Color::Primary);
// --- Shadow ---
let shadow_css = theme
.shadows
.resolve(&Shadow::Md)
.map(|s| s.to_css())
.unwrap_or_default();
// --- Formatted stats (locale-aware numbers) ---
let follower_count_fmt = format_integer(self.profile.follower_count, &self.locale.locale);
let following_count_fmt = format_integer(self.profile.following_count, &self.locale.locale);
// --- RTL layout signal ---
let layout_direction = if self.locale.is_rtl() { "rtl" } else { "ltr" };
// --- Build the output ---
format!(
r#"
ProfileCard
Layout direction: {}
Theme mode: {:?}
[Card background: rgb({},{},{}), shadow: {}]
{} {} [Header HStack, spacing=12, wrap=true]
Avatar [44×44pt, radius=Full meets touch target]
{} [font: {}pt weight={}, color: rgb({},{},{})]
{} [verified badge]
{} [Body style, {}pt, color: rgb({},{},{})]
[Stats HStack, spacing=24, wrap=true]
{} {} follower count (locale-formatted)
{} {} following count
[Button: Primary variant]
{} [color: rgb({},{},{})]
[Card ends]
"#,
layout_direction,
theme.mode,
bg_r, bg_g, bg_b,
shadow_css,
header.spacing,
if header.wrap { "wrap=true" } else { "wrap=false" },
self.profile.display_name,
name_style.size,
name_style.weight.value(),
text_r, text_g, text_b,
if self.profile.is_verified { "" } else { "" },
self.profile.bio,
body_style.size,
text_r, text_g, text_b,
follower_count_fmt,
followers_label,
following_count_fmt,
following_label,
follow_label,
primary_r, primary_g, primary_b,
)
}
}
// --- Application entry point ---
fn main() {
// 1. Configuration — layered, typed
let el_toml = r#"
[config]
app.name = "ProfileCard Example"
app.version = "1.0.0"
profile.max_bio_length = "160"
[env.development]
app.debug = "true"
"#;
let toml_source = load_from_toml(el_toml, &Environment::Development)
.expect("el.toml should be valid");
let mut config = Config::new(Environment::Development);
config.push_source(Box::new(toml_source));
let app_name = config.get::<String>("app.name").unwrap_or_default();
let app_version = config.get::<String>("app.version").unwrap_or_default();
let max_bio: u32 = config.get_or("profile.max_bio_length", 160u32);
let debug: bool = config.get_or("app.debug", false);
println!("=== {} v{} ===", app_name, app_version);
println!("Environment: {}", config.environment);
println!("Debug mode: {}", debug);
println!("Max bio: {} chars", max_bio);
// 2. Secrets — loaded at startup, never logged
let mut secret_src = InMemorySource::new();
secret_src.insert("analytics.key", "ana_abc123xyz");
let secrets = SecretsResolver::new()
.source(Box::new(secret_src))
.require("analytics.key")
.resolve()
.expect("required secrets must be present at startup");
let analytics_key = secrets.require("analytics.key");
// This will always print [REDACTED] — never the actual key
println!("Analytics key: {} (safely [REDACTED] in logs)", analytics_key);
// To actually use it:
let _actual_key: &str = analytics_key.expose();
// 3. Localization — English
let mut en_bundle = LocaleBundle::new(Locale::en_us());
en_bundle.insert("profile.follow", "Follow");
en_bundle.insert("profile.following", "Following");
let mut forms = HashMap::new();
forms.insert("one".to_string(), "{n} Follower".to_string());
forms.insert("other".to_string(), "{n} Followers".to_string());
en_bundle.insert_plural("profile.followers", forms);
let mut following_forms = HashMap::new();
following_forms.insert("one".to_string(), "{n} Following".to_string());
following_forms.insert("other".to_string(), "{n} Following".to_string());
en_bundle.insert_plural("profile.following_count", following_forms);
let en_ctx = LocaleContext::new(Locale::en_us(), en_bundle);
// 4. Theme — light, system colors
let light_theme = Theme::default_light();
let dark_theme = Theme::default_dark();
// 5. Profile data
let profile = UserProfile {
handle: "alice".to_string(),
display_name: "Alice Chen".to_string(),
bio: "Building beautiful things with el-ui. Rust enthusiast.".to_string(),
follower_count: 12_483,
following_count: 342,
is_verified: true,
is_following: false,
};
// 6. Render the card (light theme, English)
println!("\n=== Light Theme, English ===");
let card = ProfileCard::new(
UserProfile {
handle: profile.handle.clone(),
display_name: profile.display_name.clone(),
bio: profile.bio.clone(),
follower_count: profile.follower_count,
following_count: profile.following_count,
is_verified: profile.is_verified,
is_following: profile.is_following,
},
light_theme,
en_ctx.clone(),
);
// Apply style modifiers — fluent, zero-cost at compile time
let styled_card = card
.padding(16)
.background(Color::Surface)
.radius(Radius::Lg)
.shadow(Shadow::Md)
.max_width(Dimension::Fixed(480));
println!("{}", styled_card.render());
// 7. Render with dark theme
println!("=== Dark Theme, English ===");
let card_dark = ProfileCard::new(
UserProfile {
handle: profile.handle.clone(),
display_name: profile.display_name.clone(),
bio: profile.bio.clone(),
follower_count: 1, // test singular
following_count: profile.following_count,
is_verified: profile.is_verified,
is_following: true,
},
dark_theme,
en_ctx,
);
let styled_dark = card_dark
.padding(16)
.background(Color::Surface)
.radius(Radius::Lg)
.shadow(Shadow::Lg);
println!("{}", styled_dark.render());
// 8. Layout demonstration
println!("=== Layout Engine Demo ===");
// Grid: auto columns — picks 1, 2, 3... based on container width
let grid = GridLayout::new().columns_auto(200.0).gap(16);
for width in [300.0f32, 600.0, 900.0, 1200.0] {
println!(
" Container {}px → {} columns",
width,
grid.active_columns(width)
);
}
// Responsive value
let cols: Responsive<u32> = Responsive::fixed(1).md(2).lg(3);
println!("\n Responsive columns:");
for bp in [
Breakpoint::Base,
Breakpoint::Sm,
Breakpoint::Md,
Breakpoint::Lg,
Breakpoint::Xl,
] {
println!(" {:?}: {} col(s)", bp, cols.resolve(bp));
}
// Platform sizing
let ios_sizing = PlatformSizing::for_platform(PlatformFamily::Ios);
let android_sizing = PlatformSizing::for_platform(PlatformFamily::Android);
println!("\n Min touch targets:");
println!(" iOS: {}pt", ios_sizing.min_touch_target);
println!(" Android: {}dp", android_sizing.min_touch_target);
// 9. Locale formatting
println!("\n=== Locale-Aware Formatting ===");
let number = 1_234_567.89;
for (tag, locale) in [
("en-US", Locale::en_us()),
("de-DE", Locale::new("de-DE")),
("fr-FR", Locale::fr_fr()),
("ja-JP", Locale::ja()),
] {
let formatted = format_number(number, &locale, 2);
let currency = format_currency(1234.56, &locale, "USD");
println!(" {}: {} | {}", tag, formatted, currency);
}
// 10. RTL detection
println!("\n=== RTL Detection ===");
for tag in ["en-US", "ar-SA", "he", "fa", "zh-TW"] {
let locale = Locale::new(tag);
println!(" {}: {}", tag, if locale.is_rtl() { "RTL" } else { "LTR" });
}
println!("\nAll systems operational. el-ui is ready.");
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "el-aop"
version = "0.1.0"
edition = "2021"
description = "el-ui aspect-oriented programming — cross-cutting concerns as first-class features"
license = "MIT"
[lib]
name = "el_aop"
path = "src/lib.rs"
[dependencies]
thiserror = "1"
[dev-dependencies]
+490
View File
@@ -0,0 +1,490 @@
//! Built-in aspects for el-ui.
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Mutex,
},
time::{Duration, Instant},
};
use crate::{AopError, AopResult, Aspect, InvocationContext, InvocationResult, ProceedFn};
// ── @authenticate ─────────────────────────────────────────────────────────────
/// `@authenticate` — Requires a valid session before the method executes.
///
/// Checks `ctx.metadata["session_token"]` or `ctx.metadata["user_id"]`.
/// If absent, rejects with `AopError::Unauthenticated`.
pub struct AuthenticateAspect;
impl Aspect for AuthenticateAspect {
fn name(&self) -> &'static str {
"authenticate"
}
fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
// Look for a session token or user ID in metadata.
// In production, the auth middleware populates these from the JWT/session.
let has_user = ctx.metadata.contains_key("user_id")
|| ctx.metadata.contains_key("session_token");
if !has_user {
return Err(AopError::Unauthenticated);
}
Ok(())
}
}
// ── @authorize ────────────────────────────────────────────────────────────────
/// `@authorize(role: "admin")` — Requires the caller to have a specific role.
pub struct AuthorizeAspect {
pub required_role: String,
}
impl AuthorizeAspect {
pub fn new(role: impl Into<String>) -> Self {
Self { required_role: role.into() }
}
}
impl Aspect for AuthorizeAspect {
fn name(&self) -> &'static str {
"authorize"
}
fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
let user_roles = ctx
.metadata
.get("roles")
.map(|s| s.as_str())
.unwrap_or("");
// Roles are stored as comma-separated string: "admin,user"
let has_role = user_roles
.split(',')
.any(|r| r.trim() == self.required_role.as_str());
if !has_role {
return Err(AopError::Forbidden {
role: self.required_role.clone(),
});
}
Ok(())
}
}
// ── @cache ────────────────────────────────────────────────────────────────────
struct CacheEntry {
value: InvocationResult,
inserted_at: Instant,
ttl: Duration,
}
impl CacheEntry {
fn is_expired(&self) -> bool {
self.inserted_at.elapsed() > self.ttl
}
}
/// `@cache(ttl: 300)` — Cache method responses for `ttl` seconds.
///
/// Cache key is `"target::method::{args_sorted_json}"`.
pub struct CacheAspect {
pub ttl: Duration,
cache: Mutex<HashMap<String, CacheEntry>>,
}
impl CacheAspect {
pub fn new(ttl_seconds: u64) -> Self {
Self {
ttl: Duration::from_secs(ttl_seconds),
cache: Mutex::new(HashMap::new()),
}
}
fn cache_key(ctx: &InvocationContext) -> String {
let mut pairs: Vec<(&String, &String)> = ctx.args.iter().collect();
pairs.sort_by_key(|(k, _)| k.as_str());
let args = pairs
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",");
format!("{}::{}::{}", ctx.target, ctx.method, args)
}
}
impl Aspect for CacheAspect {
fn name(&self) -> &'static str {
"cache"
}
fn around(
&self,
ctx: InvocationContext,
proceed: &ProceedFn,
) -> AopResult<InvocationResult> {
let key = Self::cache_key(&ctx);
// Check cache
{
let cache = self.cache.lock().expect("cache lock poisoned");
if let Some(entry) = cache.get(&key) {
if !entry.is_expired() {
return Ok(entry.value.clone());
}
}
}
// Cache miss — proceed and store result
let result = proceed(ctx)?;
{
let mut cache = self.cache.lock().expect("cache lock poisoned");
// Evict expired entries while we're here
cache.retain(|_, v| !v.is_expired());
cache.insert(
key,
CacheEntry {
value: result.clone(),
inserted_at: Instant::now(),
ttl: self.ttl,
},
);
}
Ok(result)
}
}
// ── @rate_limit ───────────────────────────────────────────────────────────────
struct RateWindow {
count: u32,
window_start: Instant,
window_duration: Duration,
}
/// `@rate_limit(requests: 100, per: 60)` — Allow at most `requests` calls per `per` seconds.
pub struct RateLimitAspect {
pub max_requests: u32,
pub window: Duration,
state: Mutex<HashMap<String, RateWindow>>,
}
impl RateLimitAspect {
pub fn new(max_requests: u32, per_seconds: u64) -> Self {
Self {
max_requests,
window: Duration::from_secs(per_seconds),
state: Mutex::new(HashMap::new()),
}
}
/// The rate-limit key for a caller. Uses `user_id` or "anonymous".
fn caller_key(ctx: &InvocationContext) -> String {
ctx.metadata
.get("user_id")
.cloned()
.unwrap_or_else(|| "anonymous".to_string())
}
}
impl Aspect for RateLimitAspect {
fn name(&self) -> &'static str {
"rate_limit"
}
fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
let key = Self::caller_key(ctx);
let mut state = self.state.lock().expect("rate limit lock poisoned");
let now = Instant::now();
let window = state.entry(key).or_insert(RateWindow {
count: 0,
window_start: now,
window_duration: self.window,
});
// Reset window if expired
if now.duration_since(window.window_start) >= window.window_duration {
window.count = 0;
window.window_start = now;
}
if window.count >= self.max_requests {
return Err(AopError::RateLimited {
requests: self.max_requests,
per: self.window.as_secs(),
});
}
window.count += 1;
Ok(())
}
}
// ── @log ──────────────────────────────────────────────────────────────────────
/// `@log(level: "info")` — Structured logging for every method call.
pub struct LogAspect {
pub level: String,
}
impl LogAspect {
pub fn new(level: impl Into<String>) -> Self {
Self { level: level.into() }
}
}
impl Aspect for LogAspect {
fn name(&self) -> &'static str {
"log"
}
fn around(
&self,
ctx: InvocationContext,
proceed: &ProceedFn,
) -> AopResult<InvocationResult> {
// In production: use the `tracing` crate with the appropriate level macro.
let _log_entry = format!(
"[{}] {}.{}({:?})",
self.level.to_uppercase(),
ctx.target,
ctx.method,
ctx.args
);
let result = proceed(ctx.clone());
let _log_result = match &result {
Ok(r) => format!("[{}] {}.{} → ok: {}", self.level.to_uppercase(), ctx.target, ctx.method, r.value),
Err(e) => format!("[ERROR] {}.{} → err: {}", ctx.target, ctx.method, e),
};
result
}
}
// ── @validate ─────────────────────────────────────────────────────────────────
/// `@validate` — Run input validation before the method executes.
///
/// Validation rules are registered per method. If no rules are registered,
/// the aspect passes through (fail-open for ease of adoption).
pub struct ValidateAspect {
/// `"target::method"` → list of validation rules (field, rule_name)
rules: Mutex<HashMap<String, Vec<(String, String)>>>,
}
impl ValidateAspect {
pub fn new() -> Self {
Self {
rules: Mutex::new(HashMap::new()),
}
}
/// Add a validation rule. `rule` is one of: "required", "email", "min:N", "max:N".
pub fn add_rule(
&self,
target: &str,
method: &str,
field: impl Into<String>,
rule: impl Into<String>,
) {
let key = format!("{}::{}", target, method);
self.rules
.lock()
.expect("validate lock poisoned")
.entry(key)
.or_default()
.push((field.into(), rule.into()));
}
fn validate_field(value: &str, rule: &str) -> AopResult<()> {
if rule == "required" && value.trim().is_empty() {
return Err(AopError::ValidationFailed("field is required".into()));
}
if rule == "email" && !value.contains('@') {
return Err(AopError::ValidationFailed(format!(
"'{}' is not a valid email",
value
)));
}
if let Some(min_str) = rule.strip_prefix("min:") {
let min: usize = min_str.parse().unwrap_or(0);
if value.len() < min {
return Err(AopError::ValidationFailed(format!(
"minimum length is {}",
min
)));
}
}
if let Some(max_str) = rule.strip_prefix("max:") {
let max: usize = max_str.parse().unwrap_or(usize::MAX);
if value.len() > max {
return Err(AopError::ValidationFailed(format!(
"maximum length is {}",
max
)));
}
}
Ok(())
}
}
impl Default for ValidateAspect {
fn default() -> Self {
Self::new()
}
}
impl Aspect for ValidateAspect {
fn name(&self) -> &'static str {
"validate"
}
fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
let key = format!("{}::{}", ctx.target, ctx.method);
let rules = self.rules.lock().expect("validate lock poisoned");
if let Some(field_rules) = rules.get(&key) {
for (field, rule) in field_rules {
let value = ctx.args.get(field).map(|s| s.as_str()).unwrap_or("");
Self::validate_field(value, rule)?;
}
}
Ok(())
}
}
// ── @retry ────────────────────────────────────────────────────────────────────
/// `@retry(attempts: 3, backoff: "exponential")` — Retry on failure.
pub struct RetryAspect {
pub attempts: u32,
pub backoff: BackoffStrategy,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BackoffStrategy {
None,
Fixed(Duration),
Exponential { base: Duration },
}
impl RetryAspect {
pub fn new(attempts: u32) -> Self {
Self { attempts, backoff: BackoffStrategy::None }
}
pub fn with_exponential_backoff(mut self, base_ms: u64) -> Self {
self.backoff = BackoffStrategy::Exponential {
base: Duration::from_millis(base_ms),
};
self
}
pub fn with_fixed_backoff(mut self, ms: u64) -> Self {
self.backoff = BackoffStrategy::Fixed(Duration::from_millis(ms));
self
}
fn sleep_duration(&self, attempt: u32) -> Duration {
match &self.backoff {
BackoffStrategy::None => Duration::ZERO,
BackoffStrategy::Fixed(d) => *d,
BackoffStrategy::Exponential { base } => {
// base * 2^attempt, capped at 30s
let factor = 1u64 << attempt.min(10);
std::cmp::min(*base * factor as u32, Duration::from_secs(30))
}
}
}
}
impl Aspect for RetryAspect {
fn name(&self) -> &'static str {
"retry"
}
fn around(
&self,
ctx: InvocationContext,
proceed: &ProceedFn,
) -> AopResult<InvocationResult> {
let mut last_error = String::new();
for attempt in 0..self.attempts {
match proceed(ctx.clone()) {
Ok(result) => return Ok(result),
Err(e) => {
last_error = e.to_string();
let sleep_for = self.sleep_duration(attempt);
if sleep_for > Duration::ZERO && attempt + 1 < self.attempts {
std::thread::sleep(sleep_for);
}
}
}
}
Err(AopError::RetriesExhausted {
attempts: self.attempts,
last_error,
})
}
}
// ── @trace ────────────────────────────────────────────────────────────────────
static TRACE_COUNTER: AtomicU64 = AtomicU64::new(1);
/// `@trace` — Add a distributed tracing span to every method call.
///
/// Injects a `trace_id` and `span_id` into context metadata.
/// In production, emit the span to an OpenTelemetry collector.
pub struct TraceAspect {
pub service_name: String,
}
impl TraceAspect {
pub fn new(service_name: impl Into<String>) -> Self {
Self { service_name: service_name.into() }
}
fn new_span_id() -> String {
let id = TRACE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("span-{:016x}", id)
}
}
impl Aspect for TraceAspect {
fn name(&self) -> &'static str {
"trace"
}
fn around(
&self,
mut ctx: InvocationContext,
proceed: &ProceedFn,
) -> AopResult<InvocationResult> {
// Create or inherit trace ID
let trace_id = ctx
.metadata
.get("trace_id")
.cloned()
.unwrap_or_else(|| format!("trace-{:016x}", TRACE_COUNTER.load(Ordering::Relaxed)));
let span_id = Self::new_span_id();
ctx.metadata.insert("trace_id".into(), trace_id.clone());
ctx.metadata.insert("span_id".into(), span_id.clone());
let start = Instant::now();
let result = proceed(ctx.clone());
let duration_us = start.elapsed().as_micros();
// In production: emit span to OpenTelemetry:
// tracer.start_with_context("method_call", parent_cx)
// .set_attribute(KeyValue::new("service", self.service_name.clone()))
// .set_attribute(KeyValue::new("method", ctx.method.clone()))
// .set_attribute(KeyValue::new("duration_us", duration_us as i64))
// .end();
let _ = duration_us;
result.map(|mut r| {
r.metadata.insert("trace_id".into(), trace_id);
r.metadata.insert("span_id".into(), span_id);
r
})
}
}
+122
View File
@@ -0,0 +1,122 @@
//! Aspect chain — ordered execution of aspects around a method call.
//!
//! Aspects execute in order: each one wraps the next, forming a chain.
//! The innermost item is the actual method invocation.
//!
//! ```text
//! @authenticate → @authorize → @cache → @log → [method body]
//! before before check log
//! hit? ──→ return cached
//! miss? → [method body] → store → after-log
//! ```
//!
//! ## Security-by-default
//!
//! `AspectChain::with_default_auth()` prepends `AuthenticateAspect` to every
//! chain. Call this when building chains for non-`@public` functions.
use crate::{aspects::AuthenticateAspect, AopResult, Aspect, InvocationContext, InvocationResult, ProceedFn};
use std::sync::Arc;
/// An ordered chain of aspects applied to a single method.
pub struct AspectChain {
aspects: Vec<Arc<dyn Aspect>>,
}
impl AspectChain {
pub fn new() -> Self {
Self { aspects: Vec::new() }
}
/// Add an aspect to the end of the chain.
pub fn add(mut self, aspect: Arc<dyn Aspect>) -> Self {
self.aspects.push(aspect);
self
}
/// Number of aspects in this chain.
pub fn len(&self) -> usize {
self.aspects.len()
}
pub fn is_empty(&self) -> bool {
self.aspects.is_empty()
}
/// Execute the chain around the given proceed function.
///
/// Aspects run in order (left to right in the decorator list).
/// Each aspect's `around` method receives the next aspect's `around`
/// as the `proceed` function, forming a true onion model.
pub fn execute(
&self,
ctx: InvocationContext,
proceed: ProceedFn,
) -> AopResult<InvocationResult> {
if self.aspects.is_empty() {
return proceed(ctx);
}
self.run_aspect(0, ctx, proceed)
}
fn run_aspect(
&self,
index: usize,
ctx: InvocationContext,
final_proceed: ProceedFn,
) -> AopResult<InvocationResult> {
if index >= self.aspects.len() {
return final_proceed(ctx);
}
let aspect = self.aspects[index].clone();
let remaining_aspects = self.aspects[index + 1..].to_vec();
let final_proceed = Arc::new(final_proceed);
let next: ProceedFn = Box::new(move |ctx: InvocationContext| {
if remaining_aspects.is_empty() {
return final_proceed(ctx);
}
// Build remaining chain recursively
let sub_chain = AspectChain {
aspects: remaining_aspects.clone(),
};
sub_chain.execute(ctx, {
let fp = final_proceed.clone();
Box::new(move |ctx| fp(ctx))
})
});
aspect.around(ctx, &next)
}
/// Return the names of all aspects in this chain (in order).
pub fn aspect_names(&self) -> Vec<&str> {
self.aspects.iter().map(|a| a.name()).collect()
}
/// Prepend `AuthenticateAspect` to this chain.
///
/// This is the mechanism for security-by-default: the framework calls
/// `with_default_auth()` on every chain that does NOT have `@public`.
///
/// Equivalent to `.add(Arc::new(AuthenticateAspect))` at position 0, but
/// semantically explicit about what it means.
pub fn with_default_auth(self) -> Self {
let mut aspects = vec![Arc::new(AuthenticateAspect) as Arc<dyn Aspect>];
aspects.extend(self.aspects);
Self { aspects }
}
/// Returns `true` if the chain contains an `AuthenticateAspect`.
pub fn has_auth(&self) -> bool {
self.aspects.iter().any(|a| a.name() == "authenticate")
}
}
impl Default for AspectChain {
fn default() -> Self {
Self::new()
}
}
+157
View File
@@ -0,0 +1,157 @@
//! el-aop — Aspect-Oriented Programming for el-ui.
//!
//! Cross-cutting concerns as first-class language features. Not a library you
//! import. Built into the framework. Applied as decorators:
//!
//! ```text
//! @authenticate ← applied by DEFAULT to every function
//! @authorize(role: "admin")
//! @cache(ttl: 300)
//! @rate_limit(requests: 100, per: 60)
//! component AdminDashboard { ... }
//!
//! @public ← explicit opt-out of authentication
//! component LandingPage { ... }
//! ```
//!
//! ## Security-by-default
//!
//! `@authenticate` is the default. Functions without `@public` are protected.
//! This makes it as hard as possible to accidentally ship an unprotected endpoint.
pub mod aspects;
pub mod chain;
pub mod registry;
pub use aspects::{
AuthenticateAspect, AuthorizeAspect, CacheAspect, LogAspect, RateLimitAspect, RetryAspect,
TraceAspect, ValidateAspect,
};
pub use chain::AspectChain;
pub use public::PublicMarker;
pub use registry::AspectRegistry;
pub mod public;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AopError {
#[error("authentication required")]
Unauthenticated,
#[error("forbidden: requires role '{role}'")]
Forbidden { role: String },
#[error("rate limit exceeded: {requests} requests per {per}s")]
RateLimited { requests: u32, per: u64 },
#[error("validation failed: {0}")]
ValidationFailed(String),
#[error("aspect error: {0}")]
Aspect(String),
#[error("all {attempts} retry attempts failed: {last_error}")]
RetriesExhausted { attempts: u32, last_error: String },
}
pub type AopResult<T> = Result<T, AopError>;
/// Context passed through the aspect chain.
///
/// Contains the incoming arguments and metadata about the call.
/// Aspects can read and mutate this context as they execute.
#[derive(Debug, Clone)]
pub struct InvocationContext {
/// The component or service being called.
pub target: String,
/// The method being called.
pub method: String,
/// Arguments passed to the method.
pub args: HashMap<String, String>,
/// Metadata added by aspects (e.g., the authenticated user, trace ID).
pub metadata: HashMap<String, String>,
}
impl InvocationContext {
pub fn new(target: impl Into<String>, method: impl Into<String>) -> Self {
Self {
target: target.into(),
method: method.into(),
args: HashMap::new(),
metadata: HashMap::new(),
}
}
pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.args.insert(key.into(), value.into());
self
}
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn get_meta(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(|s| s.as_str())
}
}
/// The result of invoking a method through an aspect chain.
#[derive(Debug, Clone)]
pub struct InvocationResult {
pub value: String,
pub metadata: HashMap<String, String>,
}
impl InvocationResult {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
metadata: HashMap::new(),
}
}
}
/// A handler that performs the actual method invocation.
/// Aspects wrap around this.
pub type ProceedFn = Box<dyn Fn(InvocationContext) -> AopResult<InvocationResult> + Send + Sync>;
/// The core Aspect trait.
///
/// Each aspect implements `before`, `after`, or `around` advice.
/// The default implementations are no-ops — only override what you need.
pub trait Aspect: Send + Sync {
/// The aspect's name (used for debugging and registry lookup).
fn name(&self) -> &'static str;
/// Before advice — runs before the method. Can reject the call.
fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
let _ = ctx;
Ok(())
}
/// After advice — runs after the method. Receives the result.
/// Can modify the result or perform cleanup.
fn after(
&self,
ctx: &InvocationContext,
result: AopResult<InvocationResult>,
) -> AopResult<InvocationResult> {
let _ = ctx;
result
}
/// Around advice — wraps the entire invocation.
/// The default implementation calls `before`, then `proceed`, then `after`.
fn around(
&self,
mut ctx: InvocationContext,
proceed: &ProceedFn,
) -> AopResult<InvocationResult> {
self.before(&mut ctx)?;
let result = proceed(ctx.clone());
self.after(&ctx, result)
}
}
+57
View File
@@ -0,0 +1,57 @@
//! `@public` marker — the explicit opt-out of authentication.
//!
//! The security-by-default model: `@authenticate` is applied to EVERY function
//! by default. `@public` is the rare annotation that says "this endpoint
//! intentionally has no auth".
//!
//! `PublicMarker` is a zero-cost marker. When the compiler sees `@public` on a
//! function, it strips `AuthenticateAspect` from the chain for that function.
//! The chain-builder checks `is_public` before prepending default auth.
/// Zero-cost marker indicating that a function is intentionally public.
///
/// When `@public` is present, the default `AuthenticateAspect` is NOT added
/// to the function's aspect chain.
///
/// Usage in the el-ui compiler:
/// ```text
/// @public
/// fn health_check() -> Status { ... }
/// ```
///
/// In the AOP chain builder:
/// ```rust
/// use el_aop::{AspectChain, PublicMarker, AuthenticateAspect};
/// use std::sync::Arc;
///
/// fn build_chain(is_public: bool) -> AspectChain {
/// if is_public || PublicMarker::is_bypassing() {
/// AspectChain::new()
/// } else {
/// AspectChain::new().with_default_auth()
/// }
/// }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PublicMarker;
impl PublicMarker {
/// Returns `true` — always. Exists for use in match arms / conditional logic.
///
/// The presence of a `PublicMarker` in the decorator list is the signal;
/// this method is a convenience for procedural logic over decorator lists.
pub const fn is_bypassing() -> bool {
true
}
/// The decorator name this marker corresponds to.
pub const fn decorator_name() -> &'static str {
"public"
}
}
impl std::fmt::Display for PublicMarker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "@public")
}
}
+284
View File
@@ -0,0 +1,284 @@
//! Aspect registry — register built-in and custom aspects by name.
//!
//! The compiler's AOP codegen uses the registry to look up aspect implementations
//! by their decorator name (e.g., `"authenticate"` → `AuthenticateAspect`).
//!
//! ## Security-by-default
//!
//! `"public"` is a special bypass marker — NOT an aspect. When the compiler sees
//! `@public` it calls `registry.is_public_bypass(name)` and skips default auth.
//!
//! `set_default_auth_guard()` installs the global default `AuthenticateAspect`
//! that is prepended to every non-`@public` chain.
use crate::Aspect;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
type AspectFactory = Box<dyn Fn(&HashMap<String, String>) -> Arc<dyn Aspect> + Send + Sync>;
/// Registry of aspect factories, indexed by decorator name.
pub struct AspectRegistry {
factories: RwLock<HashMap<String, AspectFactory>>,
/// When `true`, the registry is configured to prepend AuthenticateAspect
/// to every non-`@public` chain via `AspectChain::with_default_auth()`.
default_auth_enabled: RwLock<bool>,
/// Names that are bypass markers (not real aspects). Currently just "public".
bypass_markers: RwLock<std::collections::HashSet<String>>,
}
impl AspectRegistry {
pub fn new() -> Self {
let mut markers = std::collections::HashSet::new();
markers.insert("public".to_string());
Self {
factories: RwLock::new(HashMap::new()),
default_auth_enabled: RwLock::new(false),
bypass_markers: RwLock::new(markers),
}
}
/// Create a registry with all built-in aspects registered.
pub fn with_builtins() -> Self {
let registry = Self::new();
registry.register_builtins();
registry
}
/// Enable security-by-default: every non-`@public` chain will have
/// `AuthenticateAspect` prepended automatically.
///
/// Call at application startup. After this, use `AspectChain::with_default_auth()`
/// when building chains for protected functions.
pub fn set_default_auth_enabled(&self, enabled: bool) {
*self.default_auth_enabled.write().expect("registry lock poisoned") = enabled;
}
/// Returns `true` if security-by-default auth is active.
pub fn is_default_auth_enabled(&self) -> bool {
*self.default_auth_enabled.read().expect("registry lock poisoned")
}
/// Returns `true` if `name` is a public bypass marker (e.g., `"public"`).
///
/// Bypass markers are NOT aspects — they signal that default auth should
/// be skipped for the decorated function.
pub fn is_public_bypass(&self, name: &str) -> bool {
self.bypass_markers
.read()
.expect("registry lock poisoned")
.contains(name)
}
/// Register a custom bypass marker name.
///
/// By default only `"public"` is registered. Use this to add custom
/// bypass annotations (e.g., `"internal_only"` that uses a different guard).
pub fn register_bypass_marker(&self, name: &str) {
self.bypass_markers
.write()
.expect("registry lock poisoned")
.insert(name.to_string());
}
/// Build an `AspectChain` for a function with the given decorators.
///
/// This is the primary chain-building entry point used by the AOP codegen.
///
/// - If any decorator is a bypass marker (`@public`), returns a plain chain
/// with no default auth.
/// - Otherwise, if `default_auth_enabled`, prepends `AuthenticateAspect`.
/// - Unknown decorator names are silently skipped (forward-compatible).
pub fn build_chain(&self, decorator_names: &[(&str, HashMap<String, String>)]) -> crate::AspectChain {
let is_public = decorator_names.iter().any(|(name, _)| self.is_public_bypass(name));
let mut chain = crate::AspectChain::new();
for (name, params) in decorator_names {
if self.is_public_bypass(name) {
continue; // bypass markers are not aspects
}
if let Some(aspect) = self.create(name, params) {
chain = chain.add(aspect);
}
}
if !is_public && self.is_default_auth_enabled() {
chain = chain.with_default_auth();
}
chain
}
/// Register all built-in aspects.
pub fn register_builtins(&self) {
use crate::aspects::*;
self.register("authenticate", |_params| {
Arc::new(AuthenticateAspect)
});
self.register("authorize", |params| {
let role = params
.get("role")
.cloned()
.unwrap_or_else(|| "user".to_string());
Arc::new(AuthorizeAspect::new(role))
});
self.register("cache", |params| {
let ttl: u64 = params
.get("ttl")
.and_then(|s| s.parse().ok())
.unwrap_or(300);
Arc::new(CacheAspect::new(ttl))
});
self.register("rate_limit", |params| {
let requests: u32 = params
.get("requests")
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let per: u64 = params
.get("per")
.and_then(|s| s.parse().ok())
.unwrap_or(60);
Arc::new(RateLimitAspect::new(requests, per))
});
self.register("log", |params| {
let level = params
.get("level")
.cloned()
.unwrap_or_else(|| "info".to_string());
Arc::new(LogAspect::new(level))
});
self.register("validate", |_params| Arc::new(ValidateAspect::new()));
self.register("retry", |params| {
let attempts: u32 = params
.get("attempts")
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let backoff = params
.get("backoff")
.map(|s| s.as_str())
.unwrap_or("none");
let aspect = RetryAspect::new(attempts);
let aspect = match backoff {
"exponential" => aspect.with_exponential_backoff(100),
"fixed" => aspect.with_fixed_backoff(500),
_ => aspect,
};
Arc::new(aspect)
});
self.register("trace", |params| {
let service = params
.get("service")
.cloned()
.unwrap_or_else(|| "el-ui".to_string());
Arc::new(TraceAspect::new(service))
});
}
/// Register a custom aspect factory.
pub fn register(
&self,
name: &str,
factory: impl Fn(&HashMap<String, String>) -> Arc<dyn Aspect> + Send + Sync + 'static,
) {
self.factories
.write()
.expect("registry lock poisoned")
.insert(name.to_string(), Box::new(factory));
}
/// Instantiate an aspect by decorator name with the given params.
pub fn create(
&self,
name: &str,
params: &HashMap<String, String>,
) -> Option<Arc<dyn Aspect>> {
let factories = self.factories.read().expect("registry lock poisoned");
factories.get(name).map(|f| f(params))
}
/// List all registered aspect names.
pub fn aspect_names(&self) -> Vec<String> {
self.factories
.read()
.expect("registry lock poisoned")
.keys()
.cloned()
.collect()
}
/// Check if an aspect name is registered.
pub fn contains(&self, name: &str) -> bool {
self.factories
.read()
.expect("registry lock poisoned")
.contains_key(name)
}
}
impl Default for AspectRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod registry_default_auth_tests {
use super::*;
#[test]
fn test_default_auth_disabled_by_default() {
let reg = AspectRegistry::new();
assert!(!reg.is_default_auth_enabled());
}
#[test]
fn test_set_default_auth_enabled() {
let reg = AspectRegistry::new();
reg.set_default_auth_enabled(true);
assert!(reg.is_default_auth_enabled());
}
#[test]
fn test_public_is_bypass_marker() {
let reg = AspectRegistry::new();
assert!(reg.is_public_bypass("public"));
assert!(!reg.is_public_bypass("authenticate"));
}
#[test]
fn test_build_chain_public_skips_default_auth() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("public", HashMap::new())];
let chain = reg.build_chain(&decorators);
assert!(!chain.has_auth(), "public chain should not have default auth");
}
#[test]
fn test_build_chain_non_public_gets_default_auth() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("log", HashMap::new())];
let chain = reg.build_chain(&decorators);
assert!(chain.has_auth(), "non-public chain should have default auth prepended");
}
#[test]
fn test_build_chain_auth_is_first_aspect() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("log", HashMap::new())];
let chain = reg.build_chain(&decorators);
let names = chain.aspect_names();
assert_eq!(names[0], "authenticate", "authenticate must be first in the chain");
}
}
+305
View File
@@ -0,0 +1,305 @@
//! Tests for el-aop.
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use crate::{
aspects::*,
chain::AspectChain,
registry::AspectRegistry,
AopError, Aspect, InvocationContext, InvocationResult,
};
fn succeed_proceed(value: impl Into<String> + Clone) -> crate::ProceedFn {
let v = value.into();
Box::new(move |_ctx| Ok(InvocationResult::new(v.clone())))
}
fn fail_proceed(msg: impl Into<String> + Clone) -> crate::ProceedFn {
let m = msg.into();
Box::new(move |_ctx| Err(AopError::Aspect(m.clone())))
}
fn ctx(target: &str, method: &str) -> InvocationContext {
InvocationContext::new(target, method)
}
fn authed_ctx(target: &str, method: &str) -> InvocationContext {
ctx(target, method).with_meta("user_id", "user-123")
}
fn admin_ctx(target: &str, method: &str) -> InvocationContext {
ctx(target, method)
.with_meta("user_id", "admin-1")
.with_meta("roles", "admin,user")
}
// ── Test 1: AuthenticateAspect rejects unauthenticated calls ─────────────
#[test]
fn test_authenticate_rejects_unauthenticated() {
let aspect = AuthenticateAspect;
let mut ctx = ctx("AdminDashboard", "load");
let result = aspect.before(&mut ctx);
assert!(result.is_err());
assert!(matches!(result, Err(AopError::Unauthenticated)));
}
// ── Test 2: AuthenticateAspect allows authenticated calls ─────────────────
#[test]
fn test_authenticate_allows_authenticated() {
let aspect = AuthenticateAspect;
let mut ctx = authed_ctx("AdminDashboard", "load");
let result = aspect.before(&mut ctx);
assert!(result.is_ok());
}
// ── Test 3: AuthorizeAspect rejects wrong role ────────────────────────────
#[test]
fn test_authorize_rejects_wrong_role() {
let aspect = AuthorizeAspect::new("admin");
let mut ctx = authed_ctx("Dashboard", "delete").with_meta("roles", "user");
let result = aspect.before(&mut ctx);
assert!(matches!(result, Err(AopError::Forbidden { .. })));
}
// ── Test 4: AuthorizeAspect allows correct role ───────────────────────────
#[test]
fn test_authorize_allows_correct_role() {
let aspect = AuthorizeAspect::new("admin");
let mut ctx = admin_ctx("Dashboard", "delete");
let result = aspect.before(&mut ctx);
assert!(result.is_ok());
}
// ── Test 5: CacheAspect returns cached result on second call ──────────────
#[test]
fn test_cache_returns_cached_result() {
let aspect = CacheAspect::new(300);
let ctx = authed_ctx("OrderService", "get_orders");
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
let cc = call_count.clone();
let proceed: crate::ProceedFn = Box::new(move |_ctx| {
let n = cc.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
Ok(InvocationResult::new(format!("result-{}", n)))
});
// First call — executes proceed
let r1 = aspect.around(ctx.clone(), &proceed).unwrap();
// Second call — should return cached (proceed not called again)
let proceed2: crate::ProceedFn = Box::new(|_ctx| {
panic!("proceed should not be called on cache hit");
});
let r2 = aspect.around(ctx.clone(), &proceed2).unwrap();
assert_eq!(r1.value, r2.value, "cached value should be returned");
}
// ── Test 6: RateLimitAspect blocks after limit exceeded ───────────────────
#[test]
fn test_rate_limit_blocks_after_limit() {
let aspect = RateLimitAspect::new(2, 60);
let mut ctx = authed_ctx("OrderService", "create_order");
// First two calls succeed
assert!(aspect.before(&mut ctx).is_ok());
assert!(aspect.before(&mut ctx).is_ok());
// Third call should be blocked
let result = aspect.before(&mut ctx);
assert!(matches!(result, Err(AopError::RateLimited { .. })));
}
// ── Test 7: LogAspect passes through to proceed ───────────────────────────
#[test]
fn test_log_aspect_passthrough() {
let aspect = LogAspect::new("info");
let ctx = authed_ctx("UserService", "get_user");
let result = aspect.around(ctx, &succeed_proceed("user-data")).unwrap();
assert_eq!(result.value, "user-data");
}
// ── Test 8: ValidateAspect rejects required field missing ────────────────
#[test]
fn test_validate_required_field() {
let aspect = ValidateAspect::new();
aspect.add_rule("UserService", "create_user", "name", "required");
let mut ctx = authed_ctx("UserService", "create_user");
// No "name" arg
let result = aspect.before(&mut ctx);
assert!(matches!(result, Err(AopError::ValidationFailed(_))));
}
// ── Test 9: ValidateAspect passes when field is present ──────────────────
#[test]
fn test_validate_required_field_present() {
let aspect = ValidateAspect::new();
aspect.add_rule("UserService", "create_user", "email", "email");
let mut ctx = authed_ctx("UserService", "create_user")
.with_arg("email", "alice@example.com");
assert!(aspect.before(&mut ctx).is_ok());
}
// ── Test 10: ValidateAspect rejects invalid email ─────────────────────────
#[test]
fn test_validate_email_rule() {
let aspect = ValidateAspect::new();
aspect.add_rule("UserService", "create_user", "email", "email");
let mut ctx = authed_ctx("UserService", "create_user")
.with_arg("email", "not-an-email");
let result = aspect.before(&mut ctx);
assert!(matches!(result, Err(AopError::ValidationFailed(_))));
}
// ── Test 11: RetryAspect retries on failure ───────────────────────────────
#[test]
fn test_retry_succeeds_on_third_attempt() {
let aspect = RetryAspect::new(3);
let ctx = authed_ctx("OrderService", "place_order");
let attempt = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let attempt_clone = attempt.clone();
let proceed: crate::ProceedFn = Box::new(move |_ctx| {
let n = attempt_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n < 2 {
Err(AopError::Aspect("transient error".into()))
} else {
Ok(InvocationResult::new("success"))
}
});
let result = aspect.around(ctx, &proceed).unwrap();
assert_eq!(result.value, "success");
}
// ── Test 12: RetryAspect exhausts all attempts ───────────────────────────
#[test]
fn test_retry_exhausted() {
let aspect = RetryAspect::new(3);
let ctx = authed_ctx("OrderService", "place_order");
let result = aspect.around(ctx, &fail_proceed("always fails"));
assert!(matches!(result, Err(AopError::RetriesExhausted { attempts: 3, .. })));
}
// ── Test 13: TraceAspect injects trace/span IDs ───────────────────────────
#[test]
fn test_trace_aspect_injects_ids() {
let aspect = TraceAspect::new("el-ui");
let ctx = authed_ctx("UserService", "get_user");
let result = aspect.around(ctx, &succeed_proceed("data")).unwrap();
assert!(
result.metadata.contains_key("trace_id"),
"should inject trace_id"
);
assert!(
result.metadata.contains_key("span_id"),
"should inject span_id"
);
}
// ── Test 14: AspectChain executes aspects in order ────────────────────────
#[test]
fn test_aspect_chain_ordering() {
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
struct OrderTracker {
name: &'static str,
order: Arc<std::sync::Mutex<Vec<&'static str>>>,
}
impl Aspect for OrderTracker {
fn name(&self) -> &'static str { self.name }
fn around(&self, ctx: InvocationContext, proceed: &crate::ProceedFn) -> crate::AopResult<InvocationResult> {
self.order.lock().unwrap().push(self.name);
proceed(ctx)
}
}
let chain = AspectChain::new()
.add(Arc::new(OrderTracker { name: "first", order: order.clone() }))
.add(Arc::new(OrderTracker { name: "second", order: order.clone() }))
.add(Arc::new(OrderTracker { name: "third", order: order.clone() }));
let ctx = authed_ctx("MyService", "my_method");
chain.execute(ctx, succeed_proceed("ok")).unwrap();
let recorded = order.lock().unwrap();
assert_eq!(*recorded, vec!["first", "second", "third"]);
}
// ── Test 15: AspectChain with auth + authorize rejects unauthenticated ────
#[test]
fn test_aspect_chain_auth_flow() {
let chain = AspectChain::new()
.add(Arc::new(AuthenticateAspect))
.add(Arc::new(AuthorizeAspect::new("admin")));
// Unauthenticated — should fail at authenticate
let ctx = ctx("AdminDashboard", "load");
let result = chain.execute(ctx, succeed_proceed("ok"));
assert!(matches!(result, Err(AopError::Unauthenticated)));
// Authenticated but wrong role — should fail at authorize
let ctx = authed_ctx("AdminDashboard", "load").with_meta("roles", "user");
let result = chain.execute(ctx, succeed_proceed("ok"));
assert!(matches!(result, Err(AopError::Forbidden { .. })));
// Admin — should succeed
let ctx = admin_ctx("AdminDashboard", "load");
let result = chain.execute(ctx, succeed_proceed("ok"));
assert!(result.is_ok());
}
// ── Test 16: AspectRegistry registers all builtins ────────────────────────
#[test]
fn test_registry_has_builtins() {
let registry = AspectRegistry::with_builtins();
for name in ["authenticate", "authorize", "cache", "rate_limit", "log", "validate", "retry", "trace"] {
assert!(registry.contains(name), "should have built-in: {}", name);
}
}
// ── Test 17: AspectRegistry creates aspects from params ───────────────────
#[test]
fn test_registry_creates_aspect() {
let registry = AspectRegistry::with_builtins();
let mut params = HashMap::new();
params.insert("role".into(), "admin".into());
let aspect = registry.create("authorize", &params);
assert!(aspect.is_some(), "should create authorize aspect");
assert_eq!(aspect.unwrap().name(), "authorize");
}
// ── Test 18: AspectRegistry::create returns None for unknown aspect ───────
#[test]
fn test_registry_unknown_aspect() {
let registry = AspectRegistry::with_builtins();
let result = registry.create("unknown_aspect", &HashMap::new());
assert!(result.is_none());
}
// ── Test 19: CacheAspect with zero TTL doesn't serve stale data ──────────
#[test]
fn test_cache_zero_ttl() {
let aspect = CacheAspect::new(0); // Immediate expiry
let ctx = authed_ctx("Service", "method");
let n = Arc::new(std::sync::atomic::AtomicU32::new(0));
let nc = n.clone();
// Both calls should hit proceed since ttl=0 means instant expiry
let p1: crate::ProceedFn = Box::new(move |_ctx| {
let v = nc.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(InvocationResult::new(v.to_string()))
});
// Zero TTL will expire immediately; we just verify it doesn't panic
let _ = aspect.around(ctx.clone(), &p1);
// Second call should also execute proceed
let p2: crate::ProceedFn = Box::new(|_ctx| Ok(InvocationResult::new("fresh")));
let r = aspect.around(ctx, &p2).unwrap();
assert_eq!(r.value, "fresh");
}
// ── Test 20: Empty AspectChain calls proceed directly ─────────────────────
#[test]
fn test_empty_chain_calls_proceed() {
let chain = AspectChain::new();
assert!(chain.is_empty());
let ctx = authed_ctx("Service", "method");
let result = chain.execute(ctx, succeed_proceed("direct")).unwrap();
assert_eq!(result.value, "direct");
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "el-auth"
version = "0.1.0"
edition = "2021"
description = "el-ui built-in authentication and authorization — native to the framework"
license = "MIT"
[lib]
name = "el_auth"
path = "src/lib.rs"
[dependencies]
thiserror = "1"
base64 = "0.22"
hmac = "0.12"
sha2 = "0.10"
el-identity = { path = "../el-identity" }
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
+78
View File
@@ -0,0 +1,78 @@
//! Auth context — the current authenticated user and their roles/permissions.
/// The authenticated user.
#[derive(Debug, Clone)]
pub struct AuthUser {
pub id: String,
pub email: String,
pub name: String,
}
impl AuthUser {
pub fn new(
id: impl Into<String>,
email: impl Into<String>,
name: impl Into<String>,
) -> Self {
Self {
id: id.into(),
email: email.into(),
name: name.into(),
}
}
}
/// The auth context — populated by `AuthMiddleware` and available to all
/// components and services downstream in the request.
///
/// Passed as `ctx.metadata["user_id"]`, `ctx.metadata["roles"]` in the AOP
/// layer (see `el-aop`).
#[derive(Debug, Clone)]
pub struct AuthContext {
pub user: Option<AuthUser>,
pub roles: Vec<String>,
pub permissions: Vec<String>,
/// The raw token/session ID that was verified.
pub token: String,
}
impl AuthContext {
pub fn anonymous() -> Self {
Self {
user: None,
roles: Vec::new(),
permissions: Vec::new(),
token: String::new(),
}
}
pub fn authenticated(user: AuthUser, roles: Vec<String>, token: impl Into<String>) -> Self {
Self {
user: Some(user),
roles,
permissions: Vec::new(),
token: token.into(),
}
}
pub fn with_permissions(mut self, perms: Vec<String>) -> Self {
self.permissions = perms;
self
}
pub fn is_authenticated(&self) -> bool {
self.user.is_some()
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.iter().any(|p| p == permission)
}
pub fn user_id(&self) -> Option<&str> {
self.user.as_ref().map(|u| u.id.as_str())
}
}
+128
View File
@@ -0,0 +1,128 @@
//! EngramSessionStore — a `SessionProvider`-compatible store backed by the Engram graph.
//!
//! Sessions are Engram graph nodes (via el-identity's `SessionManager`).
//! This allows server-side invalidation even for stateless JWT workflows:
//! on every request, the JWT's `session_id` claim is used to look up the
//! Session node in Engram. If the node is missing or expired, the request
//! is rejected regardless of JWT validity.
//!
//! Dependency: takes `Arc<dyn EngramClient>` — soft dependency, no tight coupling
//! to the Engram crate.
use crate::{AuthContext, AuthError, AuthProvider, AuthResult, AuthUser, RoleRegistry};
use el_identity::{
engram::EngramClient,
session::SessionManager,
};
use std::sync::Arc;
/// Session provider backed by the Engram identity graph.
///
/// Sessions issued by this provider are stored as Engram Session nodes.
/// The session ID returned is the Engram node's UUID, which is also embedded
/// in JWTs via `JwtClaims::session_id`.
pub struct EngramSessionStore {
session_manager: Arc<SessionManager>,
client: Arc<dyn EngramClient>,
}
impl EngramSessionStore {
/// Create a new `EngramSessionStore`.
///
/// `client` is the Engram graph client. In production, pass your real
/// Engram client. In tests, use `el_identity::engram::MockEngramClient`.
pub fn new(client: Arc<dyn EngramClient>) -> Self {
let sm = Arc::new(SessionManager::new(client.clone()));
Self { session_manager: sm, client }
}
/// Create with a custom session TTL (seconds).
pub fn with_ttl(client: Arc<dyn EngramClient>, ttl_seconds: i64) -> Self {
let sm = Arc::new(SessionManager::new(client.clone()).with_ttl(ttl_seconds));
Self { session_manager: sm, client }
}
/// Expose the underlying SessionManager for advanced use (e.g., listing sessions).
pub fn session_manager(&self) -> &Arc<SessionManager> {
&self.session_manager
}
}
impl AuthProvider for EngramSessionStore {
fn name(&self) -> &'static str {
"engram_session"
}
/// Verify a session by its ID (Engram Session node UUID).
///
/// Validates expiry via graph lookup. Returns `AuthContext` populated from
/// the Session and User nodes.
fn verify(&self, session_id: &str) -> AuthResult<AuthContext> {
// Validate session node (checks expiry, lazy-deletes expired)
let session = self
.session_manager
.validate(session_id)
.map_err(|e| match e {
el_identity::IdentityError::SessionNotFound => AuthError::SessionNotFound,
el_identity::IdentityError::SessionExpired => AuthError::SessionNotFound,
other => AuthError::Config(other.to_string()),
})?;
// Load user node
let user_id_str = session.user_id.to_string();
let user_node = self
.client
.get_node(&user_id_str)
.map_err(|e| AuthError::Config(e.to_string()))?
.ok_or(AuthError::InvalidCredentials)?;
let identity_user = el_identity::User::from_value(&user_node)
.ok_or_else(|| AuthError::Config("user node parse failed".into()))?;
let auth_user = AuthUser::new(
identity_user.id.to_string(),
&identity_user.email,
&identity_user.display_name,
);
// Load roles via has_role edges
let role_nodes = self
.client
.find_connected(&user_id_str, el_identity::nodes::EDGE_HAS_ROLE)
.map_err(|e| AuthError::Config(e.to_string()))?;
let role_names: Vec<String> = role_nodes
.iter()
.filter_map(el_identity::Role::from_value)
.map(|r| r.name)
.collect();
Ok(AuthContext::authenticated(auth_user, role_names, session_id))
}
/// Issue a new Engram-backed session for the given user.
///
/// The user must already exist as a User node in Engram. Returns the
/// session ID (UUID string) which should be embedded in the JWT's
/// `session_id` claim.
fn issue(&self, user: AuthUser, _role_registry: &RoleRegistry) -> AuthResult<String> {
// Parse user ID as UUID
let user_uuid = uuid::Uuid::parse_str(&user.id)
.map_err(|_| AuthError::Config(format!("invalid user ID UUID: {}", user.id)))?;
let session = self
.session_manager
.create(user_uuid, None)
.map_err(|e| AuthError::Config(e.to_string()))?;
Ok(session.id.to_string())
}
/// Revoke a session by deleting the Session node from the graph.
fn revoke(&self, session_id: &str) -> AuthResult<()> {
self.session_manager
.invalidate(session_id)
.map_err(|e| AuthError::Config(e.to_string()))
}
}
+314
View File
@@ -0,0 +1,314 @@
//! JWT provider — sign and verify JSON Web Tokens.
//!
//! Uses HMAC-SHA256 (HS256) for signing. Does NOT use the `jsonwebtoken` crate
//! to keep dependencies minimal; implements the JWT spec directly.
//!
//! Format: base64url(header).base64url(payload).base64url(signature)
use crate::{AuthContext, AuthError, AuthProvider, AuthResult, AuthUser, RoleRegistry};
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
/// JWT claims payload.
///
/// The `session_id` field is included so the Engram session node can be
/// validated on every request, enabling server-side session invalidation
/// even for stateless JWTs.
#[derive(Debug, Clone)]
pub struct JwtClaims {
pub sub: String, // user ID
pub email: String,
pub name: String,
pub roles: Vec<String>,
/// The Engram Session node ID. Used by `EngramSessionStore` to validate
/// the session graph node on every request, enabling server-side logout.
pub session_id: Option<String>,
pub iat: u64, // issued-at (unix seconds)
pub exp: u64, // expiry (unix seconds)
}
impl JwtClaims {
pub fn new(user: &AuthUser, roles: Vec<String>, ttl_seconds: u64) -> Self {
let now = unix_now();
Self {
sub: user.id.clone(),
email: user.email.clone(),
name: user.name.clone(),
roles,
session_id: None,
iat: now,
exp: now + ttl_seconds,
}
}
/// Create claims with an Engram session ID embedded.
pub fn new_with_session(
user: &AuthUser,
roles: Vec<String>,
session_id: impl Into<String>,
ttl_seconds: u64,
) -> Self {
let mut claims = Self::new(user, roles, ttl_seconds);
claims.session_id = Some(session_id.into());
claims
}
pub fn is_expired(&self) -> bool {
unix_now() > self.exp
}
/// Serialize claims to JSON (manual, no serde dependency).
pub fn to_json(&self) -> String {
let roles_json = self
.roles
.iter()
.map(|r| format!("\"{}\"", r))
.collect::<Vec<_>>()
.join(",");
// Build the JSON manually, inserting session_id only when present.
let mut json = format!(
"{{\"sub\":\"{}\",\"email\":\"{}\",\"name\":\"{}\",\"roles\":[{}]",
self.sub, self.email, self.name, roles_json
);
if let Some(sid) = &self.session_id {
json.push_str(&format!(",\"session_id\":\"{}\"", sid));
}
json.push_str(&format!(",\"iat\":{},\"exp\":{}}}", self.iat, self.exp));
json
}
/// Deserialize claims from JSON (manual parser).
pub fn from_json(json: &str) -> Option<Self> {
let sub = extract_str(json, "sub")?;
let email = extract_str(json, "email").unwrap_or_default();
let name = extract_str(json, "name").unwrap_or_default();
let iat = extract_u64(json, "iat").unwrap_or(0);
let exp = extract_u64(json, "exp").unwrap_or(0);
let roles = extract_str_array(json, "roles");
let session_id = extract_str(json, "session_id");
Some(Self { sub, email, name, roles, session_id, iat, exp })
}
}
/// JWT provider — issues and verifies HS256 JWTs.
pub struct JwtProvider {
secret: Vec<u8>,
/// Token TTL in seconds (default: 3600 = 1 hour).
pub ttl_seconds: u64,
}
impl JwtProvider {
pub fn new(secret: impl Into<Vec<u8>>) -> Self {
Self { secret: secret.into(), ttl_seconds: 3600 }
}
pub fn from_env(env_var: &str) -> AuthResult<Self> {
let secret = std::env::var(env_var).map_err(|_| {
AuthError::Config(format!("env var {} not set", env_var))
})?;
Ok(Self::new(secret.into_bytes()))
}
pub fn with_ttl(mut self, seconds: u64) -> Self {
self.ttl_seconds = seconds;
self
}
/// Sign a token with HMAC-SHA256.
fn sign(&self, header_payload: &str) -> String {
let mut mac = HmacSha256::new_from_slice(&self.secret)
.expect("HMAC can take key of any size");
mac.update(header_payload.as_bytes());
let result = mac.finalize();
base64url_encode(&result.into_bytes())
}
/// Encode a JWT token from claims.
pub fn encode(&self, claims: &JwtClaims) -> String {
let header = base64url_encode(b"{\"alg\":\"HS256\",\"typ\":\"JWT\"}");
let payload = base64url_encode(claims.to_json().as_bytes());
let header_payload = format!("{}.{}", header, payload);
let signature = self.sign(&header_payload);
format!("{}.{}", header_payload, signature)
}
/// Decode and verify a JWT token.
pub fn decode(&self, token: &str) -> AuthResult<JwtClaims> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(AuthError::TokenInvalid("not a valid JWT".into()));
}
let header_payload = format!("{}.{}", parts[0], parts[1]);
let expected_sig = self.sign(&header_payload);
if !constant_time_eq(parts[2], &expected_sig) {
return Err(AuthError::TokenInvalid("signature mismatch".into()));
}
let payload_bytes = base64url_decode(parts[1])
.ok_or_else(|| AuthError::TokenInvalid("payload decode failed".into()))?;
let payload_str = String::from_utf8(payload_bytes)
.map_err(|_| AuthError::TokenInvalid("payload not utf8".into()))?;
let claims = JwtClaims::from_json(&payload_str)
.ok_or_else(|| AuthError::TokenInvalid("claims parse failed".into()))?;
if claims.is_expired() {
return Err(AuthError::TokenExpired);
}
Ok(claims)
}
}
impl AuthProvider for JwtProvider {
fn name(&self) -> &'static str {
"jwt"
}
fn verify(&self, token: &str) -> AuthResult<AuthContext> {
let claims = self.decode(token)?;
let user = AuthUser::new(&claims.sub, &claims.email, &claims.name);
Ok(AuthContext::authenticated(user, claims.roles, token))
}
fn issue(&self, user: AuthUser, _role_registry: &RoleRegistry) -> AuthResult<String> {
let claims = JwtClaims::new(&user, Vec::new(), self.ttl_seconds);
Ok(self.encode(&claims))
}
fn revoke(&self, _token: &str) -> AuthResult<()> {
// JWTs are stateless — revocation requires a blocklist.
// TODO: maintain a revocation list (in-memory or Redis).
Ok(())
}
}
// ── Crypto helpers ─────────────────────────────────────────────────────────────
fn base64url_encode(input: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::new();
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(CHARS[((n >> 18) & 63) as usize] as char);
out.push(CHARS[((n >> 12) & 63) as usize] as char);
if chunk.len() > 1 {
out.push(CHARS[((n >> 6) & 63) as usize] as char);
}
if chunk.len() > 2 {
out.push(CHARS[(n & 63) as usize] as char);
}
}
out
}
fn base64url_decode(input: &str) -> Option<Vec<u8>> {
// Pad if needed
let mut s = input.replace('-', "+").replace('_', "/");
while s.len() % 4 != 0 {
s.push('=');
}
base64_decode_standard(&s)
}
fn base64_decode_standard(input: &str) -> Option<Vec<u8>> {
const TABLE: [u8; 128] = {
let mut t = [255u8; 128];
let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut i = 0usize;
while i < chars.len() {
t[chars[i] as usize] = i as u8;
i += 1;
}
t
};
let input = input.trim_end_matches('=');
let mut out = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
while i + 3 < bytes.len() {
let a = TABLE.get(bytes[i] as usize).copied().filter(|&v| v != 255)?;
let b = TABLE.get(bytes[i+1] as usize).copied().filter(|&v| v != 255)?;
let c = TABLE.get(bytes[i+2] as usize).copied().filter(|&v| v != 255)?;
let d = TABLE.get(bytes[i+3] as usize).copied().filter(|&v| v != 255)?;
let n = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | (d as u32);
out.push((n >> 16) as u8);
out.push((n >> 8) as u8);
out.push(n as u8);
i += 4;
}
// Handle remaining bytes
if i + 2 == bytes.len() {
let a = TABLE.get(bytes[i] as usize).copied().filter(|&v| v != 255)?;
let b = TABLE.get(bytes[i+1] as usize).copied().filter(|&v| v != 255)?;
out.push(((a as u32) << 2 | (b as u32) >> 4) as u8);
} else if i + 3 == bytes.len() {
let a = TABLE.get(bytes[i] as usize).copied().filter(|&v| v != 255)?;
let b = TABLE.get(bytes[i+1] as usize).copied().filter(|&v| v != 255)?;
let c = TABLE.get(bytes[i+2] as usize).copied().filter(|&v| v != 255)?;
let n = ((a as u32) << 10) | ((b as u32) << 4) | ((c as u32) >> 2);
out.push((n >> 8) as u8);
out.push(n as u8);
}
Some(out)
}
fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.bytes()
.zip(b.bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
// ── Minimal JSON field extractors ─────────────────────────────────────────────
fn extract_str(json: &str, key: &str) -> Option<String> {
let pattern = format!("\"{}\":\"", key);
let start = json.find(&pattern)? + pattern.len();
let rest = &json[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn extract_u64(json: &str, key: &str) -> Option<u64> {
let pattern = format!("\"{}\":", key);
let start = json.find(&pattern)? + pattern.len();
let rest = &json[start..];
let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
rest[..end].parse().ok()
}
fn extract_str_array(json: &str, key: &str) -> Vec<String> {
let pattern = format!("\"{}\":[", key);
let start = match json.find(&pattern) {
None => return Vec::new(),
Some(s) => s + pattern.len(),
};
let rest = &json[start..];
let end = rest.find(']').unwrap_or(rest.len());
let content = &rest[..end];
content
.split(',')
.filter_map(|s| {
let s = s.trim().trim_matches('"');
if s.is_empty() { None } else { Some(s.to_string()) }
})
.collect()
}
+68
View File
@@ -0,0 +1,68 @@
//! el-auth — Built-in authentication and authorization for el-ui.
//!
//! Not a library you add. Native to the framework.
//!
//! ```toml
//! [auth]
//! provider = "jwt"
//! jwt_secret_env = "JWT_SECRET"
//! session_store = "memory" # or "engram"
//! ```
//!
//! ## Engram-native sessions
//!
//! Use `EngramSessionStore` (in `engram_session`) for sessions backed by the
//! Engram identity graph. Sessions are graph nodes — server-side invalidation
//! works even with stateless JWTs.
pub mod context;
pub mod engram_session;
pub mod jwt;
pub mod middleware;
pub mod roles;
pub mod session;
pub use context::{AuthContext, AuthUser};
pub use engram_session::EngramSessionStore;
pub use jwt::{JwtClaims, JwtProvider};
pub use middleware::AuthMiddleware;
pub use roles::{Permission, Role, RoleRegistry};
pub use session::SessionProvider;
#[cfg(test)]
mod tests;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AuthError {
#[error("invalid credentials")]
InvalidCredentials,
#[error("token expired")]
TokenExpired,
#[error("token invalid: {0}")]
TokenInvalid(String),
#[error("session not found")]
SessionNotFound,
#[error("forbidden: requires permission '{0}'")]
Forbidden(String),
#[error("auth configuration error: {0}")]
Config(String),
}
pub type AuthResult<T> = Result<T, AuthError>;
/// The AuthProvider trait — implemented by JWT, Session, OAuth providers.
pub trait AuthProvider: Send + Sync {
/// The provider name (e.g., "jwt", "session").
fn name(&self) -> &'static str;
/// Verify a token/session string and return the auth context.
fn verify(&self, token: &str) -> AuthResult<AuthContext>;
/// Issue a new token/session for an authenticated user.
fn issue(&self, user: AuthUser, role_registry: &RoleRegistry) -> AuthResult<String>;
/// Revoke a token/session (for logout).
fn revoke(&self, token: &str) -> AuthResult<()>;
}
+59
View File
@@ -0,0 +1,59 @@
//! Auth middleware — extracts and verifies auth tokens from requests.
//!
//! In an axum application:
//! ```text
//! let app = Router::new()
//! .route("/api/users", get(list_users))
//! .layer(AuthMiddleware::new(jwt_provider));
//! ```
//!
//! The middleware populates `AuthContext` from the `Authorization` header.
use crate::{AuthContext, AuthProvider, AuthResult};
use std::sync::Arc;
/// Auth middleware — wraps an auth provider to extract context from HTTP headers.
pub struct AuthMiddleware {
provider: Arc<dyn AuthProvider>,
}
impl AuthMiddleware {
pub fn new(provider: Arc<dyn AuthProvider>) -> Self {
Self { provider }
}
/// Extract and verify the auth token from an Authorization header value.
///
/// Supported formats:
/// - `Bearer <token>` — JWT or opaque token
/// - `Session <session_id>` — server-side session
pub fn authenticate_from_header(&self, authorization: Option<&str>) -> AuthResult<AuthContext> {
match authorization {
None => Ok(AuthContext::anonymous()),
Some(header) => {
let token = if let Some(t) = header.strip_prefix("Bearer ") {
t.trim()
} else if let Some(t) = header.strip_prefix("Session ") {
t.trim()
} else {
header.trim()
};
self.provider.verify(token)
}
}
}
/// Authenticate from a query parameter (for WebSocket upgrades where
/// Authorization headers can't be set from JS).
pub fn authenticate_from_query_param(&self, token: Option<&str>) -> AuthResult<AuthContext> {
match token {
None => Ok(AuthContext::anonymous()),
Some(t) => self.provider.verify(t),
}
}
/// Get the underlying provider name.
pub fn provider_name(&self) -> &'static str {
self.provider.name()
}
}
+90
View File
@@ -0,0 +1,90 @@
//! Role and permission model.
use std::collections::HashMap;
/// A fine-grained permission (e.g., "read", "write", "delete").
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Permission(pub String);
impl Permission {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Permission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// A role that grants a set of permissions.
#[derive(Debug, Clone)]
pub struct Role {
pub name: String,
pub permissions: Vec<Permission>,
}
impl Role {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
permissions: Vec::new(),
}
}
pub fn with_permission(mut self, perm: impl Into<String>) -> Self {
self.permissions.push(Permission::new(perm));
self
}
pub fn with_permissions(mut self, perms: Vec<impl Into<String>>) -> Self {
self.permissions.extend(perms.into_iter().map(Permission::new));
self
}
pub fn has_permission(&self, perm: &str) -> bool {
self.permissions.iter().any(|p| p.0 == perm)
}
}
/// The registry of all roles in the application.
#[derive(Debug, Default)]
pub struct RoleRegistry {
roles: HashMap<String, Role>,
}
impl RoleRegistry {
pub fn new() -> Self {
Self::default()
}
/// Register a role.
pub fn register(&mut self, role: Role) {
self.roles.insert(role.name.clone(), role);
}
/// Get a role by name.
pub fn get(&self, name: &str) -> Option<&Role> {
self.roles.get(name)
}
/// Check if the given role names grant the given permission.
pub fn has_permission(&self, role_names: &[String], permission: &str) -> bool {
role_names.iter().any(|role_name| {
self.roles
.get(role_name)
.map(|r| r.has_permission(permission))
.unwrap_or(false)
})
}
/// List all registered role names.
pub fn role_names(&self) -> Vec<&str> {
self.roles.keys().map(|s| s.as_str()).collect()
}
}

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