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 18967 additions and 12085 deletions
+50 -148
View File
@@ -22,46 +22,39 @@ jobs:
- name: Install build dependencies - name: Install build dependencies
run: | run: |
apt-get update -qq apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates apt-get install -y gcc libcurl4-openssl-dev
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
# Seed: use the committed linux-amd64 binary as the bootstrap # Gen2: compile the bootstrap C source into a working elc binary
- name: Bootstrap from committed linux binary (seed) - name: Build elc from bootstrap (gen2)
run: | run: |
chmod +x dist/platform/elc-linux-amd64 # -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
echo "seed elc (committed linux-amd64 binary)" # is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
dist/platform/elc-linux-amd64 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \ gcc -O2 \
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elc-gen2.c \ dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.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 -o dist/platform/elc
chmod +x 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 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 - name: Run tests - text
run: | run: |
ELC="$(pwd)/dist/platform/elc" \ ELC="$(pwd)/dist/platform/elc" \
@@ -87,23 +80,14 @@ jobs:
bash tests/html_sanitizer/run.sh bash tests/html_sanitizer/run.sh
# Native El test suites (elc --test, compile-link-run) # 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) - name: Run tests - native (core)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c "$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 \ 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 /tmp/el_native_core
- name: Run tests - native (text) - name: Run tests - native (text)
@@ -112,8 +96,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c "$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 \ 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 /tmp/el_native_text
- name: Run tests - native (string) - name: Run tests - native (string)
@@ -122,8 +106,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c "$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 \ 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 /tmp/el_native_string
- name: Run tests - native (math) - name: Run tests - native (math)
@@ -132,8 +116,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c "$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 \ 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 /tmp/el_native_math
- name: Run tests - native (state) - name: Run tests - native (state)
@@ -142,8 +126,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c "$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 \ 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 /tmp/el_native_state
- name: Run tests - native (time) - name: Run tests - native (time)
@@ -152,8 +136,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c "$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 \ 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 /tmp/el_native_time
- name: Run tests - native (json) - name: Run tests - native (json)
@@ -162,8 +146,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c "$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 \ 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 /tmp/el_native_json
- name: Run tests - native (env) - name: Run tests - native (env)
@@ -172,8 +156,8 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c "$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 \ 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 /tmp/el_native_env
- name: Run tests - native (fs) - name: Run tests - native (fs)
@@ -182,32 +166,10 @@ jobs:
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c "$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 \ 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 /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 # Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (dev) - name: Publish El SDK to Artifact Registry (dev)
if: github.event_name == 'push' if: github.event_name == 'push'
@@ -215,16 +177,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: | run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json 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 auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695 gcloud config set project neuron-785695
VERSION="${GITHUB_SHA:0:8}" VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \ gcloud artifacts generic upload \
--repository=foundation-dev \ --repository=foundation-dev \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-elc \ --package=el/elc \
--version="${VERSION}" \ --version="${VERSION}" \
--source=dist/platform/elc --source=dist/platform/elc
@@ -232,15 +198,7 @@ jobs:
--repository=foundation-dev \ --repository=foundation-dev \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-elb \ --package=el/el_runtime.c \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c --source=el-compiler/runtime/el_runtime.c
@@ -248,65 +206,9 @@ jobs:
--repository=foundation-dev \ --repository=foundation-dev \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-runtime-h \ --package=el/el_runtime.h \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h --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" 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 rm -f /tmp/gcp-key.json
+39 -111
View File
@@ -34,25 +34,35 @@ jobs:
apt-get update -qq apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev apt-get install -y gcc libcurl4-openssl-dev
# Seed: use the committed linux-amd64 binary as the bootstrap # Gen2: compile the bootstrap C source into a working elc binary
- name: Bootstrap from committed linux binary (seed) - name: Build elc from bootstrap (gen2)
run: | run: |
chmod +x dist/platform/elc-linux-amd64 # -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
echo "seed elc (committed linux-amd64 binary)" # is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
dist/platform/elc-linux-amd64 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \ gcc -O2 \
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elc-gen2.c \ dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.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 -o dist/platform/elc
chmod +x 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 dist/platform/elc --version || true
- name: Run tests - text - name: Run tests - text
@@ -87,7 +97,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c "$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" \ 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 /tmp/el_native_core
- name: Run tests - native (text) - name: Run tests - native (text)
@@ -97,7 +107,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c "$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" \ 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 /tmp/el_native_text
- name: Run tests - native (string) - name: Run tests - native (string)
@@ -107,7 +117,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c "$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" \ 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 /tmp/el_native_string
- name: Run tests - native (math) - name: Run tests - native (math)
@@ -117,7 +127,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c "$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" \ 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 /tmp/el_native_math
- name: Run tests - native (state) - name: Run tests - native (state)
@@ -127,7 +137,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c "$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" \ 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 /tmp/el_native_state
- name: Run tests - native (time) - name: Run tests - native (time)
@@ -137,7 +147,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c "$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" \ 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 /tmp/el_native_time
- name: Run tests - native (json) - name: Run tests - native (json)
@@ -147,7 +157,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c "$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" \ 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 /tmp/el_native_json
- name: Run tests - native (env) - name: Run tests - native (env)
@@ -157,7 +167,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c "$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" \ 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 /tmp/el_native_env
- name: Run tests - native (fs) - name: Run tests - native (fs)
@@ -167,45 +177,9 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c "$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" \ 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 /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 # Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (stage) - name: Publish El SDK to Artifact Registry (stage)
if: github.event_name == 'push' if: github.event_name == 'push'
@@ -213,19 +187,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: | run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list 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 apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695 gcloud config set project neuron-785695
VERSION="${GITHUB_SHA:0:8}" VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \ gcloud artifacts generic upload \
--repository=foundation-stage \ --repository=foundation-stage \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-elc \ --package=el/elc \
--version="${VERSION}" \ --version="${VERSION}" \
--source=dist/platform/elc --source=dist/platform/elc
@@ -233,7 +208,7 @@ jobs:
--repository=foundation-stage \ --repository=foundation-stage \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-runtime-c \ --package=el/el_runtime.c \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c --source=el-compiler/runtime/el_runtime.c
@@ -241,56 +216,9 @@ jobs:
--repository=foundation-stage \ --repository=foundation-stage \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-runtime-h \ --package=el/el_runtime.h \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h --source=el-compiler/runtime/el_runtime.h
echo "Published El SDK version=${VERSION} to foundation-stage" 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 rm -f /tmp/gcp-key.json
+59 -108
View File
@@ -34,26 +34,35 @@ jobs:
apt-get update -qq apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev apt-get install -y gcc libcurl4-openssl-dev
# Seed: use the committed linux-amd64 binary as the bootstrap # Gen2: compile the bootstrap C source into a working elc binary
- name: Bootstrap from committed linux binary (seed) - name: Build elc from bootstrap (gen2)
run: | run: |
chmod +x dist/platform/elc-linux-amd64 # -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
echo "seed elc (committed linux-amd64 binary)" # is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
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
gcc -O2 \ gcc -O2 \
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elc-gen2.c \ dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.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 -o dist/platform/elc
chmod +x 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 dist/platform/elc --version || true
# Build elb binary # Build elb binary
@@ -65,30 +74,34 @@ jobs:
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elb.c \ dist/elb.c \
el-compiler/runtime/el_runtime.c \ el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \ -lcurl -lpthread -lm \
-o dist/bin/elb -o dist/bin/elb
chmod +x dist/bin/elb chmod +x dist/bin/elb
echo "elb built" 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 - name: Build epm
run: | run: |
ABS_ELB="$(pwd)/dist/bin/elb" dist/platform/elc ../epm/src/epm.el > dist/epm.c
ABS_ELC="$(pwd)/dist/platform/elc" gcc -O2 \
ABS_RUNTIME="$(pwd)/el-compiler/runtime" -I el-compiler/runtime \
ABS_OUT="$(pwd)/dist/bin" dist/epm.c \
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT") el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-o dist/bin/epm
chmod +x dist/bin/epm chmod +x dist/bin/epm
echo "epm built" echo "epm built"
# Build el-install binary using elb # Build el-install binary
- name: Build el-install - name: Build el-install
run: | run: |
ABS_ELB="$(pwd)/dist/bin/elb" dist/platform/elc tools/install/el-install.el > dist/el-install.c
ABS_ELC="$(pwd)/dist/platform/elc" gcc -O2 \
ABS_RUNTIME="$(pwd)/el-compiler/runtime" -I el-compiler/runtime \
ABS_OUT="$(pwd)/dist/bin" dist/el-install.c \
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT") el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-o dist/bin/el-install
chmod +x dist/bin/el-install chmod +x dist/bin/el-install
echo "el-install built" echo "el-install built"
@@ -124,7 +137,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c "$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" \ 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 /tmp/el_native_core
- name: Run tests - native (text) - name: Run tests - native (text)
@@ -134,7 +147,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c "$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" \ 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 /tmp/el_native_text
- name: Run tests - native (string) - name: Run tests - native (string)
@@ -144,7 +157,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c "$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" \ 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 /tmp/el_native_string
- name: Run tests - native (math) - name: Run tests - native (math)
@@ -154,7 +167,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c "$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" \ 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 /tmp/el_native_math
- name: Run tests - native (state) - name: Run tests - native (state)
@@ -164,7 +177,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c "$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" \ 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 /tmp/el_native_state
- name: Run tests - native (time) - name: Run tests - native (time)
@@ -174,7 +187,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c "$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" \ 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 /tmp/el_native_time
- name: Run tests - native (json) - name: Run tests - native (json)
@@ -184,7 +197,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c "$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" \ 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 /tmp/el_native_json
- name: Run tests - native (env) - name: Run tests - native (env)
@@ -194,7 +207,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c "$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" \ 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 /tmp/el_native_env
- name: Run tests - native (fs) - name: Run tests - native (fs)
@@ -204,7 +217,7 @@ jobs:
RUNTIME="$(pwd)/el-compiler/runtime" RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c "$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" \ 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 /tmp/el_native_fs
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly # Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
@@ -228,7 +241,7 @@ jobs:
if: github.event_name == 'push' if: github.event_name == 'push'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
env: env:
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1 GITEA_API: https://git.neuralplatform.ai/api/v1
REPO: neuron-technologies/el REPO: neuron-technologies/el
run: | run: |
@@ -289,19 +302,20 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: | run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list 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 apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695 gcloud config set project neuron-785695
VERSION="${GITHUB_SHA:0:8}" VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \ gcloud artifacts generic upload \
--repository=foundation-prod \ --repository=foundation-prod \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-elc \ --package=el/elc \
--version="${VERSION}" \ --version="${VERSION}" \
--source=dist/platform/elc --source=dist/platform/elc
@@ -309,15 +323,7 @@ jobs:
--repository=foundation-prod \ --repository=foundation-prod \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-elb \ --package=el/el_runtime.c \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c --source=el-compiler/runtime/el_runtime.c
@@ -325,75 +331,20 @@ jobs:
--repository=foundation-prod \ --repository=foundation-prod \
--location=us-central1 \ --location=us-central1 \
--project=neuron-785695 \ --project=neuron-785695 \
--package=el-runtime-h \ --package=el/el_runtime.h \
--version="${VERSION}" \ --version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h --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" 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 rm -f /tmp/gcp-key.json
- name: Dispatch el-sdk-updated to downstream repos - name: Dispatch el-sdk-updated to downstream repos
if: github.event_name == 'push' if: github.event_name == 'push'
env: env:
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1 GITEA_API: https://git.neuralplatform.ai/api/v1
run: | run: |
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do for repo in neuron-technologies/forge; do
curl -sf -X POST \ curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
+46 -46
View File
@@ -1,46 +1,46 @@
// auto-generated by elc --emit-header do not edit // 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_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: [String], key: String) -> String extern fn lang_get(profile: Any, key: String) -> String
extern fn lang_profile_en() -> [String] extern fn lang_profile_en() -> Any
extern fn lang_profile_ja() -> [String] extern fn lang_profile_ja() -> Any
extern fn lang_profile_ar() -> [String] extern fn lang_profile_ar() -> Any
extern fn lang_profile_zh() -> [String] extern fn lang_profile_zh() -> Any
extern fn lang_profile_de() -> [String] extern fn lang_profile_de() -> Any
extern fn lang_profile_es() -> [String] extern fn lang_profile_es() -> Any
extern fn lang_profile_fi() -> [String] extern fn lang_profile_fi() -> Any
extern fn lang_profile_sw() -> [String] extern fn lang_profile_sw() -> Any
extern fn lang_profile_hi() -> [String] extern fn lang_profile_hi() -> Any
extern fn lang_profile_ru() -> [String] extern fn lang_profile_ru() -> Any
extern fn lang_profile_fr() -> [String] extern fn lang_profile_fr() -> Any
extern fn lang_profile_la() -> [String] extern fn lang_profile_la() -> Any
extern fn lang_profile_he() -> [String] extern fn lang_profile_he() -> Any
extern fn lang_profile_sa() -> [String] extern fn lang_profile_sa() -> Any
extern fn lang_profile_got() -> [String] extern fn lang_profile_got() -> Any
extern fn lang_profile_non() -> [String] extern fn lang_profile_non() -> Any
extern fn lang_profile_enm() -> [String] extern fn lang_profile_enm() -> Any
extern fn lang_profile_pi() -> [String] extern fn lang_profile_pi() -> Any
extern fn lang_profile_grc() -> [String] extern fn lang_profile_grc() -> Any
extern fn lang_profile_ang() -> [String] extern fn lang_profile_ang() -> Any
extern fn lang_profile_fro() -> [String] extern fn lang_profile_fro() -> Any
extern fn lang_profile_goh() -> [String] extern fn lang_profile_goh() -> Any
extern fn lang_profile_sga() -> [String] extern fn lang_profile_sga() -> Any
extern fn lang_profile_txb() -> [String] extern fn lang_profile_txb() -> Any
extern fn lang_profile_peo() -> [String] extern fn lang_profile_peo() -> Any
extern fn lang_profile_akk() -> [String] extern fn lang_profile_akk() -> Any
extern fn lang_profile_uga() -> [String] extern fn lang_profile_uga() -> Any
extern fn lang_profile_egy() -> [String] extern fn lang_profile_egy() -> Any
extern fn lang_profile_sux() -> [String] extern fn lang_profile_sux() -> Any
extern fn lang_profile_gez() -> [String] extern fn lang_profile_gez() -> Any
extern fn lang_profile_cop() -> [String] extern fn lang_profile_cop() -> Any
extern fn lang_from_code(code: String) -> [String] extern fn lang_from_code(code: String) -> Any
extern fn lang_default() -> [String] extern fn lang_default() -> Any
extern fn lang_is_isolating(profile: [String]) -> Bool extern fn lang_is_isolating(profile: Any) -> Bool
extern fn lang_is_agglutinative(profile: [String]) -> Bool extern fn lang_is_agglutinative(profile: Any) -> Bool
extern fn lang_is_fusional(profile: [String]) -> Bool extern fn lang_is_fusional(profile: Any) -> Bool
extern fn lang_is_polysynthetic(profile: [String]) -> Bool extern fn lang_is_polysynthetic(profile: Any) -> Bool
extern fn lang_is_rtl(profile: [String]) -> Bool extern fn lang_is_rtl(profile: Any) -> Bool
extern fn lang_has_null_subject(profile: [String]) -> Bool extern fn lang_has_null_subject(profile: Any) -> Bool
extern fn lang_has_case(profile: [String]) -> Bool extern fn lang_has_case(profile: Any) -> Bool
extern fn lang_has_gender(profile: [String]) -> Bool extern fn lang_has_gender(profile: Any) -> Bool
extern fn lang_word_order(profile: [String]) -> String extern fn lang_word_order(profile: Any) -> String
extern fn lang_code(profile: [String]) -> String extern fn lang_code(profile: Any) -> String
-1
View File
@@ -56,7 +56,6 @@
// String helpers // String helpers
import "morphology.el"
fn akk_str_ends(s: String, suf: String) -> Bool { fn akk_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn akk_str_len(s: String) -> Int extern fn akk_str_len(s: String) -> Int
extern fn akk_str_drop_last(s: String, n: Int) -> String extern fn akk_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -36,7 +36,6 @@
// String helpers // String helpers
import "morphology.el"
fn ang_str_ends(s: String, suf: String) -> Bool { fn ang_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn ang_str_drop_last(s: String, n: Int) -> String extern fn ang_str_drop_last(s: String, n: Int) -> String
extern fn ang_str_last_char(s: String) -> String extern fn ang_str_last_char(s: String) -> String
-1
View File
@@ -21,7 +21,6 @@
// String helpers // String helpers
import "morphology.el"
fn ar_str_ends(s: String, suf: String) -> Bool { fn ar_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn ar_str_len(s: String) -> Int extern fn ar_str_len(s: String) -> Int
extern fn ar_str_drop_last(s: String, n: Int) -> String extern fn ar_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -54,7 +54,6 @@
// String helpers // String helpers
import "morphology.el"
fn cop_str_ends(s: String, suf: String) -> Bool { fn cop_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn cop_str_len(s: String) -> Int extern fn cop_str_len(s: String) -> Int
extern fn cop_drop(s: String, n: Int) -> String extern fn cop_drop(s: String, n: Int) -> String
-1
View File
@@ -26,7 +26,6 @@
// Dat: dem der dem den // Dat: dem der dem den
// Gen: des der des der // Gen: des der des der
import "morphology.el"
fn de_article_def(gender: String, gram_case: String, number: String) -> String { fn de_article_def(gender: String, gram_case: String, number: String) -> String {
if str_eq(number, "pl") { if str_eq(number, "pl") {
if str_eq(gram_case, "nom") { return "die" } 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_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_indef(gender: String, gram_case: String, number: String) -> String
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
-1
View File
@@ -52,7 +52,6 @@
// String helpers // String helpers
import "morphology.el"
fn egy_str_ends(s: String, suf: String) -> Bool { fn egy_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn egy_str_len(s: String) -> Int extern fn egy_str_len(s: String) -> Int
extern fn egy_drop(s: String, n: Int) -> String extern fn egy_drop(s: String, n: Int) -> String
-1
View File
@@ -31,7 +31,6 @@
// String helpers // String helpers
import "morphology.el"
fn enm_str_ends(s: String, suf: String) -> Bool { fn enm_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_str_ends(s: String, suf: String) -> Bool
extern fn enm_drop(s: String, n: Int) -> String extern fn enm_drop(s: String, n: Int) -> String
extern fn enm_first_char(s: String) -> String extern fn enm_first_char(s: String) -> String
-1
View File
@@ -12,7 +12,6 @@
// String helpers (local, matching morphology.el conventions) // String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn es_str_ends(s: String, suf: String) -> Bool { fn es_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn es_str_drop_last(s: String, n: Int) -> String extern fn es_str_drop_last(s: String, n: Int) -> String
extern fn es_str_last_char(s: String) -> 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 // If only neutral vowels are found, default to "front" (the conservative choice
// for borrowed words and those without clear back vowels). // for borrowed words and those without clear back vowels).
import "morphology.el"
fn fi_harmony(word: String) -> String { fn fi_harmony(word: String) -> String {
let n: Int = str_len(word) let n: Int = str_len(word)
let i: Int = n - 1 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_harmony(word: String) -> String
extern fn fi_suffix(base: String, harmony: 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_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
extern fn fi_str_last_char(s: 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_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_verb_stem(dict_form: 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_present_ending(stem: String, person: String, number: String, harmony: String) -> String
extern fn fi_past_stem(stem: String) -> String extern fn fi_past_stem(stem: String) -> String
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String extern fn fi_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_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fi_question_suffix(harmony: String) -> String extern fn fi_question_suffix(harmony: String) -> String
extern fn fi_make_question(verb_form: String, 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) // String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn fr_str_ends(s: String, suf: String) -> Bool { fn fr_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn fr_str_drop_last(s: String, n: Int) -> String extern fn fr_str_drop_last(s: String, n: Int) -> String
extern fn fr_str_last_char(s: String) -> String extern fn fr_str_last_char(s: String) -> String
-1
View File
@@ -53,7 +53,6 @@
// String helpers // String helpers
import "morphology.el"
fn fro_str_ends(s: String, suf: String) -> Bool { fn fro_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_str_ends(s: String, suf: String) -> Bool
extern fn fro_drop(s: String, n: Int) -> String extern fn fro_drop(s: String, n: Int) -> String
extern fn fro_slot(person: String, number: String) -> Int extern fn fro_slot(person: String, number: String) -> Int
-1
View File
@@ -64,7 +64,6 @@
// String helpers // String helpers
import "morphology.el"
fn gez_str_ends(s: String, suf: String) -> Bool { fn gez_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn gez_str_len(s: String) -> Int extern fn gez_str_len(s: String) -> Int
extern fn gez_str_drop_last(s: String, n: Int) -> String extern fn gez_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -48,7 +48,6 @@
// String helpers // String helpers
import "morphology.el"
fn goh_str_ends(s: String, suf: String) -> Bool { fn goh_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_str_ends(s: String, suf: String) -> Bool
extern fn goh_drop(s: String, n: Int) -> String extern fn goh_drop(s: String, n: Int) -> String
extern fn goh_slot(person: String, number: String) -> Int extern fn goh_slot(person: String, number: String) -> Int
-1
View File
@@ -49,7 +49,6 @@
// String helpers // String helpers
import "morphology.el"
fn got_str_ends(s: String, suf: String) -> Bool { fn got_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn got_str_drop_last(s: String, n: Int) -> String extern fn got_str_drop_last(s: String, n: Int) -> String
extern fn got_slot(person: String, number: String) -> Int extern fn got_slot(person: String, number: String) -> Int
-1
View File
@@ -31,7 +31,6 @@
// String helpers // String helpers
import "morphology.el"
fn grc_str_ends(s: String, suf: String) -> Bool { fn grc_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn grc_str_drop_last(s: String, n: Int) -> String extern fn grc_str_drop_last(s: String, n: Int) -> String
extern fn grc_str_last_char(s: String) -> String extern fn grc_str_last_char(s: String) -> String
-1
View File
@@ -51,7 +51,6 @@
// String helpers // String helpers
import "morphology.el"
fn he_str_ends(s: String, suf: String) -> Bool { fn he_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn he_str_len(s: String) -> Int extern fn he_str_len(s: String) -> Int
extern fn he_str_drop_last(s: String, n: Int) -> String extern fn he_str_drop_last(s: String, n: Int) -> String
-1
View File
@@ -24,7 +24,6 @@
// String helpers // String helpers
import "morphology.el"
fn hi_str_ends(s: String, suf: String) -> Bool { fn hi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn hi_str_drop_last(s: String, n: Int) -> String extern fn hi_str_drop_last(s: String, n: Int) -> String
extern fn hi_str_last_char(s: String) -> 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 // Note: this is a heuristic classifier for romanized input. For production use
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted. // with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
import "morphology.el"
fn ja_verb_group(dict_form: String) -> String { fn ja_verb_group(dict_form: String) -> String {
// Irregular verbs (exact match on dictionary form) // Irregular verbs (exact match on dictionary form)
if str_eq(dict_form, "する") { return "irregular" } 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_verb_group(dict_form: String) -> String
extern fn ja_ichidan_stem(dict_form: String) -> String extern fn ja_ichidan_stem(dict_form: String) -> String
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
-1
View File
@@ -25,7 +25,6 @@
// String helpers // String helpers
import "morphology.el"
fn la_str_ends(s: String, suf: String) -> Bool { fn la_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn la_str_drop_last(s: String, n: Int) -> String extern fn la_str_drop_last(s: String, n: Int) -> String
extern fn la_str_last_char(s: String) -> String extern fn la_str_last_char(s: String) -> String
-1
View File
@@ -27,7 +27,6 @@
// String helpers // String helpers
import "morphology.el"
fn non_str_ends(s: String, suf: String) -> Bool { fn non_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_str_ends(s: String, suf: String) -> Bool
extern fn non_drop(s: String, n: Int) -> String extern fn non_drop(s: String, n: Int) -> String
extern fn non_last(s: String) -> String extern fn non_last(s: String) -> String
-1
View File
@@ -31,7 +31,6 @@
// String helpers // String helpers
import "morphology.el"
fn peo_drop(s: String, n: Int) -> String { fn peo_drop(s: String, n: Int) -> String {
let len: Int = str_len(s) let len: Int = str_len(s)
if n >= len { return "" } 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_drop(s: String, n: Int) -> String
extern fn peo_ends(s: String, suf: String) -> Bool extern fn peo_ends(s: String, suf: String) -> Bool
extern fn peo_slot(person: String, number: String) -> Int extern fn peo_slot(person: String, number: String) -> Int
-1
View File
@@ -30,7 +30,6 @@
// String helpers // String helpers
import "morphology.el"
fn pi_str_ends(s: String, suf: String) -> Bool { fn pi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_str_ends(s: String, suf: String) -> Bool
extern fn pi_drop(s: String, n: Int) -> String extern fn pi_drop(s: String, n: Int) -> String
extern fn pi_last_char(s: String) -> 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 // The heuristic returns the most probable gender. Caller should override
// for known exceptions (путь, рубль are masc despite ). // for known exceptions (путь, рубль are masc despite ).
import "morphology.el"
fn ru_gender(noun: String) -> String { fn ru_gender(noun: String) -> String {
let n: Int = str_len(noun) let n: Int = str_len(noun)
if n == 0 { return "m" } 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_gender(noun: String) -> String
extern fn ru_stem_type(noun: String, gender: String) -> String extern fn ru_stem_type(noun: String, gender: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
-1
View File
@@ -42,7 +42,6 @@
// String helpers // String helpers
import "morphology.el"
fn sa_str_ends(s: String, suf: String) -> Bool { fn sa_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn sa_str_drop_last(s: String, n: Int) -> String extern fn sa_str_drop_last(s: String, n: Int) -> String
extern fn sa_slot(person: String, number: String) -> Int extern fn sa_slot(person: String, number: String) -> Int
-1
View File
@@ -31,7 +31,6 @@
// String helpers // String helpers
import "morphology.el"
fn sga_drop(s: String, n: Int) -> String { fn sga_drop(s: String, n: Int) -> String {
let len: Int = str_len(s) let len: Int = str_len(s)
if n >= len { return "" } 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_drop(s: String, n: Int) -> String
extern fn sga_first(s: String) -> String extern fn sga_first(s: String) -> String
extern fn sga_rest(s: String) -> String extern fn sga_rest(s: String) -> String
-1
View File
@@ -53,7 +53,6 @@
// String helpers // String helpers
import "morphology.el"
fn sux_str_ends(s: String, suf: String) -> Bool { fn sux_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn sux_str_drop_last(s: String, n: Int) -> String extern fn sux_str_drop_last(s: String, n: Int) -> String
extern fn sux_str_last_char(s: String) -> String extern fn sux_str_last_char(s: String) -> String
-1
View File
@@ -24,7 +24,6 @@
// String helpers // String helpers
import "morphology.el"
fn sw_str_ends(s: String, suf: String) -> Bool { fn sw_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn sw_str_drop_last(s: String, n: Int) -> String extern fn sw_str_drop_last(s: String, n: Int) -> String
extern fn sw_str_first_char(s: String) -> String extern fn sw_str_first_char(s: String) -> String
-1
View File
@@ -30,7 +30,6 @@
// String helpers // String helpers
import "morphology.el"
fn txb_drop(s: String, n: Int) -> String { fn txb_drop(s: String, n: Int) -> String {
let len: Int = str_len(s) let len: Int = str_len(s)
if n >= len { return "" } 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_drop(s: String, n: Int) -> String
extern fn txb_ends(s: String, suf: String) -> Bool extern fn txb_ends(s: String, suf: String) -> Bool
extern fn txb_slot(person: String, number: String) -> Int extern fn txb_slot(person: String, number: String) -> Int
-1
View File
@@ -48,7 +48,6 @@
// String helpers // String helpers
import "morphology.el"
fn uga_str_ends(s: String, suf: String) -> Bool { fn uga_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn uga_str_len(s: String) -> Int extern fn uga_str_len(s: String) -> Int
extern fn uga_str_drop_last(s: String, n: Int) -> String extern fn uga_str_drop_last(s: String, n: Int) -> String
-11
View File
@@ -33,17 +33,6 @@
// String helpers // 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 { fn str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf) 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_ends(s: String, suf: String) -> Bool
extern fn str_last_char(s: String) -> String extern fn str_last_char(s: String) -> String
extern fn str_last2(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 morph_apply_suffix(base: String, suffix: String) -> String
extern fn en_irregular_plural(word: String) -> String extern fn en_irregular_plural(word: String) -> String
extern fn en_irregular_singular(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_verb_3sg(base: String) -> String
extern fn en_should_double_final(base: String) -> Bool extern fn en_should_double_final(base: String) -> Bool
extern fn en_verb_past(base: String) -> String 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_pluralize_regular(singular: String) -> String
extern fn en_verb_form(base: String, tense: String, person: String, number: 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 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_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_conjugate(verb: String, tense: String, person: String, number: String, profile: Any) -> String
extern fn morph_inflect(word: String, features: String, profile: [String]) -> String extern fn morph_inflect(word: String, features: String, profile: Any) -> String
extern fn pluralize(singular: String) -> String extern fn pluralize(singular: String) -> String
extern fn singularize(plural: String) -> String extern fn singularize(plural: String) -> String
extern fn verb_form(base: String, tense: String, person: String, number: 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 // auto-generated by elc --emit-header - do not edit
extern fn lex_word(entry: [String]) -> String extern fn lex_word(entry: Any) -> String
extern fn lex_pos(entry: [String]) -> String extern fn lex_pos(entry: Any) -> String
extern fn lex_form(entry: [String], idx: Int) -> String extern fn lex_form(entry: Any, idx: Int) -> String
extern fn lex_class(entry: [String]) -> 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) -> [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) -> [String] 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) -> [String] 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) -> [String] extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> Any
extern fn build_vocab() -> [[String]] extern fn build_vocab() -> Any
extern fn get_vocab() -> [[String]] extern fn get_vocab() -> Any
extern fn vocab_lookup(word: String, lang_code: String) -> [String] extern fn vocab_lookup(word: String, lang_code: String) -> Any
extern fn vocab_lookup_en(word: String) -> [String] extern fn vocab_lookup_en(word: String) -> Any
extern fn vocab_synonym(word: String, lang_register: String, lang_code: String) -> String 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_pos(pos: String) -> Any
extern fn vocab_by_class(cls: String) -> [[String]] extern fn vocab_by_class(cls: String) -> Any
extern fn entry_found(entry: [String]) -> Bool extern fn entry_found(entry: Any) -> Bool
extern fn entry_word(entry: [String]) -> String extern fn entry_word(entry: Any) -> String
extern fn entry_pos(entry: [String]) -> String extern fn entry_pos(entry: Any) -> String
extern fn entry_form(entry: [String], n: Int) -> 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.
File diff suppressed because it is too large Load Diff
+7 -113
View File
@@ -81,8 +81,8 @@ extern "C" {
/* ── I/O ──────────────────────────────────────────────────────────────────── */ /* ── I/O ──────────────────────────────────────────────────────────────────── */
el_val_t println(el_val_t s); void println(el_val_t s);
el_val_t print(el_val_t s); void print(el_val_t s);
el_val_t readline(void); el_val_t readline(void);
/* ── String builtins ─────────────────────────────────────────────────────── */ /* ── 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 str_concat(el_val_t a, el_val_t b);
el_val_t int_to_str(el_val_t n); el_val_t int_to_str(el_val_t n);
el_val_t str_to_int(el_val_t s); 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_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_contains(el_val_t s, el_val_t sub);
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to); el_val_t str_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_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_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_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_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_delete(el_val_t url);
el_val_t http_serve(el_val_t port, el_val_t handler); void http_serve(el_val_t port, el_val_t handler);
el_val_t http_set_handler(el_val_t name); void http_set_handler(el_val_t name);
/* HTTP server v2 ───────────────────────────────────────────────────────────── /* HTTP server v2 ─────────────────────────────────────────────────────────────
* Same dispatch model as http_serve, but the handler signature is widened: * 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 * The 3-arg http_serve(port, handler) remains supported unchanged for
* existing handlers (e.g. products/web/server.el): it dispatches with * existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */ * (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_v2(el_val_t port, el_val_t handler);
void http_serve_async(el_val_t port, el_val_t handler); void http_set_handler_v2(el_val_t name);
el_val_t http_set_handler_v2(el_val_t name);
/* Build an HTTP response envelope. `headers_json` should be a JSON object /* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The * 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":[],...} * {"p":[],"a":["href","title"],"strong":[],...}
* where each value is the array of attribute names allowed for that tag. */ * 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 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 ──────────────────────────────────────────────────────────── */ /* ── Filesystem ──────────────────────────────────────────────────────────── */
@@ -531,15 +526,9 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
/* ── Process ─────────────────────────────────────────────────────────────── */ /* ── Process ─────────────────────────────────────────────────────────────── */
el_val_t exit_program(el_val_t code); void exit_program(el_val_t code);
el_val_t getpid_now(void); 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 ───────────────────────────────────────────────────────────── /* ── CGI identity ─────────────────────────────────────────────────────────────
* Called at the start of main() in CGI programs (those with a `cgi {}` block). * 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. */ * 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. */ * no nodes promoted to working memory. */
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); 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) ───────────────────────────────────────────── /* ── LLM (Anthropic API client) ─────────────────────────────────────────────
* All functions call https://api.anthropic.com/v1/messages with the API key * 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. */ * 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_create(el_val_t fn_name_v, el_val_t arg_v);
el_val_t __thread_join(el_val_t tid_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 #ifdef __cplusplus
} }
#endif #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_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_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_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_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(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); } 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_get_raw(el_val_t json_str, el_val_t key);
el_val_t __json_parse(el_val_t s); el_val_t __json_parse(el_val_t s);
el_val_t __json_stringify(el_val_t v); 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_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(el_val_t json_str, el_val_t index);
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index); el_val_t __json_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 each_c: String = cg_html_each(child, acc_var)
let parts = native_list_append(parts, each_c) 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 let i = i + 1
} }
str_join(parts, "") 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 + "} } " "{ 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. // Top-level HTML template codegen returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String { fn cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"] let root = expr["root"]
@@ -3730,7 +3715,6 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
cg_fn(stmt) cg_fn(stmt)
el_release(stmt) el_release(stmt)
el_arena_pop(fn_arena_mark) el_arena_pop(fn_arena_mark)
el_mem_check()
} }
} else { } else {
if is_top_level_decl(stmt) { 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. // emit_header write a .elh file from parsed statements.
// Scans for FnDef nodes and emits 'extern fn' declarations. // 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 { fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
let n: Int = native_list_len(stmts) let n: Int = native_list_len(stmts)
let i = 0 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) 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 // Import resolution
// //
// elc supports two forms of import: // elc supports two forms of import:
@@ -565,20 +536,16 @@ fn main() -> Void {
let src_path: String = native_list_get(positional, 0) let src_path: String = native_list_get(positional, 0)
// When --emit-header is requested, lex the source file and do a // When --emit-header is requested, parse the source file directly
// token-level signature scan (no full AST) to write a .elh file. // (without inlining imports) and write out a .elh file alongside the .c.
// 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).
if do_emit_header { if do_emit_header {
el_mem_check()
let raw_source: String = fs_read(src_path) let raw_source: String = fs_read(src_path)
let hdr_tokens: [Any] = lex(raw_source) 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) el_release(hdr_tokens)
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh" let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
emit_header_from_sigs(hdr_sigs, hdr_path) emit_header(hdr_stmts, hdr_path)
el_release(hdr_sigs) el_release(hdr_stmts)
} }
let source: String = resolve_imports(src_path) let source: String = resolve_imports(src_path)
+94 -497
View File
@@ -293,48 +293,6 @@ fn is_void_element(name: String) -> Bool {
false 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 // Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p } // closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> { 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})* // 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 { } else {
if str_eq(k, "LBrace") { if str_eq(k, "LBrace") {
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if} // Interpolation: {expr} or {#each ...} or {/each}
// 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.
let k2 = tok_kind(tokens, p + 1) let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") { if str_eq(k2, "Hash") {
// {/each} or {/if} end of block, stop // {#each list as item}
// skip { / let k3_v = tok_value(tokens, p + 2)
let p = p + 2 if str_eq(k3_v, "each") {
// skip the close-tag name token (each, if, etc.) let p = p + 3
let p = p + 1 // parse list expr up to "as" keyword
// 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)
let prev_no_block: String = state_get("__no_block_expr") let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1") 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) state_set("__no_block_expr", prev_no_block)
let cond_expr = r_cond["node"] let list_expr = r_list["node"]
let p = r_cond["pos"] let p = r_list["pos"]
el_release(r_cond) // 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 } // consume closing }
let p = expect(tokens, p, "RBrace") let p = expect(tokens, p, "RBrace")
// parse then-children until {#else} or {/if} // parse body until {/each}
let r_then = parse_html_children(tokens, p, "__if_then__") let r_body = parse_html_each_body(tokens, p)
let then_children = r_then["children"] let body_children = r_body["children"]
let p = r_then["pos"] let p = r_body["pos"]
el_release(r_then) // r_body result map fully consumed release to free peak heap.
// check for {#else} lexed as LBrace Else RBrace el_release(r_body)
let else_children: [Map<String, Any>] = native_list_empty() let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let ck = tok_kind(tokens, p) let children = native_list_append(children, each_node)
if str_eq(ck, "LBrace") { } else {
let ck2 = tok_kind(tokens, p + 1) let p = p + 1
if str_eq(ck2, "Else") { }
// consume { else } } else {
let p = p + 2 if str_eq(k2, "Slash") {
let p = expect(tokens, p, "RBrace") // {/each} end of each block, stop
// parse else-children until {/if} // skip {/each}
let r_else = parse_html_children(tokens, p, "__if_else__") let p = p + 2
let else_children = r_else["children"] // skip "each"
let p = r_else["pos"] let p = p + 1
el_release(r_else) // 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 } if is_raw_call {
let children = native_list_append(children, if_node) let raw_args = interp_val["args"]
} else { let raw_inner = native_list_get(raw_args, 0)
if str_eq(k2, "Else") { let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
// {#else} sentinel lexed as LBrace Else RBrace
// Do NOT consume leave position for caller ({#if} handler checks for it)
let running = false
} else { } else {
// Check for {#each list as item} lexed as LBrace Ident:"each" ... let children = native_list_append(children, { "html": "Interp", "value": interp_val })
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 })
}
}
} }
} }
} }
@@ -655,27 +565,6 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
if is_void_element(tag_name) { if is_void_element(tag_name) {
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) 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 // parse children
let r_children = parse_html_children(tokens, p, tag_name) let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"] 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. // as the start of the block they're expecting.
return make_result({ "expr": "Nil" }, pos) return make_result({ "expr": "Nil" }, pos)
} }
// Distinguish map literal from interpolation chain. let p = pos + 1
// A map literal requires { key: value } the second token inside { must be Colon. let pairs: [Map<String, Any>] = native_list_empty()
// An empty {} is a map literal. Everything else is an interpolation chain. let running = true
let first_k: String = tok_kind(tokens, pos + 1) while running {
let second_k: String = tok_kind(tokens, pos + 2) let k2 = tok_kind(tokens, p)
if str_eq(first_k, "RBrace") { if k2 == "RBrace" {
// Empty map literal {} let running = false
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2) } else {
} if k2 == "Eof" {
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 running = false let running = false
} else { } else {
if k2 == "Eof" { // key: Str token
let running = false 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 { } else {
// key: Str or Ident token let p = new_p
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 = expect(tokens, p, "RBrace")
return make_result({ "expr": "Map", "pairs": pairs }, p)
} }
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc. let p = expect(tokens, p, "RBrace")
// Build a BinOp(Plus, ...) concatenation chain. return make_result({ "expr": "Map", "pairs": pairs }, p)
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)
} }
// if expression // if expression
@@ -2065,219 +1875,6 @@ fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
p 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 // scan_params_c scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the C parameter // position `pos` (which should point at LParen) and return the C parameter
// declaration string along with the new position. // declaration string along with the new position.
+2 -53
View File
@@ -77,33 +77,6 @@ fn parse_manifest_entry(src: String) -> String {
return "" 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 { fn parse_manifest_name(src: String) -> String {
let lines: [String] = str_split(src, "\n") let lines: [String] = str_split(src, "\n")
let n: Int = native_list_len(lines) 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() let parts: [String] = native_list_empty()
// Include both the runtime dir (for el_runtime.h) and the output dir // Include both the runtime dir (for el_runtime.h) and the output dir
// (for module.elh cross-module forward declarations). // (for module.elh cross-module forward declarations).
// Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
// 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 i = 0 let i = 0
while i < n { while i < n {
let f: String = native_list_get(c_files, i) 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 i = i + 1
} }
let parts = native_list_append(parts, runtime_path) 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 parts = native_list_append(parts, "-o " + out_bin)
let cmd: String = str_join(parts, " ") let cmd: String = str_join(parts, " ")
println(" link " + out_bin) println(" link " + out_bin)
@@ -353,7 +312,6 @@ fn main() -> Void {
let pkg_name: String = parse_manifest_name(manifest_src) let pkg_name: String = parse_manifest_name(manifest_src)
let entry: String = parse_manifest_entry(manifest_src) let entry: String = parse_manifest_entry(manifest_src)
let extra_c: [String] = parse_manifest_c_sources(manifest_src)
if str_eq(entry, "") { if str_eq(entry, "") {
println("elb: manifest.el has no 'entry' declaration") println("elb: manifest.el has no 'entry' declaration")
exit(1) exit(1)
@@ -432,15 +390,6 @@ fn main() -> Void {
exit(1) 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 // Link
let out_bin: String = out_dir + "/" + pkg_name let out_bin: String = out_dir + "/" + pkg_name
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run) 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 // 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 --- // --- Node creation ---
fn engram_node(content: String, node_type: String, salience: Float) -> String { 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) return __engram_node(content, node_type, salience)
} }
// Signature MUST match the C primitive __engram_node_full exactly (el_seed.h): fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
// (content, node_type, label, salience, importance, confidence, tier, tags) source: String, lang: String, ts: Int, tags: String) -> String {
// The previous wrapper declared a stale 8-arg schema with wrong names AND types return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
// (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)
} }
// --- Node retrieval --- // --- Node retrieval ---
+1 -1
View File
@@ -44,7 +44,7 @@ run_runtime_case() {
fi fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ 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:" echo "FAIL ${name} — cc failed:"
cat /tmp/cal_test.cc.err | sed 's/^/ /' cat /tmp/cal_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1)) 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" echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${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" echo "==> Running"
"${OUT_BIN}" "${OUT_BIN}"
+1 -1
View File
@@ -42,7 +42,7 @@ run_runtime_case() {
fi fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ 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:" echo "FAIL ${name} — cc failed:"
cat /tmp/text_test.cc.err | sed 's/^/ /' cat /tmp/text_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1)) FAIL=$((FAIL+1))
+2 -2
View File
@@ -55,7 +55,7 @@ run_runtime_case() {
fi fi
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ 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:" echo "FAIL ${name} — cc failed:"
cat /tmp/time_test.cc.err | sed 's/^/ /' cat /tmp/time_test.cc.err | sed 's/^/ /'
FAIL=$((FAIL+1)) FAIL=$((FAIL+1))
@@ -116,7 +116,7 @@ run_typeerror_case() {
fi fi
if cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ 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" echo "FAIL ${name} — cc unexpectedly succeeded; type rule did not fire"
FAIL=$((FAIL+1)) FAIL=$((FAIL+1))
FAILED_NAMES+=("${name}") FAILED_NAMES+=("${name}")
+9
View File
@@ -10,6 +10,15 @@
// export PATH="$HOME/.el/bin:$PATH" // export PATH="$HOME/.el/bin:$PATH"
// export EL_HOME="$HOME/.el" // 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 // Constants
fn gitea_releases_url() -> String { 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