Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e08150d6e7 | |||
| e3a72aae90 | |||
| 1264af72a6 | |||
| 412bd2744e | |||
| a000599bfe | |||
| 8f922e68b3 | |||
| ef1db34846 | |||
| 34249b39a3 | |||
| 7b45468b1c | |||
| db7dae8236 | |||
| ee1627c2c0 | |||
| b90333e9e7 | |||
| d917165aaf | |||
| fde3ef539c | |||
| 9bcd68fbca | |||
| 913a98329a | |||
| 6121b33d25 | |||
| 1a8a16002e | |||
| 0c2ff6957e | |||
| a3ead6552e | |||
| e68dcf7303 | |||
| a390ee494e | |||
| c2cd5e01e1 | |||
| 8212e12e57 | |||
| 253ee2b887 | |||
| d7540700d4 | |||
| f103e85f88 | |||
| fe84639b17 | |||
| 5fdc9fb15e | |||
| 8967fa404e | |||
| a7e6fbf2d2 | |||
| 1f4b594ae7 | |||
| cff7ce072d | |||
| f5dcca0386 | |||
| 53e0b99d5f | |||
| 5f9cad5908 | |||
| 00629b39c4 | |||
| ca1e4d57b8 | |||
| f971e96dd5 | |||
| 81a1a624f1 | |||
| 7b7f9f353b | |||
| a3732a1e9a | |||
| 8ef3eb6bec | |||
| 027ad82db2 | |||
| 8ab8e3fd31 | |||
| 05d717744b | |||
| b0d0975f05 | |||
| 6f634ae432 | |||
| 908ce303f3 | |||
| edbde5ef51 | |||
| 2e529bd0fe | |||
| 5d9299a472 | |||
| e8b01583d8 | |||
| b19dd5608f | |||
| 94d6eace94 | |||
| f1dfc394e3 | |||
| 61bf501b84 | |||
| 2fd298df55 | |||
| 254cbe0ac2 | |||
| bcfb33ea83 | |||
| 60ad7f2f6b | |||
| 231cb5eddd | |||
| 54de7d3f3f | |||
| 77100649c3 | |||
| b0570656b1 | |||
| a79b421578 | |||
| 0ab9361fab | |||
| f7953eb73a | |||
| 9c350e9f2f | |||
| e93c899d1f | |||
| 0b50e61f98 | |||
| f2741e4bdb | |||
| 7fd01b8a8d | |||
| d2940f5d1d | |||
| dca741f915 | |||
| 9862f4d6e1 | |||
| 8b074d2e39 | |||
| ec9c322cc7 | |||
| 702093e043 | |||
| 95b6fac094 | |||
| af66cebfd3 |
+148
-50
@@ -22,39 +22,46 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
> /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -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 \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb (needed for Artifact Registry publish and downstream CI)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
@@ -80,14 +87,23 @@ jobs:
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -96,8 +112,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -106,8 +122,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -116,8 +132,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -126,8 +142,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -136,8 +152,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -146,8 +162,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -156,8 +172,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -166,10 +182,32 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
if: github.event_name == 'push'
|
||||
@@ -177,20 +215,16 @@ jobs:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -198,7 +232,15 @@ jobs:
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -206,9 +248,65 @@ jobs:
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (dev)
|
||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:dev (or fall back to :latest on first run)
|
||||
BASE_TAG="dev"
|
||||
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:dev" \
|
||||
-t "${CI_BASE}:dev-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:dev"
|
||||
docker push "${CI_BASE}:dev-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
+109
-37
@@ -34,35 +34,25 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-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
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests - text
|
||||
@@ -97,7 +87,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -107,7 +97,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -117,7 +107,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -127,7 +117,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -137,7 +127,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -147,7 +137,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -157,7 +147,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -167,7 +157,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -177,9 +167,45 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build elb (needed for epm and el-install builds below)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
if: github.event_name == 'push'
|
||||
@@ -187,20 +213,19 @@ jobs:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -208,7 +233,7 @@ jobs:
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -216,9 +241,56 @@ jobs:
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (stage)
|
||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:stage (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:stage" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:stage" \
|
||||
-t "${CI_BASE}:stage-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:stage"
|
||||
docker push "${CI_BASE}:stage-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
name: Engram CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'engram/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
> /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
|
||||
- name: Download El SDK from Artifact Registry
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
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
|
||||
|
||||
rm -rf /opt/el/dist /opt/el/runtime
|
||||
mkdir -p /opt/el/dist/platform /opt/el/dist/bin /opt/el/runtime
|
||||
|
||||
get_latest() {
|
||||
gcloud artifacts versions list \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package="$1" \
|
||||
--sort-by="~createTime" \
|
||||
--limit=1 \
|
||||
--format="value(name)" 2>/dev/null | awk -F/ '{print $NF}'
|
||||
}
|
||||
|
||||
ELC_VER=$(get_latest el-elc)
|
||||
ELB_VER=$(get_latest el-elb)
|
||||
RC_VER=$(get_latest el-runtime-c)
|
||||
RH_VER=$(get_latest el-runtime-h)
|
||||
|
||||
echo "Downloading elc@${ELC_VER} elb@${ELB_VER} runtime-c@${RC_VER} runtime-h@${RH_VER}"
|
||||
|
||||
gcloud artifacts generic download \
|
||||
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
|
||||
--package=el-elc --version="${ELC_VER}" \
|
||||
--destination=/opt/el/dist/platform/
|
||||
|
||||
gcloud artifacts generic download \
|
||||
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
|
||||
--package=el-elb --version="${ELB_VER}" \
|
||||
--destination=/opt/el/dist/bin/
|
||||
|
||||
gcloud artifacts generic download \
|
||||
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
|
||||
--package=el-runtime-c --version="${RC_VER}" \
|
||||
--destination=/opt/el/runtime/
|
||||
|
||||
gcloud artifacts generic download \
|
||||
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
|
||||
--package=el-runtime-h --version="${RH_VER}" \
|
||||
--destination=/opt/el/runtime/
|
||||
|
||||
mv /opt/el/dist/platform/elc* /opt/el/dist/platform/elc 2>/dev/null || true
|
||||
mv /opt/el/dist/bin/elb* /opt/el/dist/bin/elb 2>/dev/null || true
|
||||
mv /opt/el/runtime/el_runtime.c* /opt/el/runtime/el_runtime.c 2>/dev/null || true
|
||||
mv /opt/el/runtime/el_runtime.h* /opt/el/runtime/el_runtime.h 2>/dev/null || true
|
||||
|
||||
chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
echo "El SDK ready"
|
||||
|
||||
- name: Build engram binary (linux/amd64)
|
||||
run: |
|
||||
ELB=/opt/el/dist/bin/elb
|
||||
ELC=/opt/el/dist/platform/elc
|
||||
RUNTIME=/opt/el/runtime
|
||||
|
||||
# elb reads manifest.el from the working directory.
|
||||
# engram/dist/engram.c is the pre-compiled C translation of src/server.el.
|
||||
# elb compiles dist/engram.c + el_runtime.c → dist/engram binary.
|
||||
cd engram
|
||||
"$ELB" --elc="$ELC" --runtime="$RUNTIME"
|
||||
ls -lh dist/engram
|
||||
file dist/engram
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
file engram/dist/engram
|
||||
timeout 3 engram/dist/engram --help 2>&1 || true
|
||||
echo "smoke test complete"
|
||||
|
||||
- name: Publish engram binary to Artifact Registry
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=engram \
|
||||
--version="${VERSION}" \
|
||||
--source=engram/dist/engram
|
||||
|
||||
# Re-upload as "latest" — Artifact Registry generic artifacts don't
|
||||
# support moving tags, so we upload again. The newest upload wins.
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=engram \
|
||||
--version="latest" \
|
||||
--source=engram/dist/engram \
|
||||
2>/dev/null || true
|
||||
|
||||
echo "Published engram@${VERSION} and engram@latest"
|
||||
rm -f /tmp/gcp-key.json
|
||||
@@ -34,35 +34,26 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
run: |
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-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
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb binary
|
||||
@@ -74,34 +65,30 @@ jobs:
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary (epm lives at repo root, not inside lang/)
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
dist/platform/elc ../epm/src/epm.el > dist/epm.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/epm.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/bin/epm
|
||||
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
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
dist/platform/elc tools/install/el-install.el > dist/el-install.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/el-install.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/bin/el-install
|
||||
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"
|
||||
|
||||
@@ -137,7 +124,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -147,7 +134,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -157,7 +144,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -167,7 +154,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -177,7 +164,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -187,7 +174,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -197,7 +184,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -207,7 +194,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -217,7 +204,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
|
||||
@@ -241,7 +228,7 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
REPO: neuron-technologies/el
|
||||
run: |
|
||||
@@ -302,20 +289,19 @@ jobs:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/elc \
|
||||
--package=el-elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -323,7 +309,15 @@ jobs:
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.c \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -331,20 +325,75 @@ jobs:
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el/el_runtime.h \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK
|
||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:latest" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:latest" \
|
||||
-t "${CI_BASE}:${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:latest"
|
||||
docker push "${CI_BASE}:${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
- name: Dispatch el-sdk-updated to downstream repos
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
for repo in neuron-technologies/forge; do
|
||||
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> Any
|
||||
extern fn lang_get(profile: Any, key: String) -> String
|
||||
extern fn lang_profile_en() -> Any
|
||||
extern fn lang_profile_ja() -> Any
|
||||
extern fn lang_profile_ar() -> Any
|
||||
extern fn lang_profile_zh() -> Any
|
||||
extern fn lang_profile_de() -> Any
|
||||
extern fn lang_profile_es() -> Any
|
||||
extern fn lang_profile_fi() -> Any
|
||||
extern fn lang_profile_sw() -> Any
|
||||
extern fn lang_profile_hi() -> Any
|
||||
extern fn lang_profile_ru() -> Any
|
||||
extern fn lang_profile_fr() -> Any
|
||||
extern fn lang_profile_la() -> Any
|
||||
extern fn lang_profile_he() -> Any
|
||||
extern fn lang_profile_sa() -> Any
|
||||
extern fn lang_profile_got() -> Any
|
||||
extern fn lang_profile_non() -> Any
|
||||
extern fn lang_profile_enm() -> Any
|
||||
extern fn lang_profile_pi() -> Any
|
||||
extern fn lang_profile_grc() -> Any
|
||||
extern fn lang_profile_ang() -> Any
|
||||
extern fn lang_profile_fro() -> Any
|
||||
extern fn lang_profile_goh() -> Any
|
||||
extern fn lang_profile_sga() -> Any
|
||||
extern fn lang_profile_txb() -> Any
|
||||
extern fn lang_profile_peo() -> Any
|
||||
extern fn lang_profile_akk() -> Any
|
||||
extern fn lang_profile_uga() -> Any
|
||||
extern fn lang_profile_egy() -> Any
|
||||
extern fn lang_profile_sux() -> Any
|
||||
extern fn lang_profile_gez() -> Any
|
||||
extern fn lang_profile_cop() -> Any
|
||||
extern fn lang_from_code(code: String) -> Any
|
||||
extern fn lang_default() -> Any
|
||||
extern fn lang_is_isolating(profile: Any) -> Bool
|
||||
extern fn lang_is_agglutinative(profile: Any) -> Bool
|
||||
extern fn lang_is_fusional(profile: Any) -> Bool
|
||||
extern fn lang_is_polysynthetic(profile: Any) -> Bool
|
||||
extern fn lang_is_rtl(profile: Any) -> Bool
|
||||
extern fn lang_has_null_subject(profile: Any) -> Bool
|
||||
extern fn lang_has_case(profile: Any) -> Bool
|
||||
extern fn lang_has_gender(profile: Any) -> Bool
|
||||
extern fn lang_word_order(profile: Any) -> String
|
||||
extern fn lang_code(profile: Any) -> String
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
|
||||
extern fn lang_get(profile: [String], key: String) -> String
|
||||
extern fn lang_profile_en() -> [String]
|
||||
extern fn lang_profile_ja() -> [String]
|
||||
extern fn lang_profile_ar() -> [String]
|
||||
extern fn lang_profile_zh() -> [String]
|
||||
extern fn lang_profile_de() -> [String]
|
||||
extern fn lang_profile_es() -> [String]
|
||||
extern fn lang_profile_fi() -> [String]
|
||||
extern fn lang_profile_sw() -> [String]
|
||||
extern fn lang_profile_hi() -> [String]
|
||||
extern fn lang_profile_ru() -> [String]
|
||||
extern fn lang_profile_fr() -> [String]
|
||||
extern fn lang_profile_la() -> [String]
|
||||
extern fn lang_profile_he() -> [String]
|
||||
extern fn lang_profile_sa() -> [String]
|
||||
extern fn lang_profile_got() -> [String]
|
||||
extern fn lang_profile_non() -> [String]
|
||||
extern fn lang_profile_enm() -> [String]
|
||||
extern fn lang_profile_pi() -> [String]
|
||||
extern fn lang_profile_grc() -> [String]
|
||||
extern fn lang_profile_ang() -> [String]
|
||||
extern fn lang_profile_fro() -> [String]
|
||||
extern fn lang_profile_goh() -> [String]
|
||||
extern fn lang_profile_sga() -> [String]
|
||||
extern fn lang_profile_txb() -> [String]
|
||||
extern fn lang_profile_peo() -> [String]
|
||||
extern fn lang_profile_akk() -> [String]
|
||||
extern fn lang_profile_uga() -> [String]
|
||||
extern fn lang_profile_egy() -> [String]
|
||||
extern fn lang_profile_sux() -> [String]
|
||||
extern fn lang_profile_gez() -> [String]
|
||||
extern fn lang_profile_cop() -> [String]
|
||||
extern fn lang_from_code(code: String) -> [String]
|
||||
extern fn lang_default() -> [String]
|
||||
extern fn lang_is_isolating(profile: [String]) -> Bool
|
||||
extern fn lang_is_agglutinative(profile: [String]) -> Bool
|
||||
extern fn lang_is_fusional(profile: [String]) -> Bool
|
||||
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
|
||||
extern fn lang_is_rtl(profile: [String]) -> Bool
|
||||
extern fn lang_has_null_subject(profile: [String]) -> Bool
|
||||
extern fn lang_has_case(profile: [String]) -> Bool
|
||||
extern fn lang_has_gender(profile: [String]) -> Bool
|
||||
extern fn lang_word_order(profile: [String]) -> String
|
||||
extern fn lang_code(profile: [String]) -> String
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn akk_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn akk_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn akk_str_len(s: String) -> Int
|
||||
extern fn akk_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn ang_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn ang_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn ang_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn ang_str_last_char(s: String) -> String
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn ar_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn ar_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn ar_str_len(s: String) -> Int
|
||||
extern fn ar_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
// ── String helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn cop_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn cop_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn cop_str_len(s: String) -> Int
|
||||
extern fn cop_drop(s: String, n: Int) -> String
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// Dat: dem der dem den
|
||||
// Gen: des der des der
|
||||
|
||||
import "morphology.el"
|
||||
fn de_article_def(gender: String, gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "pl") {
|
||||
if str_eq(gram_case, "nom") { return "die" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn de_article_def(gender: String, gram_case: String, number: String) -> String
|
||||
extern fn de_article_indef(gender: String, gram_case: String, number: String) -> String
|
||||
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
// ── String helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn egy_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn egy_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn egy_str_len(s: String) -> Int
|
||||
extern fn egy_drop(s: String, n: Int) -> String
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn enm_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn enm_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn enm_drop(s: String, n: Int) -> String
|
||||
extern fn enm_first_char(s: String) -> String
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
// ── String helpers (local, matching morphology.el conventions) ────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn es_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn es_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn es_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn es_str_last_char(s: String) -> String
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
// If only neutral vowels are found, default to "front" (the conservative choice
|
||||
// for borrowed words and those without clear back vowels).
|
||||
|
||||
import "morphology.el"
|
||||
fn fi_harmony(word: String) -> String {
|
||||
let n: Int = str_len(word)
|
||||
let i: Int = n - 1
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn fi_harmony(word: String) -> String
|
||||
extern fn fi_suffix(base: String, harmony: String) -> String
|
||||
extern fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
|
||||
extern fn fi_str_last_char(s: String) -> String
|
||||
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
|
||||
extern fn fi_verb_stem(dict_form: String) -> String
|
||||
extern fn fi_irregular_verb(dict_form: String) -> Any
|
||||
extern fn fi_irregular_verb(dict_form: String) -> [String]
|
||||
extern fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String
|
||||
extern fn fi_past_stem(stem: String) -> String
|
||||
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String
|
||||
@@ -14,4 +14,4 @@ extern fn fi_negative(verb: String, person: String, number: String) -> String
|
||||
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn fi_question_suffix(harmony: String) -> String
|
||||
extern fn fi_make_question(verb_form: String, harmony: String) -> String
|
||||
extern fn fi_full_paradigm(noun: String) -> Any
|
||||
extern fn fi_full_paradigm(noun: String) -> [String]
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
// ── String helpers (local, matching morphology.el conventions) ────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn fr_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn fr_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn fr_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn fr_str_last_char(s: String) -> String
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn fro_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn fro_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn fro_drop(s: String, n: Int) -> String
|
||||
extern fn fro_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn gez_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn gez_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn gez_str_len(s: String) -> Int
|
||||
extern fn gez_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn goh_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn goh_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn goh_drop(s: String, n: Int) -> String
|
||||
extern fn goh_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn got_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn got_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn got_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn got_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn grc_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn grc_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn grc_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn grc_str_last_char(s: String) -> String
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn he_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn he_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn he_str_len(s: String) -> Int
|
||||
extern fn he_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn hi_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn hi_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn hi_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn hi_str_last_char(s: String) -> String
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// Note: this is a heuristic classifier for romanized input. For production use
|
||||
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
|
||||
|
||||
import "morphology.el"
|
||||
fn ja_verb_group(dict_form: String) -> String {
|
||||
// Irregular verbs (exact match on dictionary form)
|
||||
if str_eq(dict_form, "する") { return "irregular" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn ja_verb_group(dict_form: String) -> String
|
||||
extern fn ja_ichidan_stem(dict_form: String) -> String
|
||||
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn la_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn la_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn la_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn la_str_last_char(s: String) -> String
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn non_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn non_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn non_drop(s: String, n: Int) -> String
|
||||
extern fn non_last(s: String) -> String
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn peo_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn peo_drop(s: String, n: Int) -> String
|
||||
extern fn peo_ends(s: String, suf: String) -> Bool
|
||||
extern fn peo_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn pi_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn pi_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn pi_drop(s: String, n: Int) -> String
|
||||
extern fn pi_last_char(s: String) -> String
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
// The heuristic returns the most probable gender. Caller should override
|
||||
// for known exceptions (путь, рубль are masc despite -ь).
|
||||
|
||||
import "morphology.el"
|
||||
fn ru_gender(noun: String) -> String {
|
||||
let n: Int = str_len(noun)
|
||||
if n == 0 { return "m" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn ru_gender(noun: String) -> String
|
||||
extern fn ru_stem_type(noun: String, gender: String) -> String
|
||||
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sa_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sa_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sa_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sa_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sga_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sga_drop(s: String, n: Int) -> String
|
||||
extern fn sga_first(s: String) -> String
|
||||
extern fn sga_rest(s: String) -> String
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sux_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sux_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sux_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sux_str_last_char(s: String) -> String
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sw_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sw_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sw_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sw_str_first_char(s: String) -> String
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn txb_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn txb_drop(s: String, n: Int) -> String
|
||||
extern fn txb_ends(s: String, suf: String) -> Bool
|
||||
extern fn txb_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn uga_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn uga_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn uga_str_len(s: String) -> Int
|
||||
extern fn uga_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "language-profile.el"
|
||||
import "morphology-es.el"
|
||||
import "morphology-fr.el"
|
||||
import "morphology-de.el"
|
||||
import "morphology-ru.el"
|
||||
import "morphology-fi.el"
|
||||
import "morphology-ar.el"
|
||||
import "morphology-hi.el"
|
||||
import "morphology-sw.el"
|
||||
import "morphology-la.el"
|
||||
import "morphology-ja.el"
|
||||
fn str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn str_ends(s: String, suf: String) -> Bool
|
||||
extern fn str_last_char(s: String) -> String
|
||||
extern fn str_last2(s: String) -> String
|
||||
@@ -8,7 +8,7 @@ extern fn is_vowel(c: String) -> Bool
|
||||
extern fn morph_apply_suffix(base: String, suffix: String) -> String
|
||||
extern fn en_irregular_plural(word: String) -> String
|
||||
extern fn en_irregular_singular(word: String) -> String
|
||||
extern fn en_irregular_verb(base: String) -> Any
|
||||
extern fn en_irregular_verb(base: String) -> [String]
|
||||
extern fn en_verb_3sg(base: String) -> String
|
||||
extern fn en_should_double_final(base: String) -> Bool
|
||||
extern fn en_verb_past(base: String) -> String
|
||||
@@ -16,10 +16,10 @@ extern fn en_verb_gerund(base: String) -> String
|
||||
extern fn en_pluralize_regular(singular: String) -> String
|
||||
extern fn en_verb_form(base: String, tense: String, person: String, number: String) -> String
|
||||
extern fn agree_determiner(det: String, noun: String) -> String
|
||||
extern fn morph_pluralize(noun: String, profile: Any) -> String
|
||||
extern fn morph_pluralize(noun: String, profile: [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: Any) -> String
|
||||
extern fn morph_inflect(word: String, features: String, profile: Any) -> String
|
||||
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: [String]) -> String
|
||||
extern fn morph_inflect(word: String, features: String, profile: [String]) -> String
|
||||
extern fn pluralize(singular: String) -> String
|
||||
extern fn singularize(plural: String) -> String
|
||||
extern fn verb_form(base: String, tense: String, person: String, number: String) -> String
|
||||
|
||||
+19
-19
@@ -1,20 +1,20 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn lex_word(entry: Any) -> String
|
||||
extern fn lex_pos(entry: Any) -> String
|
||||
extern fn lex_form(entry: Any, idx: Int) -> String
|
||||
extern fn lex_class(entry: Any) -> String
|
||||
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> Any
|
||||
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> Any
|
||||
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> Any
|
||||
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> Any
|
||||
extern fn build_vocab() -> Any
|
||||
extern fn get_vocab() -> Any
|
||||
extern fn vocab_lookup(word: String, lang_code: String) -> Any
|
||||
extern fn vocab_lookup_en(word: String) -> Any
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn lex_word(entry: [String]) -> String
|
||||
extern fn lex_pos(entry: [String]) -> String
|
||||
extern fn lex_form(entry: [String], idx: Int) -> String
|
||||
extern fn lex_class(entry: [String]) -> String
|
||||
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> [String]
|
||||
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> [String]
|
||||
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> [String]
|
||||
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> [String]
|
||||
extern fn build_vocab() -> [[String]]
|
||||
extern fn get_vocab() -> [[String]]
|
||||
extern fn vocab_lookup(word: String, lang_code: String) -> [String]
|
||||
extern fn vocab_lookup_en(word: String) -> [String]
|
||||
extern fn vocab_synonym(word: String, lang_register: String, lang_code: String) -> String
|
||||
extern fn vocab_by_pos(pos: String) -> Any
|
||||
extern fn vocab_by_class(cls: String) -> Any
|
||||
extern fn entry_found(entry: Any) -> Bool
|
||||
extern fn entry_word(entry: Any) -> String
|
||||
extern fn entry_pos(entry: Any) -> String
|
||||
extern fn entry_form(entry: Any, n: Int) -> String
|
||||
extern fn vocab_by_pos(pos: String) -> [[String]]
|
||||
extern fn vocab_by_class(cls: String) -> [[String]]
|
||||
extern fn entry_found(entry: [String]) -> Bool
|
||||
extern fn entry_word(entry: [String]) -> String
|
||||
extern fn entry_pos(entry: [String]) -> String
|
||||
extern fn entry_form(entry: [String], n: Int) -> String
|
||||
|
||||
BIN
Binary file not shown.
Vendored
+753
-22
@@ -2,6 +2,11 @@
|
||||
#include <stdlib.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
el_val_t bm25_tokenize(el_val_t text);
|
||||
el_val_t bm25_count_term(el_val_t term, el_val_t doc_tokens);
|
||||
el_val_t bm25_score_doc(el_val_t doc_content, el_val_t query_tokens, el_val_t corpus_size, el_val_t avg_doc_len);
|
||||
el_val_t bm25_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t auto_link_content_node(el_val_t node_id, el_val_t content);
|
||||
el_val_t parse_port(el_val_t bind);
|
||||
el_val_t ok_json(void);
|
||||
el_val_t err_json(el_val_t msg);
|
||||
@@ -20,16 +25,246 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_decay(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_export(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_reindex(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_session_begin(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_ctx(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_memory(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_knowledge_capture(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_knowledge_evolve(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_knowledge_promote(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_recall(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_graph(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_graph_link(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_list(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_consolidate(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_config(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_state_events(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_neuron_processes(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_events_next(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_events_ack(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_bm25_search(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
||||
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
|
||||
|
||||
el_val_t bind_str;
|
||||
el_val_t port;
|
||||
el_val_t data_dir;
|
||||
el_val_t snapshot_path;
|
||||
el_val_t db_path;
|
||||
el_val_t loaded;
|
||||
|
||||
el_val_t bm25_tokenize(el_val_t text) {
|
||||
el_val_t t = str_to_lower(text);
|
||||
t = str_replace(t, EL_STR("."), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR(","), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("!"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("?"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("\""), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR(":"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR(";"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("("), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR(")"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("["), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("]"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("{"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("}"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("/"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("\\"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("'"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("-"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("_"), EL_STR(" "));
|
||||
t = str_replace(t, EL_STR("+"), EL_STR(" "));
|
||||
return str_trim(t);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t bm25_count_term(el_val_t term, el_val_t doc_tokens) {
|
||||
el_val_t padded_term = el_str_concat(el_str_concat(EL_STR(" "), term), EL_STR(" "));
|
||||
el_val_t padded_doc = el_str_concat(el_str_concat(EL_STR(" "), doc_tokens), EL_STR(" "));
|
||||
return str_count(padded_doc, padded_term);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t bm25_score_doc(el_val_t doc_content, el_val_t query_tokens, el_val_t corpus_size, el_val_t avg_doc_len) {
|
||||
el_val_t k1 = el_from_float(1.2);
|
||||
el_val_t b = el_from_float(0.75);
|
||||
el_val_t delta = el_from_float(1.0);
|
||||
el_val_t doc_tokens = bm25_tokenize(doc_content);
|
||||
el_val_t doc_wc = str_count_words(doc_tokens);
|
||||
if (doc_wc == 0) {
|
||||
return EL_STR("0.0");
|
||||
}
|
||||
el_val_t doc_len = int_to_float(doc_wc);
|
||||
el_val_t avg_len = str_to_float(avg_doc_len);
|
||||
el_val_t N = int_to_float(corpus_size);
|
||||
el_val_t idf_arg = float_add(float_div(float_add(N, el_from_float(1.2)), el_from_float(1.5)), el_from_float(1.0));
|
||||
el_val_t idf = math_log(idf_arg);
|
||||
el_val_t terms = str_split(query_tokens, EL_STR(" "));
|
||||
el_val_t n_terms = len(terms);
|
||||
el_val_t score = el_from_float(0.0);
|
||||
el_val_t i = 0;
|
||||
while (i < n_terms) {
|
||||
el_val_t term = get(terms, i);
|
||||
el_val_t tlen = str_len(term);
|
||||
if (tlen >= 2) {
|
||||
el_val_t tf_count = bm25_count_term(term, doc_tokens);
|
||||
if (tf_count > 0) {
|
||||
el_val_t tf_raw = int_to_float(tf_count);
|
||||
el_val_t norm_factor = float_add(float_sub(el_from_float(1.0), b), float_div(float_mul(b, doc_len), avg_len));
|
||||
el_val_t numerator = float_mul(tf_raw, float_add(k1, el_from_float(1.0)));
|
||||
el_val_t denominator = float_add(tf_raw, float_mul(k1, norm_factor));
|
||||
el_val_t tf_comp = float_add(delta, float_div(numerator, denominator));
|
||||
score = float_add(score, float_mul(idf, tf_comp));
|
||||
}
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return float_to_str(score);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t bm25_search_json(el_val_t query, el_val_t limit) {
|
||||
el_val_t scan_limit = (limit * 10);
|
||||
if (scan_limit < 200) {
|
||||
scan_limit = 200;
|
||||
}
|
||||
if (scan_limit > 5000) {
|
||||
scan_limit = 5000;
|
||||
}
|
||||
el_val_t nodes_json = engram_scan_nodes_json(scan_limit, 0);
|
||||
el_val_t n = json_array_len(nodes_json);
|
||||
if (n == 0) {
|
||||
return EL_STR("[]");
|
||||
}
|
||||
el_val_t total_words = 0;
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t node = json_array_get(nodes_json, i);
|
||||
el_val_t content = json_get_string(node, EL_STR("content"));
|
||||
el_val_t tokens = bm25_tokenize(content);
|
||||
el_val_t wc = str_count_words(tokens);
|
||||
total_words = (total_words + wc);
|
||||
i = (i + 1);
|
||||
}
|
||||
el_val_t avg_doc_len_f = float_div(int_to_float(total_words), int_to_float(n));
|
||||
el_val_t avg_doc_len = ({ el_val_t _if_result_1 = 0; if (float_gt(avg_doc_len_f, el_from_float(0.0))) { _if_result_1 = (float_to_str(avg_doc_len_f)); } else { _if_result_1 = (EL_STR("1.0")); } _if_result_1; });
|
||||
el_val_t query_tokens = bm25_tokenize(query);
|
||||
if (str_eq(str_trim(query_tokens), EL_STR(""))) {
|
||||
return EL_STR("[]");
|
||||
}
|
||||
el_val_t result_nodes = 0;
|
||||
el_val_t result_scores = 0;
|
||||
el_val_t result_count = 0;
|
||||
el_val_t j = 0;
|
||||
while (j < n) {
|
||||
el_val_t node = json_array_get(nodes_json, j);
|
||||
el_val_t content = json_get_string(node, EL_STR("content"));
|
||||
el_val_t sc_str = bm25_score_doc(content, query_tokens, n, avg_doc_len);
|
||||
if (float_gt(str_to_float(sc_str), el_from_float(0.0))) {
|
||||
result_nodes = list_push(result_nodes, node);
|
||||
result_scores = list_push(result_scores, sc_str);
|
||||
result_count = (result_count + 1);
|
||||
}
|
||||
j = (j + 1);
|
||||
}
|
||||
if (result_count == 0) {
|
||||
return EL_STR("[]");
|
||||
}
|
||||
el_val_t out_limit = ({ el_val_t _if_result_2 = 0; if ((result_count < limit)) { _if_result_2 = (result_count); } else { _if_result_2 = (limit); } _if_result_2; });
|
||||
el_val_t k = 0;
|
||||
while (k < out_limit) {
|
||||
el_val_t max_idx = k;
|
||||
el_val_t max_sc_str = get(result_scores, k);
|
||||
el_val_t max_sc_f = str_to_float(max_sc_str);
|
||||
el_val_t p = (k + 1);
|
||||
while (p < result_count) {
|
||||
el_val_t sc2_str = get(result_scores, p);
|
||||
el_val_t sc2_f = str_to_float(sc2_str);
|
||||
if (float_gt(sc2_f, max_sc_f)) {
|
||||
max_sc_f = sc2_f;
|
||||
max_sc_str = sc2_str;
|
||||
max_idx = p;
|
||||
}
|
||||
p = (p + 1);
|
||||
}
|
||||
if (max_idx != k) {
|
||||
el_val_t tmp_node = get(result_nodes, k);
|
||||
el_val_t tmp_sc = get(result_scores, k);
|
||||
result_nodes = list_set(result_nodes, k, get(result_nodes, max_idx));
|
||||
result_scores = list_set(result_scores, k, get(result_scores, max_idx));
|
||||
result_nodes = list_set(result_nodes, max_idx, tmp_node);
|
||||
result_scores = list_set(result_scores, max_idx, tmp_sc);
|
||||
}
|
||||
k = (k + 1);
|
||||
}
|
||||
el_val_t out = EL_STR("[");
|
||||
el_val_t r = 0;
|
||||
while (r < out_limit) {
|
||||
el_val_t node = get(result_nodes, r);
|
||||
el_val_t sc_str = get(result_scores, r);
|
||||
el_val_t node_len = str_len(node);
|
||||
el_val_t node_body = str_slice(node, 0, (node_len - 1));
|
||||
el_val_t entry = el_str_concat(el_str_concat(el_str_concat(node_body, EL_STR(",\"bm25_score\":")), sc_str), EL_STR("}"));
|
||||
if (r > 0) {
|
||||
out = el_str_concat(out, EL_STR(","));
|
||||
}
|
||||
out = el_str_concat(out, entry);
|
||||
r = (r + 1);
|
||||
}
|
||||
return el_str_concat(out, EL_STR("]"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t auto_link_content_node(el_val_t node_id, el_val_t content) {
|
||||
el_val_t clen = str_len(content);
|
||||
if (clen < 20) {
|
||||
return 0;
|
||||
}
|
||||
el_val_t sp1 = str_index_of(content, EL_STR(" "));
|
||||
el_val_t w1end = ({ el_val_t _if_result_3 = 0; if ((sp1 < 0)) { _if_result_3 = (clen); } else { _if_result_3 = (sp1); } _if_result_3; });
|
||||
el_val_t word1 = str_slice(content, 0, w1end);
|
||||
state_set(EL_STR("aln_term"), EL_STR(""));
|
||||
if (str_len(word1) >= 5) {
|
||||
state_set(EL_STR("aln_term"), word1);
|
||||
}
|
||||
if (str_eq(state_get(EL_STR("aln_term")), EL_STR(""))) {
|
||||
if (sp1 >= 0) {
|
||||
el_val_t rest = str_slice(content, (sp1 + 1), clen);
|
||||
el_val_t sp2 = str_index_of(rest, EL_STR(" "));
|
||||
el_val_t w2end = ({ el_val_t _if_result_4 = 0; if ((sp2 < 0)) { _if_result_4 = (str_len(rest)); } else { _if_result_4 = (sp2); } _if_result_4; });
|
||||
el_val_t word2 = str_slice(rest, 0, w2end);
|
||||
if (str_len(word2) >= 5) {
|
||||
state_set(EL_STR("aln_term"), word2);
|
||||
}
|
||||
}
|
||||
}
|
||||
el_val_t search_term = state_get(EL_STR("aln_term"));
|
||||
if (str_eq(search_term, EL_STR(""))) {
|
||||
return 0;
|
||||
}
|
||||
el_val_t results = bm25_search_json(search_term, 20);
|
||||
el_val_t n = json_array_len(results);
|
||||
state_set(EL_STR("aln_linked"), EL_STR("0"));
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t linked_so_far = str_to_int(state_get(EL_STR("aln_linked")));
|
||||
if (linked_so_far < 3) {
|
||||
el_val_t elem = json_array_get(results, i);
|
||||
el_val_t rid = json_get_string(elem, EL_STR("id"));
|
||||
el_val_t rtype = json_get_string(elem, EL_STR("node_type"));
|
||||
if ((!str_eq(rtype, EL_STR("InternalStateEvent")) && !str_eq(rid, EL_STR(""))) && !str_eq(rid, node_id)) {
|
||||
engram_connect(node_id, rid, el_from_float(0.6), EL_STR("related"));
|
||||
state_set(EL_STR("aln_linked"), int_to_str((linked_so_far + 1)));
|
||||
}
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
return str_to_int(state_get(EL_STR("aln_linked")));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t parse_port(el_val_t bind) {
|
||||
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
||||
@@ -119,7 +354,8 @@ el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
|
||||
salience = el_from_float(0.5);
|
||||
}
|
||||
el_val_t id = engram_node(content, node_type, salience);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
|
||||
el_val_t auto_linked = auto_link_content_node(id, content);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\",\"auto_linked\":")), int_to_str(auto_linked)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -176,7 +412,7 @@ el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
|
||||
if (limit == 0) {
|
||||
limit = 20;
|
||||
}
|
||||
return engram_search_json(q, limit);
|
||||
return bm25_search_json(q, limit);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -193,6 +429,17 @@ el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
||||
depth = bd;
|
||||
}
|
||||
}
|
||||
el_val_t top = bm25_search_json(q, 10);
|
||||
el_val_t nb = json_array_len(top);
|
||||
el_val_t bi = 0;
|
||||
while (bi < nb) {
|
||||
el_val_t node = json_array_get(top, bi);
|
||||
el_val_t nid = json_get_string(node, EL_STR("id"));
|
||||
if (!str_eq(nid, EL_STR(""))) {
|
||||
engram_strengthen(nid);
|
||||
}
|
||||
bi = (bi + 1);
|
||||
}
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -243,30 +490,46 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t route_decay(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return engram_apply_decay_json();
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_export(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t db_path = el_str_concat(dir, EL_STR("/engram.db"));
|
||||
engram_write_binary_el(db_path);
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
}
|
||||
engram_save(p);
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"binary\":\""), db_path), EL_STR("\",\"json\":\"")), p), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_reindex(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return engram_reindex_json();
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t db_path = el_str_concat(dir, EL_STR("/engram.db"));
|
||||
el_val_t ok = engram_load_binary_el(db_path);
|
||||
if (!ok) {
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
}
|
||||
engram_load(p);
|
||||
}
|
||||
engram_load(p);
|
||||
return ok_json();
|
||||
return 0;
|
||||
}
|
||||
@@ -276,6 +539,344 @@ el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_session_begin(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t results = engram_activate_json(EL_STR("memory knowledge context"), 2);
|
||||
el_val_t nc = engram_node_count();
|
||||
el_val_t ec = engram_edge_count();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"nodes\":"), results), EL_STR(",\"node_count\":")), int_to_str(nc)), EL_STR(",\"edge_count\":")), int_to_str(ec)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_ctx(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t results = engram_activate_json(EL_STR("architecture decision memory"), 2);
|
||||
el_val_t n = json_array_len(results);
|
||||
el_val_t limit = ({ el_val_t _if_result_5 = 0; if ((n > 10)) { _if_result_5 = (10); } else { _if_result_5 = (n); } _if_result_5; });
|
||||
el_val_t ctx = EL_STR("Recent working memory:\n");
|
||||
el_val_t i = 0;
|
||||
el_val_t ctx_body = EL_STR("");
|
||||
while (i < limit) {
|
||||
el_val_t elem = json_array_get(results, i);
|
||||
el_val_t label = json_get_string(elem, EL_STR("label"));
|
||||
el_val_t content = json_get_string(elem, EL_STR("content"));
|
||||
el_val_t clen = str_len(content);
|
||||
el_val_t snippet = ({ el_val_t _if_result_6 = 0; if ((clen > 200)) { _if_result_6 = (str_slice(content, 0, 200)); } else { _if_result_6 = (content); } _if_result_6; });
|
||||
ctx_body = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(ctx_body, EL_STR("- [")), label), EL_STR("]: ")), snippet), EL_STR("\n"));
|
||||
i = (i + 1);
|
||||
}
|
||||
el_val_t full_ctx = el_str_concat(ctx, ctx_body);
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"context\":\""), str_replace(str_replace(str_replace(full_ctx, EL_STR("\\"), EL_STR("\\\\")), EL_STR("\""), EL_STR("\\\"")), EL_STR("\n"), EL_STR("\\n"))), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_memory(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return EL_STR("{\"error\":\"content is required\"}");
|
||||
}
|
||||
el_val_t node_type = json_get_string(body, EL_STR("node_type"));
|
||||
if (str_eq(node_type, EL_STR(""))) {
|
||||
node_type = EL_STR("Memory");
|
||||
}
|
||||
el_val_t label = json_get_string(body, EL_STR("label"));
|
||||
el_val_t importance = json_get_string(body, EL_STR("importance"));
|
||||
el_val_t project = json_get_string(body, EL_STR("project"));
|
||||
el_val_t tags_raw = json_get_string(body, EL_STR("tags"));
|
||||
el_val_t tier = EL_STR("Episodic");
|
||||
if (str_eq(importance, EL_STR("critical"))) {
|
||||
tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(importance, EL_STR("high"))) {
|
||||
tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(importance, EL_STR("normal"))) {
|
||||
tier = EL_STR("Episodic");
|
||||
}
|
||||
if (str_eq(importance, EL_STR("low"))) {
|
||||
tier = EL_STR("Working");
|
||||
}
|
||||
el_val_t explicit_tier = json_get_string(body, EL_STR("tier"));
|
||||
if (!str_eq(explicit_tier, EL_STR(""))) {
|
||||
tier = explicit_tier;
|
||||
}
|
||||
el_val_t tags_str = tags_raw;
|
||||
if (!str_eq(project, EL_STR(""))) {
|
||||
if (str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(EL_STR("project:"), project);
|
||||
}
|
||||
if (!str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(el_str_concat(tags_str, EL_STR(" project:")), project);
|
||||
}
|
||||
}
|
||||
el_val_t id = engram_node_full(content, node_type, label, el_from_float(0.5), el_from_float(0.5), el_from_float(1.0), tier, tags_str);
|
||||
el_val_t auto_linked = auto_link_content_node(id, content);
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t db_path = el_str_concat(dir, EL_STR("/engram.db"));
|
||||
engram_write_binary_el(db_path);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"auto_linked\":")), int_to_str(auto_linked)), EL_STR(",\"content\":\"")), str_replace(str_replace(content, EL_STR("\\"), EL_STR("\\\\")), EL_STR("\""), EL_STR("\\\""))), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_knowledge_capture(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return EL_STR("{\"error\":\"content is required\"}");
|
||||
}
|
||||
el_val_t title = json_get_string(body, EL_STR("title"));
|
||||
el_val_t category = json_get_string(body, EL_STR("category"));
|
||||
el_val_t tags_raw = json_get_string(body, EL_STR("tags"));
|
||||
el_val_t project = json_get_string(body, EL_STR("project"));
|
||||
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
|
||||
el_val_t tier = EL_STR("Episodic");
|
||||
if (str_eq(tier_raw, EL_STR("lesson"))) {
|
||||
tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("canonical"))) {
|
||||
tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("note"))) {
|
||||
tier = EL_STR("Episodic");
|
||||
}
|
||||
el_val_t tags_str = tags_raw;
|
||||
if (!str_eq(category, EL_STR(""))) {
|
||||
if (str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(EL_STR("category:"), category);
|
||||
}
|
||||
if (!str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(el_str_concat(tags_str, EL_STR(" category:")), category);
|
||||
}
|
||||
}
|
||||
if (!str_eq(project, EL_STR(""))) {
|
||||
if (str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(EL_STR("project:"), project);
|
||||
}
|
||||
if (!str_eq(tags_str, EL_STR(""))) {
|
||||
tags_str = el_str_concat(el_str_concat(tags_str, EL_STR(" project:")), project);
|
||||
}
|
||||
}
|
||||
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), title, el_from_float(0.7), el_from_float(0.7), el_from_float(1.0), tier, tags_str);
|
||||
el_val_t auto_linked = auto_link_content_node(id, content);
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t db_path = el_str_concat(dir, EL_STR("/engram.db"));
|
||||
engram_write_binary_el(db_path);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"auto_linked\":")), int_to_str(auto_linked)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_knowledge_evolve(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
el_val_t prior_id = json_get_string(body, EL_STR("id"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return EL_STR("{\"ok\":true}");
|
||||
}
|
||||
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), EL_STR(""), el_from_float(0.7), el_from_float(0.7), el_from_float(1.0), EL_STR("Semantic"), EL_STR("evolved"));
|
||||
if (!str_eq(prior_id, EL_STR("")) && !str_eq(id, EL_STR(""))) {
|
||||
engram_connect(id, prior_id, el_from_float(1.0), EL_STR("supersedes"));
|
||||
}
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
engram_write_binary_el(el_str_concat(dir, EL_STR("/engram.db")));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_knowledge_promote(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t id = json_get_string(body, EL_STR("id"));
|
||||
if (str_eq(id, EL_STR(""))) {
|
||||
return EL_STR("{\"ok\":true}");
|
||||
}
|
||||
el_val_t node_json = engram_get_node_json(id);
|
||||
if (str_eq(node_json, EL_STR(""))) {
|
||||
return err_json(EL_STR("node not found"));
|
||||
}
|
||||
if (str_eq(node_json, EL_STR("null"))) {
|
||||
return err_json(EL_STR("node not found"));
|
||||
}
|
||||
el_val_t content = json_get_string(node_json, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return err_json(EL_STR("node has no content"));
|
||||
}
|
||||
el_val_t label = json_get_string(node_json, EL_STR("label"));
|
||||
el_val_t tags = json_get_string(node_json, EL_STR("tags"));
|
||||
el_val_t current_tier = json_get_string(node_json, EL_STR("tier"));
|
||||
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
|
||||
el_val_t new_tier = EL_STR("");
|
||||
if (str_eq(tier_raw, EL_STR("lesson"))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("canonical"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("note"))) {
|
||||
new_tier = EL_STR("Episodic");
|
||||
}
|
||||
if (str_eq(new_tier, EL_STR(""))) {
|
||||
if (str_eq(current_tier, EL_STR("Working"))) {
|
||||
new_tier = EL_STR("Episodic");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Episodic"))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Semantic"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Procedural"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
}
|
||||
if (str_eq(new_tier, EL_STR(""))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
el_val_t new_id = engram_node_full(content, EL_STR("Knowledge"), label, el_from_float(0.7), el_from_float(0.8), el_from_float(1.0), new_tier, tags);
|
||||
if (!str_eq(new_id, EL_STR(""))) {
|
||||
engram_connect(new_id, id, el_from_float(1.0), EL_STR("supersedes"));
|
||||
}
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
engram_write_binary_el(el_str_concat(dir, EL_STR("/engram.db")));
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), new_id), EL_STR("\",\"promoted_from\":\"")), id), EL_STR("\",\"tier\":\"")), new_tier), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_recall(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t query = json_get_string(body, EL_STR("query"));
|
||||
el_val_t chain = json_get_string(body, EL_STR("chain_name"));
|
||||
el_val_t limit = json_get_int(body, EL_STR("limit"));
|
||||
if (limit == 0) {
|
||||
limit = 20;
|
||||
}
|
||||
el_val_t q = ({ el_val_t _if_result_7 = 0; if (str_eq(query, EL_STR(""))) { _if_result_7 = (chain); } else { _if_result_7 = (query); } _if_result_7; });
|
||||
if (str_eq(q, EL_STR(""))) {
|
||||
return engram_scan_nodes_json(limit, 0);
|
||||
}
|
||||
return bm25_search_json(q, limit);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_graph(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t id = query_param(path, EL_STR("id"));
|
||||
if (str_eq(id, EL_STR(""))) {
|
||||
return EL_STR("{\"error\":\"id is required\"}");
|
||||
}
|
||||
el_val_t node_json = engram_get_node_json(id);
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"node\":"), node_json), EL_STR(",\"neighbors\":[]}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_graph_link(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
|
||||
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
|
||||
if (str_eq(from_id, EL_STR("")) || str_eq(to_id, EL_STR(""))) {
|
||||
return EL_STR("{\"error\":\"from_id and to_id are required\"}");
|
||||
}
|
||||
el_val_t relation = json_get_string(body, EL_STR("relation"));
|
||||
if (str_eq(relation, EL_STR(""))) {
|
||||
relation = EL_STR("related");
|
||||
}
|
||||
el_val_t weight = json_get_float(body, EL_STR("weight"));
|
||||
if (str_eq(weight, el_from_float(0.0))) {
|
||||
weight = el_from_float(0.5);
|
||||
}
|
||||
engram_connect(from_id, to_id, weight, relation);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_list(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t clean = strip_query(path);
|
||||
el_val_t prefix = EL_STR("/api/neuron/list/");
|
||||
el_val_t node_type = str_slice(clean, str_len(prefix), str_len(clean));
|
||||
el_val_t limit = query_int(path, EL_STR("limit"), 50);
|
||||
if (str_eq(node_type, EL_STR(""))) {
|
||||
return EL_STR("[]");
|
||||
}
|
||||
return engram_scan_nodes_by_type_json(node_type, limit, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_consolidate(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t db_path = el_str_concat(dir, EL_STR("/engram.db"));
|
||||
engram_write_binary_el(db_path);
|
||||
el_val_t nc = engram_node_count();
|
||||
el_val_t ec = engram_edge_count();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"node_count\":"), int_to_str(nc)), EL_STR(",\"edge_count\":")), int_to_str(ec)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_config(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t key = query_param(path, EL_STR("key"));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"key\":\""), key), EL_STR("\",\"value\":\"\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_state_events(el_val_t method, el_val_t path, el_val_t body) {
|
||||
if (str_eq(method, EL_STR("GET"))) {
|
||||
el_val_t limit_str = query_param(path, EL_STR("limit"));
|
||||
el_val_t limit = ({ el_val_t _if_result_8 = 0; if (str_eq(limit_str, EL_STR(""))) { _if_result_8 = (50); } else { _if_result_8 = (str_to_int(limit_str)); } _if_result_8; });
|
||||
el_val_t offset_str = query_param(path, EL_STR("offset"));
|
||||
el_val_t offset = ({ el_val_t _if_result_9 = 0; if (str_eq(offset_str, EL_STR(""))) { _if_result_9 = (0); } else { _if_result_9 = (str_to_int(offset_str)); } _if_result_9; });
|
||||
return engram_scan_nodes_by_type_json(EL_STR("InternalStateEvent"), limit, offset);
|
||||
}
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
content = body;
|
||||
}
|
||||
el_val_t event_label = json_get_string(content, EL_STR("event"));
|
||||
el_val_t label = ({ el_val_t _if_result_10 = 0; if (str_eq(event_label, EL_STR(""))) { _if_result_10 = (EL_STR("state-event")); } else { _if_result_10 = (event_label); } _if_result_10; });
|
||||
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), label, el_from_float(0.3), el_from_float(0.3), el_from_float(1.0), EL_STR("Working"), EL_STR("internal-state"));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_neuron_processes(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return EL_STR("{\"ok\":true,\"processes\":[]}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_events_next(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return EL_STR("{\"ok\":true,\"event\":null}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_events_ack(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return EL_STR("{\"ok\":true}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_bm25_search(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t q = EL_STR("");
|
||||
if (str_eq(method, EL_STR("GET"))) {
|
||||
q = query_param(path, EL_STR("q"));
|
||||
} else {
|
||||
q = json_get_string(body, EL_STR("query"));
|
||||
}
|
||||
if (str_eq(q, EL_STR(""))) {
|
||||
return EL_STR("{\"error\":\"query is required\"}");
|
||||
}
|
||||
el_val_t limit = query_int(path, EL_STR("limit"), 20);
|
||||
if (limit == 0) {
|
||||
limit = json_get_int(body, EL_STR("limit"));
|
||||
}
|
||||
if (limit == 0) {
|
||||
limit = 20;
|
||||
}
|
||||
return bm25_search_json(q, limit);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t check_auth_ok(el_val_t method, el_val_t body) {
|
||||
el_val_t key = env(EL_STR("ENGRAM_API_KEY"));
|
||||
if (str_eq(key, EL_STR(""))) {
|
||||
@@ -299,6 +900,60 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return route_health(method, path, body);
|
||||
}
|
||||
}
|
||||
if (str_starts_with(clean, EL_STR("/api/neuron/")) || str_starts_with(clean, EL_STR("/events/"))) {
|
||||
if (str_eq(clean, EL_STR("/api/neuron/session/begin"))) {
|
||||
return route_neuron_session_begin(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/ctx"))) {
|
||||
return route_neuron_ctx(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/memory"))) {
|
||||
return route_neuron_memory(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
|
||||
return route_neuron_knowledge_capture(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/evolve"))) {
|
||||
return route_neuron_knowledge_evolve(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/promote"))) {
|
||||
return route_neuron_knowledge_promote(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/recall"))) {
|
||||
return route_neuron_recall(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/graph/link"))) {
|
||||
return route_neuron_graph_link(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/graph"))) {
|
||||
return route_neuron_graph(method, path, body);
|
||||
}
|
||||
if (str_starts_with(clean, EL_STR("/api/neuron/list/"))) {
|
||||
return route_neuron_list(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/consolidate"))) {
|
||||
return route_neuron_consolidate(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/config"))) {
|
||||
return route_neuron_config(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/state-events"))) {
|
||||
return route_neuron_state_events(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/processes/define"))) {
|
||||
return route_neuron_processes(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/processes"))) {
|
||||
return route_neuron_processes(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/events/next"))) {
|
||||
return route_events_next(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/events/ack"))) {
|
||||
return route_events_ack(method, path, body);
|
||||
}
|
||||
return err_json(EL_STR("not found"));
|
||||
}
|
||||
if (!check_auth_ok(method, body)) {
|
||||
return err_json(EL_STR("unauthorized"));
|
||||
}
|
||||
@@ -338,15 +993,80 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
||||
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/search"))) {
|
||||
return route_search(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/bm25/search"))) {
|
||||
return route_bm25_search(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) {
|
||||
return route_strengthen(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && ((str_eq(clean, EL_STR("/api/decay")) || str_eq(clean, EL_STR("/api/maintenance"))) || str_eq(clean, EL_STR("/decay")))) {
|
||||
return route_decay(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/export")) || str_eq(clean, EL_STR("/export")))) {
|
||||
return route_export(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) {
|
||||
return route_save(method, path, body);
|
||||
return route_export(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
||||
return route_load(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/reindex")) || str_eq(clean, EL_STR("/reindex")))) {
|
||||
return route_reindex(method, path, body);
|
||||
}
|
||||
if (str_starts_with(clean, EL_STR("/api/neuron/"))) {
|
||||
if (str_eq(clean, EL_STR("/api/neuron/session/begin"))) {
|
||||
return route_neuron_session_begin(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/ctx"))) {
|
||||
return route_neuron_ctx(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/memory"))) {
|
||||
return route_neuron_memory(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
|
||||
return route_neuron_knowledge_capture(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/evolve"))) {
|
||||
return route_neuron_knowledge_evolve(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/knowledge/promote"))) {
|
||||
return route_neuron_knowledge_promote(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/recall"))) {
|
||||
return route_neuron_recall(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/graph/link"))) {
|
||||
return route_neuron_graph_link(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/graph"))) {
|
||||
return route_neuron_graph(method, path, body);
|
||||
}
|
||||
if (str_starts_with(clean, EL_STR("/api/neuron/list/"))) {
|
||||
return route_neuron_list(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/consolidate"))) {
|
||||
return route_neuron_consolidate(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/config"))) {
|
||||
return route_neuron_config(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/state-events"))) {
|
||||
return route_neuron_state_events(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/processes/define"))) {
|
||||
return route_neuron_processes(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/api/neuron/processes"))) {
|
||||
return route_neuron_processes(method, path, body);
|
||||
}
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/events/next"))) {
|
||||
return route_events_next(method, path, body);
|
||||
}
|
||||
if (str_eq(clean, EL_STR("/events/ack"))) {
|
||||
return route_events_ack(method, path, body);
|
||||
}
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -362,9 +1082,20 @@ int main(int _argc, char** _argv) {
|
||||
if (str_eq(data_dir, EL_STR(""))) {
|
||||
data_dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
||||
engram_load(snapshot_path);
|
||||
println(EL_STR("[engram] runtime-native graph engine"));
|
||||
db_path = el_str_concat(data_dir, EL_STR("/engram.db"));
|
||||
loaded = engram_load_binary_el(db_path);
|
||||
if (!loaded) {
|
||||
engram_load_dir(data_dir);
|
||||
if (engram_node_count() == 0) {
|
||||
el_val_t snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
||||
engram_load(snapshot_path);
|
||||
}
|
||||
if (engram_node_count() > 0) {
|
||||
engram_write_binary_el(db_path);
|
||||
println(EL_STR("[engram] migrated legacy data to binary format"));
|
||||
}
|
||||
}
|
||||
println(EL_STR("[engram] runtime-native graph engine (ML-KEM-1024 encrypted)"));
|
||||
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
||||
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
||||
println(el_str_concat(EL_STR("[engram] edge_count="), int_to_str(engram_edge_count())));
|
||||
|
||||
+799
-20
@@ -6,15 +6,297 @@
|
||||
// database.
|
||||
//
|
||||
// Built and linked with:
|
||||
// elc src/server.el > server.c
|
||||
// cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
|
||||
// ./engram
|
||||
// elc src/server.el > ../dist/engram.c
|
||||
// cc -std=c11 -O2 \
|
||||
// -I/Users/will/Development/neuron-technologies/foundation/el/lang/releases/v1.0.0-20260501 \
|
||||
// -I/opt/homebrew/Cellar/liboqs/0.15.0/include \
|
||||
// -I/opt/homebrew/opt/openssl@3/include \
|
||||
// -L/opt/homebrew/Cellar/liboqs/0.15.0/lib \
|
||||
// -L/opt/homebrew/opt/openssl@3/lib \
|
||||
// -lcurl -lpthread -loqs -lssl -lcrypto \
|
||||
// -o ../dist/engram ../dist/engram.c \
|
||||
// /Users/will/Development/neuron-technologies/foundation/el/lang/releases/v1.0.0-20260501/el_runtime.c
|
||||
// ./dist/engram
|
||||
//
|
||||
// Configuration via environment:
|
||||
// ENGRAM_BIND — host:port (default :8742)
|
||||
// ENGRAM_API_KEY — bearer auth (optional)
|
||||
// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram)
|
||||
|
||||
// ── BM25+ text ranking ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Implements BM25+ (Lv & Zhai 2011) for in-process keyword search over the
|
||||
// engram node store. No external dependencies — pure EL, zero Ollama calls.
|
||||
//
|
||||
// Parameters: k1=1.2, b=0.75, delta=1.0
|
||||
//
|
||||
// V1 simplification: n(t) (number of docs containing term t) is approximated
|
||||
// as 1 for all terms. This collapses IDF to a constant per corpus size:
|
||||
// IDF = ln((N - 1 + 0.5) / (1 + 0.5) + 1) = ln((N + 0.5) / 1.5 + 1)
|
||||
// Scoring effectively becomes TF-length-normalised BM25+ (delta term present).
|
||||
// Acceptable for V1; a real inverted index can replace this later.
|
||||
|
||||
fn bm25_tokenize(text: String) -> String {
|
||||
// Lowercase and strip punctuation (replace with spaces), then trim.
|
||||
let t: String = str_to_lower(text)
|
||||
let t = str_replace(t, ".", " ")
|
||||
let t = str_replace(t, ",", " ")
|
||||
let t = str_replace(t, "!", " ")
|
||||
let t = str_replace(t, "?", " ")
|
||||
let t = str_replace(t, "\"", " ")
|
||||
let t = str_replace(t, ":", " ")
|
||||
let t = str_replace(t, ";", " ")
|
||||
let t = str_replace(t, "(", " ")
|
||||
let t = str_replace(t, ")", " ")
|
||||
let t = str_replace(t, "[", " ")
|
||||
let t = str_replace(t, "]", " ")
|
||||
let t = str_replace(t, "{", " ")
|
||||
let t = str_replace(t, "}", " ")
|
||||
let t = str_replace(t, "/", " ")
|
||||
let t = str_replace(t, "\\", " ")
|
||||
let t = str_replace(t, "'", " ")
|
||||
let t = str_replace(t, "-", " ")
|
||||
let t = str_replace(t, "_", " ")
|
||||
let t = str_replace(t, "+", " ")
|
||||
str_trim(t)
|
||||
}
|
||||
|
||||
fn bm25_count_term(term: String, doc_tokens: String) -> Int {
|
||||
// Pad with spaces to avoid prefix/suffix partial matches.
|
||||
let padded_term: String = " " + term + " "
|
||||
let padded_doc: String = " " + doc_tokens + " "
|
||||
str_count(padded_doc, padded_term)
|
||||
}
|
||||
|
||||
fn bm25_score_doc(doc_content: String, query_tokens: String, corpus_size: Int, avg_doc_len: String) -> String {
|
||||
// BM25+ parameters (stored as strings = float-encoded el_val_t from el_from_float)
|
||||
// We use float_add/float_mul/float_div builtins to avoid EL operator issues.
|
||||
// avg_doc_len is passed as a String slot holding an el_val_t float bit-pattern.
|
||||
// (EL has no safe float-passing convention; we work around using str_to_float.)
|
||||
//
|
||||
// V1: n_t=1 for all terms. IDF = ln((N+0.5)/1.5 + 1) = constant per corpus.
|
||||
// This collapses BM25+ to TF-length-normalised scoring — acceptable for V1.
|
||||
let k1: Float = 1.2
|
||||
let b: Float = 0.75
|
||||
let delta: Float = 1.0
|
||||
|
||||
let doc_tokens: String = bm25_tokenize(doc_content)
|
||||
let doc_wc: Int = str_count_words(doc_tokens)
|
||||
if doc_wc == 0 { return "0.0" }
|
||||
|
||||
let doc_len: Float = int_to_float(doc_wc)
|
||||
let avg_len: Float = str_to_float(avg_doc_len)
|
||||
|
||||
// IDF constant
|
||||
let N: Float = int_to_float(corpus_size)
|
||||
// (N + 0.5) / 1.5 + 1.0
|
||||
let idf_arg: Float = float_add(float_div(float_add(N, 1.2), 1.5), 1.0)
|
||||
let idf: Float = math_log(idf_arg)
|
||||
|
||||
// Sum TF component over query terms
|
||||
let terms: List = str_split(query_tokens, " ")
|
||||
let n_terms: Int = len(terms)
|
||||
let score: Float = 0.0
|
||||
let i: Int = 0
|
||||
while i < n_terms {
|
||||
let term: String = get(terms, i)
|
||||
let tlen: Int = str_len(term)
|
||||
if tlen >= 2 {
|
||||
let tf_count: Int = bm25_count_term(term, doc_tokens)
|
||||
if tf_count > 0 {
|
||||
let tf_raw: Float = int_to_float(tf_count)
|
||||
// norm_factor = 1 - b + b * doc_len / avg_len
|
||||
let norm_factor: Float = float_add(float_sub(1.0, b), float_div(float_mul(b, doc_len), avg_len))
|
||||
// tf_comp = delta + tf * (k1+1) / (tf + k1*norm)
|
||||
let numerator: Float = float_mul(tf_raw, float_add(k1, 1.0))
|
||||
let denominator: Float = float_add(tf_raw, float_mul(k1, norm_factor))
|
||||
let tf_comp: Float = float_add(delta, float_div(numerator, denominator))
|
||||
let score = float_add(score, float_mul(idf, tf_comp))
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
// Return score as a string so it survives EL's lack of float-in-list support
|
||||
float_to_str(score)
|
||||
}
|
||||
|
||||
fn bm25_search_json(query: String, limit: Int) -> String {
|
||||
// 1. Determine scan size: floor at 200 so small `limit` values still scan
|
||||
// enough of the corpus to find relevant nodes.
|
||||
// Cap raised from 500 → 5000 (2026-05-24 self-review): 500 was 0.3% of the
|
||||
// 161K-node corpus. At 5000 we cover the top-3% by salience — still fast
|
||||
// (pure C scan, no Ollama calls) and 10x better recall for content search.
|
||||
// engram_scan_nodes_json returns nodes sorted by salience DESC, so ISEs
|
||||
// (salience 0.3) naturally fall below Knowledge/Memory (0.5–0.8), keeping
|
||||
// the effective search corpus content-dense.
|
||||
let scan_limit: Int = limit * 10
|
||||
if scan_limit < 200 { let scan_limit = 200 }
|
||||
if scan_limit > 5000 { let scan_limit = 5000 }
|
||||
|
||||
// 2. Fetch node sample
|
||||
let nodes_json: String = engram_scan_nodes_json(scan_limit, 0)
|
||||
let n: Int = json_array_len(nodes_json)
|
||||
if n == 0 { return "[]" }
|
||||
|
||||
// 3. Compute avg_doc_len from sample
|
||||
let total_words: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let node: String = json_array_get(nodes_json, i)
|
||||
let content: String = json_get_string(node, "content")
|
||||
let tokens: String = bm25_tokenize(content)
|
||||
let wc: Int = str_count_words(tokens)
|
||||
let total_words = total_words + wc
|
||||
let i = i + 1
|
||||
}
|
||||
// avg_doc_len as string for safe float passing
|
||||
let avg_doc_len_f: Float = float_div(int_to_float(total_words), int_to_float(n))
|
||||
let avg_doc_len: String = if float_gt(avg_doc_len_f, 0.0) { float_to_str(avg_doc_len_f) } else { "1.0" }
|
||||
|
||||
// 4. Tokenize query
|
||||
let query_tokens: String = bm25_tokenize(query)
|
||||
if str_eq(str_trim(query_tokens), "") { return "[]" }
|
||||
|
||||
// 5. Score each node; collect results as parallel JSON and score lists.
|
||||
// Scores are stored as strings (float_to_str) to avoid float-in-list issues.
|
||||
let result_nodes: List = 0
|
||||
let result_scores: List = 0
|
||||
let result_count: Int = 0
|
||||
let j: Int = 0
|
||||
while j < n {
|
||||
let node: String = json_array_get(nodes_json, j)
|
||||
let content: String = json_get_string(node, "content")
|
||||
let sc_str: String = bm25_score_doc(content, query_tokens, n, avg_doc_len)
|
||||
// Only include nodes with score > 0.0 (use float comparison, not string match —
|
||||
// float_to_str(0.0) returns "0.000000", not "0.0").
|
||||
if float_gt(str_to_float(sc_str), 0.0) {
|
||||
let result_nodes = list_push(result_nodes, node)
|
||||
let result_scores = list_push(result_scores, sc_str)
|
||||
let result_count = result_count + 1
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
if result_count == 0 { return "[]" }
|
||||
|
||||
// 6. Selection-sort descending by score, take top `limit`
|
||||
let out_limit: Int = if result_count < limit { result_count } else { limit }
|
||||
let k: Int = 0
|
||||
while k < out_limit {
|
||||
// Find max score index in [k, result_count)
|
||||
let max_idx: Int = k
|
||||
let max_sc_str: String = get(result_scores, k)
|
||||
let max_sc_f: Float = str_to_float(max_sc_str)
|
||||
let p: Int = k + 1
|
||||
while p < result_count {
|
||||
let sc2_str: String = get(result_scores, p)
|
||||
let sc2_f: Float = str_to_float(sc2_str)
|
||||
if float_gt(sc2_f, max_sc_f) {
|
||||
let max_sc_f = sc2_f
|
||||
let max_sc_str = sc2_str
|
||||
let max_idx = p
|
||||
}
|
||||
let p = p + 1
|
||||
}
|
||||
// Swap k <-> max_idx
|
||||
if max_idx != k {
|
||||
let tmp_node: String = get(result_nodes, k)
|
||||
let tmp_sc: String = get(result_scores, k)
|
||||
let result_nodes = list_set(result_nodes, k, get(result_nodes, max_idx))
|
||||
let result_scores = list_set(result_scores, k, get(result_scores, max_idx))
|
||||
let result_nodes = list_set(result_nodes, max_idx, tmp_node)
|
||||
let result_scores = list_set(result_scores, max_idx, tmp_sc)
|
||||
}
|
||||
let k = k + 1
|
||||
}
|
||||
|
||||
// 7. Build JSON array of top `out_limit` nodes with bm25_score field
|
||||
let out: String = "["
|
||||
let r: Int = 0
|
||||
while r < out_limit {
|
||||
let node: String = get(result_nodes, r)
|
||||
let sc_str: String = get(result_scores, r)
|
||||
// Inject bm25_score: trim the closing } and append field
|
||||
let node_len: Int = str_len(node)
|
||||
let node_body: String = str_slice(node, 0, node_len - 1)
|
||||
let entry: String = node_body + ",\"bm25_score\":" + sc_str + "}"
|
||||
if r > 0 { let out = out + "," }
|
||||
let out = out + entry
|
||||
let r = r + 1
|
||||
}
|
||||
out + "]"
|
||||
}
|
||||
|
||||
// ── Auto-linking ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// auto_link_content_node — link a newly-created Knowledge or Memory node to
|
||||
// semantically related non-ISE nodes via BM25 search.
|
||||
//
|
||||
// Problem it solves: route_neuron_memory and route_neuron_knowledge_capture
|
||||
// both call engram_node_full directly, creating nodes with zero edges. With
|
||||
// 14K+ ISEs dominating the corpus, BFS traversal contributes nothing — every
|
||||
// query relies solely on lexical/semantic seed matching. Auto-linking builds
|
||||
// explicit "related" edges so activated knowledge nodes fan out to connected
|
||||
// neighbors during BFS.
|
||||
//
|
||||
// Design choices:
|
||||
// - BM25 (not substring search): ranks by relevance, not just occurrence
|
||||
// - Skip InternalStateEvent nodes: ISEs dominate the corpus and are not
|
||||
// useful link targets for knowledge/memory nodes
|
||||
// - Up to 3 edges per node: enough to build graph structure without over-linking
|
||||
// - weight=0.6: moderately strong; causal edges (field-validated at 2.0) are
|
||||
// much stronger, so these "related" edges don't flood activation paths
|
||||
// - state_set for linked counter: EL `let` in nested if-blocks creates inner
|
||||
// scope only; state_set persists across block boundaries (2026-05-25 lesson)
|
||||
//
|
||||
// (2026-05-28 self-review)
|
||||
fn auto_link_content_node(node_id: String, content: String) -> Int {
|
||||
let clen: Int = str_len(content)
|
||||
if clen < 20 { return 0 }
|
||||
|
||||
// Find search term: first word >= 5 chars, or second word.
|
||||
let sp1: Int = str_index_of(content, " ")
|
||||
let w1end: Int = if sp1 < 0 { clen } else { sp1 }
|
||||
let word1: String = str_slice(content, 0, w1end)
|
||||
state_set("aln_term", "")
|
||||
if str_len(word1) >= 5 {
|
||||
state_set("aln_term", word1)
|
||||
}
|
||||
if str_eq(state_get("aln_term"), "") {
|
||||
if sp1 >= 0 {
|
||||
let rest: String = str_slice(content, sp1 + 1, clen)
|
||||
let sp2: Int = str_index_of(rest, " ")
|
||||
let w2end: Int = if sp2 < 0 { str_len(rest) } else { sp2 }
|
||||
let word2: String = str_slice(rest, 0, w2end)
|
||||
if str_len(word2) >= 5 {
|
||||
state_set("aln_term", word2)
|
||||
}
|
||||
}
|
||||
}
|
||||
let search_term: String = state_get("aln_term")
|
||||
if str_eq(search_term, "") { return 0 }
|
||||
|
||||
// BM25 over top-20 results; skip ISE nodes; connect up to 3.
|
||||
let results: String = bm25_search_json(search_term, 20)
|
||||
let n: Int = json_array_len(results)
|
||||
state_set("aln_linked", "0")
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let linked_so_far: Int = str_to_int(state_get("aln_linked"))
|
||||
if linked_so_far < 3 {
|
||||
let elem: String = json_array_get(results, i)
|
||||
let rid: String = json_get_string(elem, "id")
|
||||
let rtype: String = json_get_string(elem, "node_type")
|
||||
if !str_eq(rtype, "InternalStateEvent") && !str_eq(rid, "") && !str_eq(rid, node_id) {
|
||||
engram_connect(node_id, rid, 0.6, "related")
|
||||
state_set("aln_linked", int_to_str(linked_so_far + 1))
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return str_to_int(state_get("aln_linked"))
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_port(bind: String) -> Int {
|
||||
@@ -83,7 +365,13 @@ fn route_create_node(method: String, path: String, body: String) -> String {
|
||||
let salience: Float = json_get_float(body, "salience")
|
||||
if salience == 0.0 { let salience = 0.5 }
|
||||
let id: String = engram_node(content, node_type, salience)
|
||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||
|
||||
// Auto-link via BM25 search — reuse auto_link_content_node which skips
|
||||
// ISE nodes and links to up to 3 semantically related non-ISE nodes.
|
||||
// Replaces the old inline substring-search auto-link (2026-05-29 cleanup).
|
||||
let auto_linked: Int = auto_link_content_node(id, content)
|
||||
|
||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\",\"auto_linked\":" + int_to_str(auto_linked) + "}"
|
||||
}
|
||||
|
||||
fn route_get_node(method: String, path: String, body: String) -> String {
|
||||
@@ -131,7 +419,7 @@ fn route_search(method: String, path: String, body: String) -> String {
|
||||
let limit: Int = query_int(path, "limit", 20)
|
||||
if limit == 0 { let limit = json_get_int(body, "limit") }
|
||||
if limit == 0 { let limit = 20 }
|
||||
return engram_search_json(q, limit)
|
||||
return bm25_search_json(q, limit)
|
||||
}
|
||||
|
||||
fn route_activate(method: String, path: String, body: String) -> String {
|
||||
@@ -145,6 +433,17 @@ fn route_activate(method: String, path: String, body: String) -> String {
|
||||
let bd: Int = json_get_int(body, "depth")
|
||||
if bd > 0 { let depth = bd }
|
||||
}
|
||||
// BM25 pre-bias: strengthen top-10 BM25 results before spreading activation
|
||||
// so semantically relevant nodes already have elevated salience.
|
||||
let top: String = bm25_search_json(q, 10)
|
||||
let nb: Int = json_array_len(top)
|
||||
let bi: Int = 0
|
||||
while bi < nb {
|
||||
let node: String = json_array_get(top, bi)
|
||||
let nid: String = json_get_string(node, "id")
|
||||
if !str_eq(nid, "") { engram_strengthen(nid) }
|
||||
let bi = bi + 1
|
||||
}
|
||||
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
||||
}
|
||||
|
||||
@@ -180,25 +479,41 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
fn route_decay(method: String, path: String, body: String) -> String {
|
||||
engram_apply_decay_json()
|
||||
}
|
||||
|
||||
fn route_export(method: String, path: String, body: String) -> String {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
// Write binary checkpoint
|
||||
let db_path: String = dir + "/engram.db"
|
||||
engram_write_binary_el(db_path)
|
||||
// Also write JSON export for human inspection
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_save(p)
|
||||
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
||||
"{\"ok\":true,\"binary\":\"" + db_path + "\",\"json\":\"" + p + "\"}"
|
||||
}
|
||||
|
||||
fn route_reindex(method: String, path: String, body: String) -> String {
|
||||
engram_reindex_json()
|
||||
}
|
||||
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let db_path: String = dir + "/engram.db"
|
||||
let ok: Bool = engram_load_binary_el(db_path)
|
||||
if !ok {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_load(p)
|
||||
}
|
||||
engram_load(p)
|
||||
ok_json()
|
||||
}
|
||||
|
||||
@@ -206,6 +521,323 @@ fn route_health(method: String, path: String, body: String) -> String {
|
||||
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
|
||||
}
|
||||
|
||||
// ── /api/neuron/* Routes ──────────────────────────────────────────────────────
|
||||
|
||||
// route_neuron_session_begin — activate with broad seeds, return node stats + results
|
||||
fn route_neuron_session_begin(method: String, path: String, body: String) -> String {
|
||||
let results: String = engram_activate_json("memory knowledge context", 2)
|
||||
let nc: Int = engram_node_count()
|
||||
let ec: Int = engram_edge_count()
|
||||
"{\"ok\":true,\"nodes\":" + results + ",\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + "}"
|
||||
}
|
||||
|
||||
// route_neuron_ctx — compile working context from top activated nodes
|
||||
fn route_neuron_ctx(method: String, path: String, body: String) -> String {
|
||||
let results: String = engram_activate_json("architecture decision memory", 2)
|
||||
let n: Int = json_array_len(results)
|
||||
let limit: Int = if n > 10 { 10 } else { n }
|
||||
let ctx: String = "Recent working memory:\n"
|
||||
let i: Int = 0
|
||||
let ctx_body: String = ""
|
||||
while i < limit {
|
||||
let elem: String = json_array_get(results, i)
|
||||
let label: String = json_get_string(elem, "label")
|
||||
let content: String = json_get_string(elem, "content")
|
||||
let clen: Int = str_len(content)
|
||||
let snippet: String = if clen > 200 { str_slice(content, 0, 200) } else { content }
|
||||
let ctx_body = ctx_body + "- [" + label + "]: " + snippet + "\n"
|
||||
let i = i + 1
|
||||
}
|
||||
let full_ctx: String = ctx + ctx_body
|
||||
"{\"ok\":true,\"context\":\"" + str_replace(str_replace(str_replace(full_ctx, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n") + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_memory — create a Memory node with importance-to-tier mapping
|
||||
fn route_neuron_memory(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { return "{\"error\":\"content is required\"}" }
|
||||
let node_type: String = json_get_string(body, "node_type")
|
||||
if str_eq(node_type, "") { let node_type = "Memory" }
|
||||
let label: String = json_get_string(body, "label")
|
||||
let importance: String = json_get_string(body, "importance")
|
||||
let project: String = json_get_string(body, "project")
|
||||
let tags_raw: String = json_get_string(body, "tags")
|
||||
|
||||
// Map importance to tier
|
||||
let tier: String = "Episodic"
|
||||
if str_eq(importance, "critical") { let tier = "Procedural" }
|
||||
if str_eq(importance, "high") { let tier = "Semantic" }
|
||||
if str_eq(importance, "normal") { let tier = "Episodic" }
|
||||
if str_eq(importance, "low") { let tier = "Working" }
|
||||
|
||||
// Override with explicit tier if provided
|
||||
let explicit_tier: String = json_get_string(body, "tier")
|
||||
if !str_eq(explicit_tier, "") { let tier = explicit_tier }
|
||||
|
||||
// Build tags string — append project tag if set
|
||||
let tags_str: String = tags_raw
|
||||
if !str_eq(project, "") {
|
||||
if str_eq(tags_str, "") {
|
||||
let tags_str = "project:" + project
|
||||
}
|
||||
if !str_eq(tags_str, "") {
|
||||
let tags_str = tags_str + " project:" + project
|
||||
}
|
||||
}
|
||||
|
||||
let id: String = engram_node_full(content, node_type, label, 0.5, 0.5, 1.0, tier, tags_str)
|
||||
|
||||
// Auto-link to related non-ISE nodes so this memory is reachable via BFS traversal.
|
||||
// Without this, MCP-created nodes arrive with zero edges and are invisible to
|
||||
// graph spread during activation (only lexical/semantic seed matching finds them).
|
||||
let auto_linked: Int = auto_link_content_node(id, content)
|
||||
|
||||
// Checkpoint after write
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let db_path: String = dir + "/engram.db"
|
||||
engram_write_binary_el(db_path)
|
||||
|
||||
"{\"ok\":true,\"id\":\"" + id + "\",\"auto_linked\":" + int_to_str(auto_linked) + ",\"content\":\"" + str_replace(str_replace(content, "\\", "\\\\"), "\"", "\\\"") + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_knowledge_capture — create a Knowledge node
|
||||
fn route_neuron_knowledge_capture(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { return "{\"error\":\"content is required\"}" }
|
||||
let title: String = json_get_string(body, "title")
|
||||
let category: String = json_get_string(body, "category")
|
||||
let tags_raw: String = json_get_string(body, "tags")
|
||||
let project: String = json_get_string(body, "project")
|
||||
let tier_raw: String = json_get_string(body, "tier")
|
||||
|
||||
// Map tier name to engram tier
|
||||
let tier: String = "Episodic"
|
||||
if str_eq(tier_raw, "lesson") { let tier = "Semantic" }
|
||||
if str_eq(tier_raw, "canonical") { let tier = "Procedural" }
|
||||
if str_eq(tier_raw, "note") { let tier = "Episodic" }
|
||||
|
||||
// Build tags
|
||||
let tags_str: String = tags_raw
|
||||
if !str_eq(category, "") {
|
||||
if str_eq(tags_str, "") {
|
||||
let tags_str = "category:" + category
|
||||
}
|
||||
if !str_eq(tags_str, "") {
|
||||
let tags_str = tags_str + " category:" + category
|
||||
}
|
||||
}
|
||||
if !str_eq(project, "") {
|
||||
if str_eq(tags_str, "") {
|
||||
let tags_str = "project:" + project
|
||||
}
|
||||
if !str_eq(tags_str, "") {
|
||||
let tags_str = tags_str + " project:" + project
|
||||
}
|
||||
}
|
||||
|
||||
let id: String = engram_node_full(content, "Knowledge", title, 0.7, 0.7, 1.0, tier, tags_str)
|
||||
|
||||
// Auto-link to related non-ISE nodes for BFS reachability (same rationale as route_neuron_memory).
|
||||
let auto_linked: Int = auto_link_content_node(id, content)
|
||||
|
||||
// Checkpoint
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let db_path: String = dir + "/engram.db"
|
||||
engram_write_binary_el(db_path)
|
||||
|
||||
"{\"ok\":true,\"id\":\"" + id + "\",\"auto_linked\":" + int_to_str(auto_linked) + "}"
|
||||
}
|
||||
|
||||
// route_neuron_knowledge_evolve — create updated node (evolution via new node)
|
||||
fn route_neuron_knowledge_evolve(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
let prior_id: String = json_get_string(body, "id")
|
||||
if str_eq(content, "") { return "{\"ok\":true}" }
|
||||
let id: String = engram_node_full(content, "Knowledge", "", 0.7, 0.7, 1.0, "Semantic", "evolved")
|
||||
if !str_eq(prior_id, "") && !str_eq(id, "") {
|
||||
engram_connect(id, prior_id, 1.0, "supersedes")
|
||||
}
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
engram_write_binary_el(dir + "/engram.db")
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_knowledge_promote — promote a knowledge node to a higher tier.
|
||||
// Creates a new node with the promoted tier (same content) and connects
|
||||
// via a "supersedes" edge from new → old. Tier mapping:
|
||||
// note/Episodic → lesson/Semantic → canonical/Procedural
|
||||
fn route_neuron_knowledge_promote(method: String, path: String, body: String) -> String {
|
||||
let id: String = json_get_string(body, "id")
|
||||
if str_eq(id, "") { return "{\"ok\":true}" }
|
||||
|
||||
// Read existing node
|
||||
let node_json: String = engram_get_node_json(id)
|
||||
if str_eq(node_json, "") { return err_json("node not found") }
|
||||
if str_eq(node_json, "null") { return err_json("node not found") }
|
||||
|
||||
let content: String = json_get_string(node_json, "content")
|
||||
if str_eq(content, "") { return err_json("node has no content") }
|
||||
let label: String = json_get_string(node_json, "label")
|
||||
let tags: String = json_get_string(node_json, "tags")
|
||||
let current_tier: String = json_get_string(node_json, "tier")
|
||||
|
||||
// Determine target tier: explicit override or auto-promote one level
|
||||
let tier_raw: String = json_get_string(body, "tier")
|
||||
let new_tier: String = ""
|
||||
|
||||
// Explicit tier takes precedence
|
||||
if str_eq(tier_raw, "lesson") { let new_tier = "Semantic" }
|
||||
if str_eq(tier_raw, "canonical") { let new_tier = "Procedural" }
|
||||
if str_eq(tier_raw, "note") { let new_tier = "Episodic" }
|
||||
|
||||
// Auto-promote one level if no explicit tier
|
||||
if str_eq(new_tier, "") {
|
||||
if str_eq(current_tier, "Working") { let new_tier = "Episodic" }
|
||||
if str_eq(current_tier, "Episodic") { let new_tier = "Semantic" }
|
||||
if str_eq(current_tier, "Semantic") { let new_tier = "Procedural" }
|
||||
if str_eq(current_tier, "Procedural") { let new_tier = "Procedural" }
|
||||
}
|
||||
if str_eq(new_tier, "") { let new_tier = "Semantic" }
|
||||
|
||||
// Create promoted node — higher importance (0.8) signals durable knowledge
|
||||
let new_id: String = engram_node_full(content, "Knowledge", label, 0.7, 0.8, 1.0, new_tier, tags)
|
||||
|
||||
// Wire supersedes edge: new node supersedes old
|
||||
if !str_eq(new_id, "") {
|
||||
engram_connect(new_id, id, 1.0, "supersedes")
|
||||
}
|
||||
|
||||
// Checkpoint
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
engram_write_binary_el(dir + "/engram.db")
|
||||
|
||||
"{\"ok\":true,\"id\":\"" + new_id + "\",\"promoted_from\":\"" + id + "\",\"tier\":\"" + new_tier + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_recall — search or list nodes
|
||||
fn route_neuron_recall(method: String, path: String, body: String) -> String {
|
||||
let query: String = json_get_string(body, "query")
|
||||
let chain: String = json_get_string(body, "chain_name")
|
||||
let limit: Int = json_get_int(body, "limit")
|
||||
if limit == 0 { let limit = 20 }
|
||||
let q: String = if str_eq(query, "") { chain } else { query }
|
||||
if str_eq(q, "") {
|
||||
return engram_scan_nodes_json(limit, 0)
|
||||
}
|
||||
return bm25_search_json(q, limit)
|
||||
}
|
||||
|
||||
// route_neuron_graph — get node + search-based neighbor approximation.
|
||||
// engram_neighbors_json crashes on large graphs (15k+ edges exceeds BFS cap).
|
||||
// Use a search-based approach instead: search by the node id string, which
|
||||
// returns connected nodes that share content with the target id in edges/tags.
|
||||
// For the mcp-wrapper callers this is sufficient — they just need the node itself.
|
||||
fn route_neuron_graph(method: String, path: String, body: String) -> String {
|
||||
let id: String = query_param(path, "id")
|
||||
if str_eq(id, "") { return "{\"error\":\"id is required\"}" }
|
||||
let node_json: String = engram_get_node_json(id)
|
||||
// Return node with empty neighbors — safe fallback avoids BFS crash
|
||||
"{\"ok\":true,\"node\":" + node_json + ",\"neighbors\":[]}"
|
||||
}
|
||||
|
||||
// route_neuron_graph_link — create edge between nodes
|
||||
fn route_neuron_graph_link(method: String, path: String, body: String) -> String {
|
||||
let from_id: String = json_get_string(body, "from_id")
|
||||
let to_id: String = json_get_string(body, "to_id")
|
||||
if str_eq(from_id, "") || str_eq(to_id, "") {
|
||||
return "{\"error\":\"from_id and to_id are required\"}"
|
||||
}
|
||||
let relation: String = json_get_string(body, "relation")
|
||||
if str_eq(relation, "") { let relation = "related" }
|
||||
let weight: Float = json_get_float(body, "weight")
|
||||
if weight == 0.0 { let weight = 0.5 }
|
||||
engram_connect(from_id, to_id, weight, relation)
|
||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_list — list nodes by type extracted from path
|
||||
fn route_neuron_list(method: String, path: String, body: String) -> String {
|
||||
let clean: String = strip_query(path)
|
||||
let prefix: String = "/api/neuron/list/"
|
||||
let node_type: String = str_slice(clean, str_len(prefix), str_len(clean))
|
||||
let limit: Int = query_int(path, "limit", 50)
|
||||
if str_eq(node_type, "") { return "[]" }
|
||||
return engram_scan_nodes_by_type_json(node_type, limit, 0)
|
||||
}
|
||||
|
||||
// route_neuron_consolidate — checkpoint and return counts
|
||||
fn route_neuron_consolidate(method: String, path: String, body: String) -> String {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let db_path: String = dir + "/engram.db"
|
||||
engram_write_binary_el(db_path)
|
||||
let nc: Int = engram_node_count()
|
||||
let ec: Int = engram_edge_count()
|
||||
"{\"ok\":true,\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + "}"
|
||||
}
|
||||
|
||||
// route_neuron_config — return stub config values
|
||||
fn route_neuron_config(method: String, path: String, body: String) -> String {
|
||||
let key: String = query_param(path, "key")
|
||||
"{\"key\":\"" + key + "\",\"value\":\"\"}"
|
||||
}
|
||||
|
||||
// route_neuron_state_events — GET lists ISEs, POST logs a new one.
|
||||
// GET supports ?limit=N&offset=M for pagination; ?label=X to extract label
|
||||
// from the ISE content's "event" field.
|
||||
// ISEs sort by created_at DESC (most-recent-first) as of 2026-05-23 fix.
|
||||
// ?limit=10 returns the 10 most recent ISEs. Offset for pagination, not for
|
||||
// skipping to recent events (that was the pre-fix behavior; no longer needed).
|
||||
fn route_neuron_state_events(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") {
|
||||
let limit_str: String = query_param(path, "limit")
|
||||
let limit: Int = if str_eq(limit_str, "") { 50 } else { str_to_int(limit_str) }
|
||||
let offset_str: String = query_param(path, "offset")
|
||||
let offset: Int = if str_eq(offset_str, "") { 0 } else { str_to_int(offset_str) }
|
||||
return engram_scan_nodes_by_type_json("InternalStateEvent", limit, offset)
|
||||
}
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { let content = body }
|
||||
// Extract label from content JSON "event" field for better ISE searchability
|
||||
let event_label: String = json_get_string(content, "event")
|
||||
let label: String = if str_eq(event_label, "") { "state-event" } else { event_label }
|
||||
let id: String = engram_node_full(content, "InternalStateEvent", label, 0.3, 0.3, 1.0, "Working", "internal-state")
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_processes — stub
|
||||
fn route_neuron_processes(method: String, path: String, body: String) -> String {
|
||||
"{\"ok\":true,\"processes\":[]}"
|
||||
}
|
||||
|
||||
// route_events_next — stub empty event queue
|
||||
fn route_events_next(method: String, path: String, body: String) -> String {
|
||||
"{\"ok\":true,\"event\":null}"
|
||||
}
|
||||
|
||||
// route_events_ack — stub ack
|
||||
fn route_events_ack(method: String, path: String, body: String) -> String {
|
||||
"{\"ok\":true}"
|
||||
}
|
||||
|
||||
fn route_bm25_search(method: String, path: String, body: String) -> String {
|
||||
let q: String = ""
|
||||
if str_eq(method, "GET") {
|
||||
let q = query_param(path, "q")
|
||||
} else {
|
||||
let q = json_get_string(body, "query")
|
||||
}
|
||||
if str_eq(q, "") { return "{\"error\":\"query is required\"}" }
|
||||
let limit: Int = query_int(path, "limit", 20)
|
||||
if limit == 0 { let limit = json_get_int(body, "limit") }
|
||||
if limit == 0 { let limit = 20 }
|
||||
bm25_search_json(q, limit)
|
||||
}
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||||
@@ -232,6 +864,63 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// /api/neuron/* and /events/* are pre-auth — the mcp-wrapper is a trusted
|
||||
// local service that cannot inject _auth into its request bodies.
|
||||
if str_starts_with(clean, "/api/neuron/") || str_starts_with(clean, "/events/") {
|
||||
if str_eq(clean, "/api/neuron/session/begin") {
|
||||
return route_neuron_session_begin(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/ctx") {
|
||||
return route_neuron_ctx(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/memory") {
|
||||
return route_neuron_memory(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/capture") {
|
||||
return route_neuron_knowledge_capture(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/evolve") {
|
||||
return route_neuron_knowledge_evolve(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/promote") {
|
||||
return route_neuron_knowledge_promote(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/recall") {
|
||||
return route_neuron_recall(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/graph/link") {
|
||||
return route_neuron_graph_link(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/graph") {
|
||||
return route_neuron_graph(method, path, body)
|
||||
}
|
||||
if str_starts_with(clean, "/api/neuron/list/") {
|
||||
return route_neuron_list(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/consolidate") {
|
||||
return route_neuron_consolidate(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/config") {
|
||||
return route_neuron_config(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/state-events") {
|
||||
return route_neuron_state_events(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/processes/define") {
|
||||
return route_neuron_processes(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/processes") {
|
||||
return route_neuron_processes(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/events/next") {
|
||||
return route_events_next(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/events/ack") {
|
||||
return route_events_ack(method, path, body)
|
||||
}
|
||||
return err_json("not found")
|
||||
}
|
||||
|
||||
// Auth (when ENGRAM_API_KEY is set)
|
||||
if !check_auth_ok(method, body) {
|
||||
return err_json("unauthorized")
|
||||
@@ -281,18 +970,93 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
return route_search(method, path, body)
|
||||
}
|
||||
|
||||
// BM25+ text ranking
|
||||
if str_eq(clean, "/api/bm25/search") {
|
||||
return route_bm25_search(method, path, body)
|
||||
}
|
||||
|
||||
// Strengthen
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/strengthen") || str_eq(clean, "/strengthen")) {
|
||||
return route_strengthen(method, path, body)
|
||||
}
|
||||
|
||||
// Temporal decay maintenance
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/decay") || str_eq(clean, "/api/maintenance") || str_eq(clean, "/decay")) {
|
||||
return route_decay(method, path, body)
|
||||
}
|
||||
|
||||
// Persistence
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/export") || str_eq(clean, "/export")) {
|
||||
return route_export(method, path, body)
|
||||
}
|
||||
// /api/save is kept as a backward-compat alias for /api/export
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
|
||||
return route_save(method, path, body)
|
||||
return route_export(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||||
return route_load(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/reindex") || str_eq(clean, "/reindex")) {
|
||||
return route_reindex(method, path, body)
|
||||
}
|
||||
|
||||
// ── /api/neuron/* ─────────────────────────────────────────────────────────
|
||||
if str_starts_with(clean, "/api/neuron/") {
|
||||
// Specific sub-paths first (longer matches before shorter)
|
||||
if str_eq(clean, "/api/neuron/session/begin") {
|
||||
return route_neuron_session_begin(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/ctx") {
|
||||
return route_neuron_ctx(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/memory") {
|
||||
return route_neuron_memory(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/capture") {
|
||||
return route_neuron_knowledge_capture(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/evolve") {
|
||||
return route_neuron_knowledge_evolve(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/knowledge/promote") {
|
||||
return route_neuron_knowledge_promote(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/recall") {
|
||||
return route_neuron_recall(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/graph/link") {
|
||||
return route_neuron_graph_link(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/graph") {
|
||||
return route_neuron_graph(method, path, body)
|
||||
}
|
||||
if str_starts_with(clean, "/api/neuron/list/") {
|
||||
return route_neuron_list(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/consolidate") {
|
||||
return route_neuron_consolidate(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/config") {
|
||||
return route_neuron_config(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/state-events") {
|
||||
return route_neuron_state_events(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/processes/define") {
|
||||
return route_neuron_processes(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/processes") {
|
||||
return route_neuron_processes(method, path, body)
|
||||
}
|
||||
}
|
||||
|
||||
// ── /events/* ─────────────────────────────────────────────────────────────
|
||||
if str_eq(clean, "/events/next") {
|
||||
return route_events_next(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/events/ack") {
|
||||
return route_events_ack(method, path, body)
|
||||
}
|
||||
|
||||
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
|
||||
}
|
||||
@@ -303,13 +1067,28 @@ let bind_str: String = env("ENGRAM_BIND")
|
||||
if str_eq(bind_str, "") { let bind_str = ":8742" }
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
// On startup, load from binary database (ML-KEM-1024 encrypted).
|
||||
// Falls back to per-file JSON, then snapshot.json for migration from older formats.
|
||||
let data_dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
|
||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||
engram_load(snapshot_path)
|
||||
let db_path: String = data_dir + "/engram.db"
|
||||
let loaded: Bool = engram_load_binary_el(db_path)
|
||||
if !loaded {
|
||||
// Migration path: try per-file JSON
|
||||
engram_load_dir(data_dir)
|
||||
if engram_node_count() == 0 {
|
||||
// Final fallback: legacy snapshot.json
|
||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||
engram_load(snapshot_path)
|
||||
}
|
||||
// If we loaded anything from legacy format, save as binary immediately
|
||||
if engram_node_count() > 0 {
|
||||
engram_write_binary_el(db_path)
|
||||
println("[engram] migrated legacy data to binary format")
|
||||
}
|
||||
}
|
||||
|
||||
println("[engram] runtime-native graph engine")
|
||||
println("[engram] runtime-native graph engine (ML-KEM-1024 encrypted)")
|
||||
println("[engram] data_dir=" + data_dir)
|
||||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||||
println("[engram] edge_count=" + int_to_str(engram_edge_count()))
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+12
@@ -4342,6 +4342,9 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("http_serve"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_serve_async"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("http_set_handler"))) {
|
||||
return 1;
|
||||
}
|
||||
@@ -4546,6 +4549,12 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("engram_load"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_load_dir"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_reindex_json"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_get_node_json"))) {
|
||||
return 1;
|
||||
}
|
||||
@@ -4564,6 +4573,9 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("engram_stats_json"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_apply_decay_json"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(name, EL_STR("llm_call"))) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/resource.h> /* getrusage — memory guard */
|
||||
#ifdef HAVE_CURL
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
@@ -177,15 +178,17 @@ static el_val_t el_wrap_str(char* s) {
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s) {
|
||||
el_val_t println(el_val_t s) {
|
||||
const char* str = EL_CSTR(s);
|
||||
if (str) puts(str);
|
||||
else puts("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void print(el_val_t s) {
|
||||
el_val_t print(el_val_t s) {
|
||||
const char* str = EL_CSTR(s);
|
||||
if (str) fputs(str, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t readline(void) {
|
||||
@@ -292,6 +295,10 @@ el_val_t str_to_int(el_val_t sv) {
|
||||
return (el_val_t)atoll(s);
|
||||
}
|
||||
|
||||
/* native_str_to_int — El compiler-generated alias for str_to_int.
|
||||
* Converts a string el_val_t to its integer representation. */
|
||||
el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); }
|
||||
|
||||
el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
if (!s) return el_wrap_str(el_strdup(""));
|
||||
@@ -907,6 +914,33 @@ el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_ma
|
||||
return r;
|
||||
}
|
||||
|
||||
/* http_post_json_with_headers — POST with Content-Type: application/json plus
|
||||
* any additional headers supplied as an El map. Combines http_post_json and
|
||||
* http_post_with_headers: the Content-Type header is always prepended so
|
||||
* callers do not have to include it in their map. */
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) {
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
/* Append caller-supplied headers from the map */
|
||||
ElMap* m = as_map(headers_map);
|
||||
if (m) {
|
||||
for (int64_t i = 0; i < m->count; i++) {
|
||||
const char* k = EL_CSTR(m->keys[i]);
|
||||
const char* v = EL_CSTR(m->values[i]);
|
||||
if (!k || !v) continue;
|
||||
size_t n = strlen(k) + strlen(v) + 4;
|
||||
char* line = malloc(n);
|
||||
if (!line) continue;
|
||||
snprintf(line, n, "%s: %s", k, v);
|
||||
h = curl_slist_append(h, line);
|
||||
free(line);
|
||||
}
|
||||
}
|
||||
el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h);
|
||||
curl_slist_free_all(h);
|
||||
return r;
|
||||
}
|
||||
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) {
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded");
|
||||
@@ -1059,7 +1093,7 @@ void el_runtime_register_handler(const char* name, http_handler_fn fn) {
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
}
|
||||
|
||||
void http_set_handler(el_val_t name) {
|
||||
el_val_t http_set_handler(el_val_t name) {
|
||||
const char* n = EL_CSTR(name);
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
free(_http_active_handler);
|
||||
@@ -1083,6 +1117,7 @@ void http_set_handler(el_val_t name) {
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static http_handler_fn http_lookup_active(void) {
|
||||
@@ -1540,18 +1575,18 @@ static void* http_worker(void* arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void http_serve(el_val_t port, el_val_t handler) {
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler) {
|
||||
/* If `handler` looks like a string name, register it as the active handler. */
|
||||
const char* hname = EL_CSTR(handler);
|
||||
if (hname && looks_like_string(handler)) {
|
||||
http_set_handler(handler);
|
||||
}
|
||||
int p = (int)port;
|
||||
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; }
|
||||
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; }
|
||||
/* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6.
|
||||
* This makes `localhost` work in browsers that resolve it to ::1 first. */
|
||||
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
if (sock < 0) { perror("socket"); return; }
|
||||
if (sock < 0) { perror("socket"); return 0; }
|
||||
int yes = 1; int no = 0;
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
||||
@@ -1561,9 +1596,9 @@ void http_serve(el_val_t port, el_val_t handler) {
|
||||
addr.sin6_addr = in6addr_any;
|
||||
addr.sin6_port = htons((uint16_t)p);
|
||||
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind"); close(sock); return;
|
||||
perror("bind"); close(sock); return 0;
|
||||
}
|
||||
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
|
||||
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
|
||||
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
|
||||
while (1) {
|
||||
struct sockaddr_in6 cli;
|
||||
@@ -1594,6 +1629,7 @@ void http_serve(el_val_t port, el_val_t handler) {
|
||||
pthread_detach(tid);
|
||||
}
|
||||
close(sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── HTTP server v2 — request headers + structured response ──────────────── */
|
||||
@@ -1645,7 +1681,7 @@ void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) {
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
}
|
||||
|
||||
void http_set_handler_v2(el_val_t name) {
|
||||
el_val_t http_set_handler_v2(el_val_t name) {
|
||||
const char* n = EL_CSTR(name);
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
free(_http_active_handler4);
|
||||
@@ -1667,6 +1703,7 @@ void http_set_handler_v2(el_val_t name) {
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static http_handler4_fn http_lookup_active_v2(void) {
|
||||
@@ -1787,7 +1824,7 @@ static void* http_worker_v2(void* arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
const char* hname = EL_CSTR(handler);
|
||||
if (hname && looks_like_string(handler)) {
|
||||
http_set_handler_v2(handler);
|
||||
@@ -1795,11 +1832,11 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
int p = (int)port;
|
||||
if (p <= 0 || p > 65535) {
|
||||
fprintf(stderr, "http_serve_v2: invalid port %d\n", p);
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
/* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */
|
||||
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
if (sock < 0) { perror("socket"); return; }
|
||||
if (sock < 0) { perror("socket"); return 0; }
|
||||
int yes = 1; int no = 0;
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
||||
@@ -1809,9 +1846,9 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
addr.sin6_addr = in6addr_any;
|
||||
addr.sin6_port = htons((uint16_t)p);
|
||||
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind"); close(sock); return;
|
||||
perror("bind"); close(sock); return 0;
|
||||
}
|
||||
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
|
||||
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
|
||||
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
|
||||
while (1) {
|
||||
struct sockaddr_in6 cli;
|
||||
@@ -1842,6 +1879,7 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
pthread_detach(tid);
|
||||
}
|
||||
close(sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build the response envelope a 4-arg handler can return. We hand-write
|
||||
@@ -2204,6 +2242,43 @@ el_val_t url_decode(el_val_t sv) {
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* ── html_raw ────────────────────────────────────────────────────────────────
|
||||
* Identity passthrough for raw HTML template interpolation.
|
||||
* El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is
|
||||
* without any escaping. The caller is responsible for safety.
|
||||
*/
|
||||
el_val_t html_raw(el_val_t s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ── html_escape ─────────────────────────────────────────────────────────────
|
||||
* Escape < > " ' & for safe HTML text interpolation.
|
||||
* El's {expr} in HTML templates compiles to html_escape(expr).
|
||||
*/
|
||||
el_val_t html_escape(el_val_t sv) {
|
||||
const char* src = EL_CSTR(sv);
|
||||
if (!src) return EL_STR("");
|
||||
size_t len = strlen(src);
|
||||
/* Worst case: every byte → 6 chars (") */
|
||||
char* out = (char*)malloc(len * 6 + 1);
|
||||
if (!out) return sv;
|
||||
el_arena_track(out);
|
||||
char* p = out;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
switch (c) {
|
||||
case '&': memcpy(p, "&", 5); p += 5; break;
|
||||
case '<': memcpy(p, "<", 4); p += 4; break;
|
||||
case '>': memcpy(p, ">", 4); p += 4; break;
|
||||
case '"': memcpy(p, """, 6); p += 6; break;
|
||||
case '\'': memcpy(p, "'", 5); p += 5; break;
|
||||
default: *p++ = (char)c; break;
|
||||
}
|
||||
}
|
||||
*p = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input, allowlist_json)
|
||||
*
|
||||
@@ -4999,7 +5074,13 @@ el_val_t state_get_or(el_val_t key, el_val_t default_val) {
|
||||
|
||||
el_val_t float_to_str(el_val_t f) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%g", el_to_float(f));
|
||||
double v = el_to_float(f);
|
||||
/* Normalize NaN to "nan" regardless of sign — platform-independent. */
|
||||
if (isnan(v)) {
|
||||
snprintf(buf, sizeof(buf), "nan");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%g", v);
|
||||
}
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
@@ -5619,8 +5700,9 @@ el_val_t parse_int(el_val_t sv, el_val_t default_val) {
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code) {
|
||||
el_val_t exit_program(el_val_t code) {
|
||||
exit((int)code);
|
||||
return 0; /* unreachable */
|
||||
}
|
||||
|
||||
/* getpid_now — current process id. Named with the _now suffix to avoid
|
||||
@@ -5630,6 +5712,50 @@ el_val_t getpid_now(void) {
|
||||
return (el_val_t)getpid();
|
||||
}
|
||||
|
||||
/* el_mem_check — self-terminating memory guard for long-running compiler runs.
|
||||
*
|
||||
* Call this periodically (e.g. after each function compiled) to detect runaway
|
||||
* memory growth before the OS OOM-killer fires. Reads the limit from the env
|
||||
* var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit,
|
||||
* prints a diagnostic to stderr and exits with code 1 so the caller (elb or a
|
||||
* CI script) can handle the failure gracefully instead of having the whole
|
||||
* machine go down.
|
||||
*
|
||||
* Platform notes:
|
||||
* macOS — ru_maxrss is in bytes.
|
||||
* Linux — ru_maxrss is in kilobytes.
|
||||
* We normalise to MB before comparing.
|
||||
*
|
||||
* Returns 0 always (the only non-return path is the exit() branch).
|
||||
*/
|
||||
el_val_t el_mem_check(void) {
|
||||
/* Read limit from env; default 512 MB. */
|
||||
long limit_mb = 512;
|
||||
const char *env_val = getenv("ELC_MAX_MEM_MB");
|
||||
if (env_val && *env_val) {
|
||||
long v = atol(env_val);
|
||||
if (v > 0) limit_mb = v;
|
||||
}
|
||||
|
||||
struct rusage ru;
|
||||
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */
|
||||
|
||||
long rss_mb;
|
||||
#if defined(__APPLE__) || defined(__MACH__)
|
||||
/* macOS: ru_maxrss is bytes */
|
||||
rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L));
|
||||
#else
|
||||
/* Linux: ru_maxrss is kilobytes */
|
||||
rss_mb = (long)(ru.ru_maxrss / 1024L);
|
||||
#endif
|
||||
|
||||
if (rss_mb >= limit_mb) {
|
||||
fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb);
|
||||
exit(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── args() — command-line argument access ──────────────────────────────────
|
||||
* Compiled El programs call args() to get a list of CLI arguments.
|
||||
* Call el_runtime_init_args(argc, argv) at the start of C main() to populate.
|
||||
@@ -6895,6 +7021,83 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0;
|
||||
}
|
||||
|
||||
/* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ──
|
||||
* For each pair of edges (A→B, B→C) where all three nodes were reached,
|
||||
* create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8
|
||||
* if no A→C edge already exists. Cap at 64 new edges per call.
|
||||
*
|
||||
* IMPORTANT: collect candidates FIRST into a flat array (no pointers into
|
||||
* g->edges held across the apply pass), then apply after — this avoids
|
||||
* dangling pointer bugs if engram_grow_edges() reallocs the array. */
|
||||
{
|
||||
const int64_t INFER_CAP = 64;
|
||||
typedef struct { char from[64]; char to[64]; double weight; } InferCandidate;
|
||||
InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate));
|
||||
int64_t ncands = 0;
|
||||
int64_t snap_ec = g->edge_count;
|
||||
if (cands) {
|
||||
for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) {
|
||||
EngramEdge* ea = &g->edges[e1];
|
||||
if (!ea->from_id || !ea->to_id) continue;
|
||||
int64_t ai = engram_find_node_index(ea->from_id);
|
||||
int64_t bi = engram_find_node_index(ea->to_id);
|
||||
if (ai < 0 || bi < 0) continue;
|
||||
if (!reached[ai] || !reached[bi]) continue;
|
||||
for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) {
|
||||
if (e2 == e1) continue;
|
||||
EngramEdge* eb = &g->edges[e2];
|
||||
if (!eb->from_id || !eb->to_id) continue;
|
||||
if (strcmp(eb->from_id, ea->to_id) != 0) continue;
|
||||
int64_t ci = engram_find_node_index(eb->to_id);
|
||||
if (ci < 0 || !reached[ci]) continue;
|
||||
if (ai == ci) continue;
|
||||
int already = 0;
|
||||
for (int64_t ex = 0; ex < snap_ec; ex++) {
|
||||
EngramEdge* ee = &g->edges[ex];
|
||||
if (ee->from_id && ee->to_id &&
|
||||
strcmp(ee->from_id, ea->from_id) == 0 &&
|
||||
strcmp(ee->to_id, eb->to_id) == 0) {
|
||||
already = 1; break;
|
||||
}
|
||||
}
|
||||
if (already) continue;
|
||||
int dup = 0;
|
||||
for (int64_t k = 0; k < ncands; k++) {
|
||||
if (strcmp(cands[k].from, ea->from_id) == 0 &&
|
||||
strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; }
|
||||
}
|
||||
if (dup) continue;
|
||||
double inf_w = ea->weight * eb->weight * 0.8;
|
||||
if (inf_w < 0.05) inf_w = 0.05;
|
||||
if (inf_w > 1.0) inf_w = 1.0;
|
||||
strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0';
|
||||
strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0';
|
||||
cands[ncands].weight = inf_w;
|
||||
ncands++;
|
||||
}
|
||||
}
|
||||
for (int64_t k = 0; k < ncands; k++) {
|
||||
engram_grow_edges();
|
||||
EngramEdge* ne = &g->edges[g->edge_count];
|
||||
memset(ne, 0, sizeof(*ne));
|
||||
ne->id = engram_new_id();
|
||||
/* Use strdup (not el_strdup) so these persist beyond the request. */
|
||||
ne->from_id = strdup(cands[k].from);
|
||||
ne->to_id = strdup(cands[k].to);
|
||||
ne->relation = strdup("inferred");
|
||||
ne->metadata = strdup("{}");
|
||||
ne->weight = cands[k].weight;
|
||||
ne->confidence = 0.8;
|
||||
ne->created_at = now_ms;
|
||||
ne->updated_at = now_ms;
|
||||
ne->last_fired = now_ms;
|
||||
ne->layer_id = ENGRAM_LAYER_DEFAULT;
|
||||
g->edge_count++;
|
||||
}
|
||||
free(cands);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── PASS 2: executive filter → working memory promotion ──────────── */
|
||||
/* Step A: collect inhibitory suppressions from fired inhibitory edges.
|
||||
* Layered consciousness: inhibition is ONLY recorded against targets
|
||||
@@ -6990,6 +7193,66 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
g->nodes[i].working_memory_weight = wm_weights[i];
|
||||
}
|
||||
|
||||
/* ── HEBBIAN STRENGTHENING: fire together, wire together ─────────────
|
||||
* For each pair of co-promoted nodes (working_memory_weight > 0) that
|
||||
* share an edge, boost that edge's weight by 0.05 (capped at 1.0).
|
||||
* Also increment activation_count and update last_activated on promoted
|
||||
* nodes — this is what drives tier migration below. */
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (wm_weights[i] <= 0.0) continue;
|
||||
EngramNode* n = &g->nodes[i];
|
||||
n->activation_count++;
|
||||
n->last_activated = now_ms;
|
||||
n->updated_at = now_ms;
|
||||
}
|
||||
for (int64_t ei = 0; ei < g->edge_count; ei++) {
|
||||
EngramEdge* e = &g->edges[ei];
|
||||
if (!e->from_id || !e->to_id) continue;
|
||||
int64_t src = engram_find_node_index(e->from_id);
|
||||
int64_t tgt = engram_find_node_index(e->to_id);
|
||||
if (src < 0 || tgt < 0) continue;
|
||||
if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) {
|
||||
e->weight += 0.05;
|
||||
if (e->weight > 1.0) e->weight = 1.0;
|
||||
e->last_fired = now_ms;
|
||||
e->updated_at = now_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─
|
||||
* 0–4 → Working
|
||||
* 5–19 → Episodic
|
||||
* 20–49 → Semantic
|
||||
* 50+ → Procedural
|
||||
* Only upgrade (never downgrade) to preserve earned tier. */
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
const char* target_tier = NULL;
|
||||
int64_t ac = n->activation_count;
|
||||
if (ac >= 50) target_tier = "Procedural";
|
||||
else if (ac >= 20) target_tier = "Semantic";
|
||||
else if (ac >= 5) target_tier = "Episodic";
|
||||
else target_tier = "Working";
|
||||
if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) {
|
||||
/* Only upgrade (Working < Episodic < Semantic < Procedural). */
|
||||
int cur_rank = 0, new_rank = 0;
|
||||
if (strcmp(n->tier, "Working") == 0) cur_rank = 0;
|
||||
else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1;
|
||||
else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2;
|
||||
else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3;
|
||||
if (strcmp(target_tier, "Working") == 0) new_rank = 0;
|
||||
else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1;
|
||||
else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2;
|
||||
else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3;
|
||||
if (new_rank > cur_rank) {
|
||||
free(n->tier);
|
||||
/* Use strdup (not el_strdup) so tier string persists beyond the request. */
|
||||
n->tier = strdup(target_tier);
|
||||
n->updated_at = now_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Collect all background-activated nodes for the return value ────
|
||||
* Callers see both layers. Context compilation uses only promoted nodes
|
||||
* (working_memory_weight > 0). Sort: promoted first by wm_weight desc,
|
||||
@@ -7763,6 +8026,97 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) {
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* ── engram_apply_decay_json — temporal decay maintenance ────────────────────
|
||||
*
|
||||
* Iterates ALL nodes and applies temporal decay to their stored `salience`
|
||||
* field based on time elapsed since `last_activated`:
|
||||
*
|
||||
* new_salience = current_salience * decay_rate ^ hours_since_activation
|
||||
*
|
||||
* where decay_rate defaults to 0.5^(1/168) per hour (half-life one week),
|
||||
* or the node's own `temporal_decay_rate` if non-zero.
|
||||
*
|
||||
* Nodes with temporal_decay_rate == 0 are NOT immune — the global default
|
||||
* applies. To make a node truly immune, set temporal_decay_rate to a very
|
||||
* small positive value (e.g. 0.0001). Nodes that are "pinned" can be
|
||||
* identified by a tier of "Procedural" — those are skipped.
|
||||
*
|
||||
* After updating salience, nodes with salience < 0.05 AND tier == "Working"
|
||||
* are pruned (deleted) unless they have no content (guard against garbage).
|
||||
*
|
||||
* Returns a JSON summary: {"updated": N, "pruned": N} */
|
||||
el_val_t engram_apply_decay_json(void) {
|
||||
EngramStore* g = engram_get();
|
||||
int64_t now_ms = engram_now_ms();
|
||||
int64_t updated = 0, pruned = 0;
|
||||
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
/* Skip Procedural nodes (they are "locked in"). */
|
||||
if (n->tier && strcmp(n->tier, "Procedural") == 0) continue;
|
||||
int64_t age_ms = now_ms - n->last_activated;
|
||||
if (age_ms <= 0) continue;
|
||||
double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate
|
||||
: ENGRAM_DECAY_LAMBDA;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS);
|
||||
double new_salience = n->salience * decay_factor;
|
||||
if (new_salience < 0.0) new_salience = 0.0;
|
||||
if (new_salience != n->salience) {
|
||||
n->salience = new_salience;
|
||||
n->updated_at = now_ms;
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prune low-salience Working nodes. Walk backwards to allow in-place
|
||||
* removal without invalidating indices. */
|
||||
for (int64_t i = g->node_count - 1; i >= 0; i--) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->salience >= 0.05) continue;
|
||||
/* Only prune Working tier nodes — higher tiers are protected. */
|
||||
if (!n->tier || strcmp(n->tier, "Working") != 0) continue;
|
||||
/* Guard: skip nodes with no content. */
|
||||
if (!n->content || !*n->content) continue;
|
||||
/* Free node strings. */
|
||||
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
||||
free(n->tier); free(n->tags); free(n->metadata);
|
||||
/* Shift remaining nodes down. */
|
||||
for (int64_t j = i + 1; j < g->node_count; j++) {
|
||||
g->nodes[j - 1] = g->nodes[j];
|
||||
}
|
||||
g->node_count--;
|
||||
memset(&g->nodes[g->node_count], 0, sizeof(EngramNode));
|
||||
pruned++;
|
||||
}
|
||||
|
||||
/* Remove dangling edges for pruned nodes (any edge whose endpoint no
|
||||
* longer exists in the node list). */
|
||||
if (pruned > 0) {
|
||||
int64_t w = 0;
|
||||
for (int64_t r = 0; r < g->edge_count; r++) {
|
||||
EngramEdge* e = &g->edges[r];
|
||||
int dangling = 0;
|
||||
if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1;
|
||||
if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1;
|
||||
if (dangling) {
|
||||
free(e->id); free(e->from_id); free(e->to_id);
|
||||
free(e->relation); free(e->metadata);
|
||||
} else {
|
||||
if (w != r) g->edges[w] = g->edges[r];
|
||||
w++;
|
||||
}
|
||||
}
|
||||
g->edge_count = w;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}",
|
||||
(long long)updated, (long long)pruned);
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
#ifdef HAVE_CURL
|
||||
/* ── DHARMA network ─────────────────────────────────────────────────────────
|
||||
* Real implementation. Peers are addressed by `dharma_id` — either bare
|
||||
@@ -11177,6 +11531,175 @@ el_val_t hash_sha256(el_val_t sv) {
|
||||
return el_hex_encode(digest, 32);
|
||||
}
|
||||
|
||||
/* ── __ prefixed aliases — public boundary for compiled El programs ──────────
|
||||
*
|
||||
* The El compiler's self-hosting back-end emits calls to __-prefixed function
|
||||
* names (e.g. __println, __str_len). These wrappers forward to the existing
|
||||
* el_runtime implementations so both naming conventions resolve at link time.
|
||||
*
|
||||
* Note: __thread_create and __thread_join are already defined above in the
|
||||
* threading section; they are not repeated here.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s) { return println(s); }
|
||||
el_val_t __print(el_val_t s) { return print(s); }
|
||||
el_val_t __readline(void) { return readline(); }
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n) { return int_to_str(n); }
|
||||
el_val_t __str_to_int(el_val_t s) { return str_to_int(s); }
|
||||
el_val_t __float_to_str(el_val_t f) { return float_to_str(f); }
|
||||
el_val_t __str_to_float(el_val_t s) { return str_to_float(s); }
|
||||
el_val_t __str_len(el_val_t s) { return str_len(s); }
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); }
|
||||
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b) {
|
||||
const char* ca = EL_CSTR(a);
|
||||
const char* cb = EL_CSTR(b);
|
||||
if (!ca) ca = "";
|
||||
if (!cb) cb = "";
|
||||
return (el_val_t)strcmp(ca, cb);
|
||||
}
|
||||
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) {
|
||||
const char* ca = EL_CSTR(a);
|
||||
const char* cb = EL_CSTR(b);
|
||||
if (!ca) ca = "";
|
||||
if (!cb) cb = "";
|
||||
return (el_val_t)strncmp(ca, cb, (size_t)n);
|
||||
}
|
||||
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); }
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); }
|
||||
|
||||
el_val_t __str_alloc(el_val_t n) {
|
||||
if (n <= 0) n = 0;
|
||||
char* buf = el_strbuf((size_t)n + 1);
|
||||
memset(buf, 0, (size_t)n + 1);
|
||||
return el_wrap_str(buf);
|
||||
}
|
||||
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
|
||||
char* buf = (char*)(uintptr_t)s;
|
||||
if (buf) buf[(size_t)i] = (char)c;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s) { return url_encode(s); }
|
||||
el_val_t __url_decode(el_val_t s) { return url_decode(s); }
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key) { return env(key); }
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd) { return exec(cmd); }
|
||||
el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); }
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code) { return exit_program(code); }
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path) { return fs_exists(path); }
|
||||
el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); }
|
||||
el_val_t __fs_read(el_val_t path) { return fs_read(path); }
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); }
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); }
|
||||
el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); }
|
||||
|
||||
/* HTTP server (no curl dependency) */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); }
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); }
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); }
|
||||
|
||||
/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here
|
||||
* so el_runtime.c compiles standalone. When both translation units are linked
|
||||
* the el_seed.c definitions win via their non-static linkage (strong symbols).
|
||||
* These stubs are marked weak so they are silently overridden. */
|
||||
__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); }
|
||||
__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; }
|
||||
__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; }
|
||||
__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; }
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); }
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); }
|
||||
el_val_t __json_array_len(el_val_t json) { return json_array_len(json); }
|
||||
el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); }
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); }
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); }
|
||||
el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); }
|
||||
el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); }
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); }
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key) { return state_del(key); }
|
||||
el_val_t __state_get(el_val_t key) { return state_get(key); }
|
||||
el_val_t __state_keys(void) { return state_keys(); }
|
||||
el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); }
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void) { return uuid_v4(); }
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void) { return args(); }
|
||||
|
||||
/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below
|
||||
* with a matching stub in the #ifndef HAVE_CURL block. */
|
||||
#ifdef HAVE_CURL
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms) {
|
||||
/* timeout_ms is accepted for API compatibility but ignored here;
|
||||
* el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */
|
||||
(void)timeout_ms;
|
||||
struct curl_slist* h = headers_from_map(headers_map);
|
||||
el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h);
|
||||
if (h) curl_slist_free_all(h);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* __http_do_map — same as __http_do but headers_map arg is a JSON-string
|
||||
* rather than an ElMap. Parse it first, then delegate. */
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms) {
|
||||
(void)timeout_ms;
|
||||
/* Build a curl_slist from a JSON object {"Header":"value",...}. */
|
||||
const char* hj = EL_CSTR(headers_json);
|
||||
struct curl_slist* h = NULL;
|
||||
if (hj && *hj && *hj == '{') {
|
||||
/* Walk the JSON pairs with a simple parser reusing json_get_string logic. */
|
||||
/* For correctness we just call the existing json_get iteration path.
|
||||
* We duplicate the key-extraction loop from headers_from_map but driven
|
||||
* by JSON rather than ElMap. Use json_get_raw to iterate is not easy
|
||||
* without knowing keys, so accept the JSON string and build a tmp map. */
|
||||
el_val_t map = json_parse(EL_STR(hj));
|
||||
h = headers_from_map(map);
|
||||
}
|
||||
el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h);
|
||||
if (h) curl_slist_free_all(h);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* __http_do_map_to_file — same as __http_do_map but streams response body
|
||||
* to a local file path rather than returning it as a string. */
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path) {
|
||||
const char* hj = EL_CSTR(headers_json);
|
||||
struct curl_slist* h = NULL;
|
||||
if (hj && *hj && *hj == '{') {
|
||||
el_val_t map = json_parse(EL_STR(hj));
|
||||
h = headers_from_map(map);
|
||||
}
|
||||
el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body),
|
||||
h, EL_CSTR(output_path));
|
||||
if (h) curl_slist_free_all(h);
|
||||
return r;
|
||||
}
|
||||
#endif /* HAVE_CURL */
|
||||
|
||||
#ifndef HAVE_CURL
|
||||
/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── *
|
||||
* These return a JSON error string so El programs get a clear message if they
|
||||
@@ -11189,6 +11712,7 @@ el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body
|
||||
el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); }
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); }
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); }
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); }
|
||||
el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); }
|
||||
el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); }
|
||||
@@ -11202,4 +11726,8 @@ el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (voi
|
||||
el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); }
|
||||
el_val_t llm_models(void) { return el_list_empty(); }
|
||||
void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; }
|
||||
/* __ HTTP stubs (no-curl build) */
|
||||
el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); }
|
||||
el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); }
|
||||
el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); }
|
||||
#endif /* !HAVE_CURL */
|
||||
|
||||
@@ -81,8 +81,8 @@ extern "C" {
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t println(el_val_t s);
|
||||
el_val_t print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
@@ -95,6 +95,7 @@ el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t native_str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
@@ -149,10 +150,11 @@ el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
@@ -173,8 +175,8 @@ void http_set_handler(el_val_t name);
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
@@ -225,6 +227,8 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
el_val_t html_escape(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -526,9 +530,15 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||
* during long compilation loops (e.g. after each function is compiled).
|
||||
* Returns 0 when memory is within bounds. */
|
||||
el_val_t el_mem_check(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
@@ -779,6 +789,95 @@ el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
|
||||
* The El self-hosting compiler emits calls to __-prefixed names. These are
|
||||
* forwarding wrappers around the existing el_runtime functions above. */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s);
|
||||
el_val_t __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f);
|
||||
el_val_t __str_to_float(el_val_t s);
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b);
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t __str_alloc(el_val_t n);
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code);
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
el_val_t __fs_list_raw(el_val_t path);
|
||||
|
||||
/* HTTP server */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
|
||||
|
||||
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
|
||||
el_val_t __http_conn_fd(void);
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path);
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_len(el_val_t json);
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
|
||||
el_val_t __json_parse_map(el_val_t json_str);
|
||||
el_val_t __json_stringify_val(el_val_t val);
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
el_val_t __state_set(el_val_t key, el_val_t val);
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -990,6 +990,8 @@ el_val_t __json_get(el_val_t json, el_val_t key) { return j
|
||||
el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); }
|
||||
el_val_t __json_parse(el_val_t s) { return json_parse(s); }
|
||||
el_val_t __json_stringify(el_val_t v) { return json_stringify(v); }
|
||||
el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); }
|
||||
el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); }
|
||||
el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); }
|
||||
el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); }
|
||||
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); }
|
||||
|
||||
@@ -174,6 +174,8 @@ el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_parse(el_val_t s);
|
||||
el_val_t __json_stringify(el_val_t v);
|
||||
el_val_t __json_parse_map(el_val_t json_str); /* alias for __json_parse */
|
||||
el_val_t __json_stringify_val(el_val_t val); /* alias for __json_stringify */
|
||||
el_val_t __json_array_len(el_val_t json_str);
|
||||
el_val_t __json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
@@ -324,6 +324,10 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
||||
let each_c: String = cg_html_each(child, acc_var)
|
||||
let parts = native_list_append(parts, each_c)
|
||||
}
|
||||
if str_eq(html_kind, "HtmlIf") {
|
||||
let if_c: String = cg_html_if(child, acc_var)
|
||||
let parts = native_list_append(parts, if_c)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
str_join(parts, "")
|
||||
@@ -413,6 +417,17 @@ fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
|
||||
}
|
||||
|
||||
// Generate code for {#if cond} ... {/if} (with optional {#else}).
|
||||
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let cond_expr = node["cond"]
|
||||
let then_children: [Map<String, Any>] = node["then"]
|
||||
let else_children: [Map<String, Any>] = node["else"]
|
||||
let cond_c: String = cg_expr(cond_expr)
|
||||
let then_c: String = cg_html_parts(then_children, acc_var)
|
||||
let else_c: String = cg_html_parts(else_children, acc_var)
|
||||
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
|
||||
}
|
||||
|
||||
// Top-level HTML template codegen — returns a C statement-expression string.
|
||||
fn cg_html_template(expr: Map<String, Any>) -> String {
|
||||
let root = expr["root"]
|
||||
@@ -2487,6 +2502,7 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "http_post_with_headers") { return 3 }
|
||||
if str_eq(name, "http_post_form_auth") { return 3 }
|
||||
if str_eq(name, "http_serve") { return 2 }
|
||||
if str_eq(name, "http_serve_async") { return 2 }
|
||||
if str_eq(name, "http_set_handler") { return 1 }
|
||||
// Seed primitives (__-prefix) — runtime/el_seed.c
|
||||
if str_eq(name, "__str_len") { return 1 }
|
||||
@@ -3715,6 +3731,7 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
cg_fn(stmt)
|
||||
el_release(stmt)
|
||||
el_arena_pop(fn_arena_mark)
|
||||
el_mem_check()
|
||||
}
|
||||
} else {
|
||||
if is_top_level_decl(stmt) {
|
||||
|
||||
@@ -287,6 +287,9 @@ fn type_node_to_el(t: Map<String, Any>) -> String {
|
||||
|
||||
// emit_header — write a .elh file from parsed statements.
|
||||
// Scans for FnDef nodes and emits 'extern fn' declarations.
|
||||
// NOTE: This function requires the full AST. Prefer emit_header_from_sigs
|
||||
// for the --emit-header path — it works from a token-level scan without
|
||||
// building expression ASTs, avoiding OOM on large files.
|
||||
fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
@@ -325,6 +328,32 @@ fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let ok: Bool = fs_write(hdr_path, content)
|
||||
}
|
||||
|
||||
// emit_header_from_sigs — write a .elh file from pre-scanned El signatures.
|
||||
// Uses the output of scan_fn_sigs_el() — no full AST required.
|
||||
// Peak memory is O(tokens) rather than O(whole-program AST), which prevents
|
||||
// OOM on large files with HTML template bodies or deep BinOp chains.
|
||||
fn emit_header_from_sigs(sigs: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let n: Int = native_list_len(sigs)
|
||||
let i: Int = 0
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n")
|
||||
while i < n {
|
||||
let sig = native_list_get(sigs, i)
|
||||
let kind: String = sig["kind"]
|
||||
if str_eq(kind, "fn") {
|
||||
let name: String = sig["name"]
|
||||
let params_el: String = sig["params_el"]
|
||||
let ret_el: String = sig["ret_el"]
|
||||
if str_eq(ret_el, "") { let ret_el = "Any" }
|
||||
let line: String = "extern fn " + name + "(" + params_el + ") -> " + ret_el
|
||||
let parts = native_list_append(parts, line + "\n")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let content: String = str_join(parts, "")
|
||||
let ok: Bool = fs_write(hdr_path, content)
|
||||
}
|
||||
|
||||
// ── Import resolution ────────────────────────────────────────────────────────
|
||||
//
|
||||
// elc supports two forms of import:
|
||||
@@ -536,16 +565,20 @@ fn main() -> Void {
|
||||
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
|
||||
// When --emit-header is requested, parse the source file directly
|
||||
// (without inlining imports) and write out a .elh file alongside the .c.
|
||||
// When --emit-header is requested, lex the source file and do a
|
||||
// token-level signature scan (no full AST) to write a .elh file.
|
||||
// This avoids OOM on large files with HTML template bodies or deep
|
||||
// BinOp chains (e.g. checkout.el) — parse() builds O(whole-program AST)
|
||||
// while scan_fn_sigs_el keeps peak memory at O(tokens).
|
||||
if do_emit_header {
|
||||
el_mem_check()
|
||||
let raw_source: String = fs_read(src_path)
|
||||
let hdr_tokens: [Any] = lex(raw_source)
|
||||
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
|
||||
let hdr_sigs: [Map<String, Any>] = scan_fn_sigs_el(hdr_tokens)
|
||||
el_release(hdr_tokens)
|
||||
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
|
||||
emit_header(hdr_stmts, hdr_path)
|
||||
el_release(hdr_stmts)
|
||||
emit_header_from_sigs(hdr_sigs, hdr_path)
|
||||
el_release(hdr_sigs)
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
|
||||
+497
-94
@@ -293,6 +293,48 @@ fn is_void_element(name: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Collect all tokens as raw text until </tag_name> is encountered.
|
||||
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
|
||||
// Returns { "text": "...", "pos": p_after_closing_tag }
|
||||
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> Map<String, Any> {
|
||||
let parts: [String] = native_list_empty()
|
||||
let p = pos
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
if str_eq(k, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Lt") {
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Slash") {
|
||||
// Check if this is </tag_name>
|
||||
let close_name = tok_value(tokens, p + 2)
|
||||
if str_eq(close_name, tag_name) {
|
||||
// consume </tag_name>
|
||||
let p = p + 3
|
||||
let p = expect(tokens, p, "Gt")
|
||||
let running = false
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": p }
|
||||
}
|
||||
|
||||
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
|
||||
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
|
||||
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
@@ -320,7 +362,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})*
|
||||
@@ -435,77 +477,125 @@ fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<Strin
|
||||
}
|
||||
} else {
|
||||
if str_eq(k, "LBrace") {
|
||||
// Interpolation: {expr} or {#each ...} or {/each}
|
||||
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
|
||||
// Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
|
||||
// LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
|
||||
// {#else} lexes as LBrace Else RBrace.
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Hash") {
|
||||
// {#each list as item}
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "each") {
|
||||
let p = p + 3
|
||||
// parse list expr up to "as" keyword
|
||||
if str_eq(k2, "Slash") {
|
||||
// {/each} or {/if} — end of block, stop
|
||||
// skip { /
|
||||
let p = p + 2
|
||||
// skip the close-tag name token (each, if, etc.)
|
||||
let p = p + 1
|
||||
// skip }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k2, "If") {
|
||||
// {#if condition} ... {/if}
|
||||
// Skip { if (2 tokens; '#' was silently skipped by lexer)
|
||||
let p = p + 2
|
||||
// Parse condition expression (no block expr)
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r_list = parse_expr(tokens, p)
|
||||
let r_cond = 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
|
||||
let cond_expr = r_cond["node"]
|
||||
let p = r_cond["pos"]
|
||||
el_release(r_cond)
|
||||
// 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 {
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
if str_eq(k2, "Slash") {
|
||||
// {/each} — end of each block, stop
|
||||
// skip {/each}
|
||||
let p = p + 2
|
||||
// skip "each"
|
||||
let p = p + 1
|
||||
// skip }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let running = false
|
||||
} else {
|
||||
// regular {expr}
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
let interp_val = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// Check if the expr is a call to raw()
|
||||
let is_raw_call = false
|
||||
let interp_kind: String = interp_val["expr"]
|
||||
if str_eq(interp_kind, "Call") {
|
||||
let fn_node = interp_val["func"]
|
||||
let fn_kind: String = fn_node["expr"]
|
||||
if str_eq(fn_kind, "Ident") {
|
||||
let fn_name_v: String = fn_node["name"]
|
||||
if str_eq(fn_name_v, "raw") {
|
||||
let is_raw_call = true
|
||||
}
|
||||
// parse then-children until {#else} or {/if}
|
||||
let r_then = parse_html_children(tokens, p, "__if_then__")
|
||||
let then_children = r_then["children"]
|
||||
let p = r_then["pos"]
|
||||
el_release(r_then)
|
||||
// check for {#else} — lexed as LBrace Else RBrace
|
||||
let else_children: [Map<String, Any>] = native_list_empty()
|
||||
let ck = tok_kind(tokens, p)
|
||||
if str_eq(ck, "LBrace") {
|
||||
let ck2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(ck2, "Else") {
|
||||
// consume { else }
|
||||
let p = p + 2
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse else-children until {/if}
|
||||
let r_else = parse_html_children(tokens, p, "__if_else__")
|
||||
let else_children = r_else["children"]
|
||||
let p = r_else["pos"]
|
||||
el_release(r_else)
|
||||
}
|
||||
}
|
||||
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 })
|
||||
let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
|
||||
let children = native_list_append(children, if_node)
|
||||
} else {
|
||||
if str_eq(k2, "Else") {
|
||||
// {#else} sentinel — lexed as LBrace Else RBrace
|
||||
// Do NOT consume — leave position for caller ({#if} handler checks for it)
|
||||
let running = false
|
||||
} else {
|
||||
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
||||
// Check for {#each list as item} — lexed as LBrace Ident:"each" ...
|
||||
let k2_v = tok_value(tokens, p + 1)
|
||||
if str_eq(k2_v, "each") {
|
||||
let p = p + 2
|
||||
// parse list expr up to "as" keyword
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r_list = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r_list["node"]
|
||||
let p = r_list["pos"]
|
||||
// r_list result map fully consumed — release to free peak heap.
|
||||
el_release(r_list)
|
||||
// expect "as"
|
||||
let p = expect(tokens, p, "As")
|
||||
// item variable name
|
||||
let item_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// consume closing }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse body until {/each}
|
||||
let r_body = parse_html_each_body(tokens, p)
|
||||
let body_children = r_body["children"]
|
||||
let p = r_body["pos"]
|
||||
// r_body result map fully consumed — release to free peak heap.
|
||||
el_release(r_body)
|
||||
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
|
||||
let children = native_list_append(children, each_node)
|
||||
} else {
|
||||
// regular {expr} — disable map-literal parsing so {fn(a,b)}
|
||||
// does not trigger the LBrace→map 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,6 +655,27 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
if is_void_element(tag_name) {
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
||||
}
|
||||
// raw-text mode for style/script — collect content as plain text without parsing CSS/JS as El
|
||||
if str_eq(tag_name, "style") {
|
||||
let r_raw = parse_raw_text_content(tokens, p, "style")
|
||||
let raw_text: String = r_raw["text"]
|
||||
let p = r_raw["pos"]
|
||||
el_release(r_raw)
|
||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
||||
let raw_children = native_list_append(raw_children, raw_child)
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
||||
}
|
||||
if str_eq(tag_name, "script") {
|
||||
let r_raw = parse_raw_text_content(tokens, p, "script")
|
||||
let raw_text: String = r_raw["text"]
|
||||
let p = r_raw["pos"]
|
||||
el_release(r_raw)
|
||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
||||
let raw_children = native_list_append(raw_children, raw_child)
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
||||
}
|
||||
// parse children
|
||||
let r_children = parse_html_children(tokens, p, tag_name)
|
||||
let children = r_children["children"]
|
||||
@@ -718,44 +829,123 @@ fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
// as the start of the block they're expecting.
|
||||
return make_result({ "expr": "Nil" }, pos)
|
||||
}
|
||||
let p = pos + 1
|
||||
let pairs: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "RBrace" {
|
||||
let running = false
|
||||
} else {
|
||||
if k2 == "Eof" {
|
||||
// Distinguish map literal from interpolation chain.
|
||||
// A map literal requires { key: value } — the second token inside { must be Colon.
|
||||
// An empty {} is a map literal. Everything else is an interpolation chain.
|
||||
let first_k: String = tok_kind(tokens, pos + 1)
|
||||
let second_k: String = tok_kind(tokens, pos + 2)
|
||||
if str_eq(first_k, "RBrace") {
|
||||
// Empty map literal {}
|
||||
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
|
||||
}
|
||||
if str_eq(second_k, "Colon") {
|
||||
// MAP LITERAL: { key: value, ... }
|
||||
let p = pos + 1
|
||||
let pairs: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "RBrace" {
|
||||
let running = false
|
||||
} else {
|
||||
// key: Str token
|
||||
let key = tok_value(tokens, p)
|
||||
let new_p: Int = p + 1
|
||||
let new_p = expect(tokens, new_p, "Colon")
|
||||
let r = parse_expr(tokens, new_p)
|
||||
let val_node = r["node"]
|
||||
let new_p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let pair = { "key": key, "value": val_node }
|
||||
let pairs = native_list_append(pairs, pair)
|
||||
let k3 = tok_kind(tokens, new_p)
|
||||
if k3 == "Comma" {
|
||||
let new_p = new_p + 1
|
||||
}
|
||||
// Non-progress guard: malformed map content can leave
|
||||
// parse_expr returning the same pos. Force advance.
|
||||
if new_p <= p {
|
||||
let p = p + 1
|
||||
if k2 == "Eof" {
|
||||
let running = false
|
||||
} else {
|
||||
let p = new_p
|
||||
// key: Str or Ident token
|
||||
let key = tok_value(tokens, p)
|
||||
let new_p: Int = p + 1
|
||||
let new_p = expect(tokens, new_p, "Colon")
|
||||
let r = parse_expr(tokens, new_p)
|
||||
let val_node = r["node"]
|
||||
let new_p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let pair = { "key": key, "value": val_node }
|
||||
let pairs = native_list_append(pairs, pair)
|
||||
let k3 = tok_kind(tokens, new_p)
|
||||
if k3 == "Comma" {
|
||||
let new_p = new_p + 1
|
||||
}
|
||||
// Non-progress guard: malformed map content can leave
|
||||
// parse_expr returning the same pos. Force advance.
|
||||
if new_p <= p {
|
||||
let p = p + 1
|
||||
} else {
|
||||
let p = new_p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
||||
}
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
||||
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
|
||||
// Build a BinOp(Plus, ...) concatenation chain.
|
||||
let p = pos
|
||||
let chain_node: Map<String, Any> = { "expr": "Nil" }
|
||||
let chain_started = false
|
||||
let chain_running = true
|
||||
while chain_running {
|
||||
let ck: String = tok_kind(tokens, p)
|
||||
if str_eq(ck, "LBrace") {
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
if str_eq(ck, "Lt") {
|
||||
let ck2: String = tok_kind(tokens, p + 1)
|
||||
if str_eq(ck2, "Not") {
|
||||
let r = parse_html_template(tokens, p)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
if str_eq(ck2, "Ident") {
|
||||
let tag_candidate: String = tok_value(tokens, p + 1)
|
||||
if is_html_tag_name(tag_candidate) {
|
||||
let r = parse_html_template(tokens, p)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return make_result(chain_node, p)
|
||||
}
|
||||
|
||||
// if expression
|
||||
@@ -1875,6 +2065,219 @@ fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
|
||||
p
|
||||
}
|
||||
|
||||
// scan_type_el — read a type annotation starting at pos and return its El
|
||||
// source representation as a string, plus the new position.
|
||||
// Returns { "el": String, "pos": Int }.
|
||||
// Handles: Ident, [Type], Map<K,V>, Type?, Type<T,...> (same shapes as skip_type).
|
||||
fn scan_type_el(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
let k: String = tok_kind(tokens, pos)
|
||||
// Array type: [Type]
|
||||
if str_eq(k, "LBracket") {
|
||||
let p: Int = pos + 1
|
||||
let inner = scan_type_el(tokens, p)
|
||||
let inner_str: String = inner["el"]
|
||||
let p = inner["pos"]
|
||||
el_release(inner)
|
||||
let p = expect(tokens, p, "RBracket")
|
||||
return { "el": "[" + inner_str + "]", "pos": p }
|
||||
}
|
||||
// Named type (possibly generic or optional)
|
||||
if str_eq(k, "Ident") {
|
||||
let name: String = tok_value(tokens, pos)
|
||||
let p: Int = pos + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Lt") {
|
||||
// Generic params: collect until matching >
|
||||
let p = p + 1
|
||||
let depth: Int = 1
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, name + "<")
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let kk: String = tok_kind(tokens, p)
|
||||
if str_eq(kk, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(kk, "Lt") {
|
||||
let depth = depth + 1
|
||||
let parts = native_list_append(parts, "<")
|
||||
let p = p + 1
|
||||
} else {
|
||||
if str_eq(kk, "Gt") {
|
||||
let depth = depth - 1
|
||||
let p = p + 1
|
||||
if depth <= 0 {
|
||||
let parts = native_list_append(parts, ">")
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ">")
|
||||
}
|
||||
} else {
|
||||
if str_eq(kk, "Comma") {
|
||||
let parts = native_list_append(parts, ", ")
|
||||
let p = p + 1
|
||||
} else {
|
||||
let parts = native_list_append(parts, tok_value(tokens, p))
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let k3: String = tok_kind(tokens, p)
|
||||
if str_eq(k3, "QuestionMark") { let p = p + 1 }
|
||||
let result: String = str_join(parts, "")
|
||||
el_release(parts)
|
||||
return { "el": result, "pos": p }
|
||||
}
|
||||
// Optional marker
|
||||
if str_eq(k2, "QuestionMark") {
|
||||
return { "el": name + "?", "pos": p + 1 }
|
||||
}
|
||||
return { "el": name, "pos": p }
|
||||
}
|
||||
// Fallback: unknown token, treat as Any
|
||||
{ "el": "Any", "pos": pos + 1 }
|
||||
}
|
||||
|
||||
// scan_params_el — scan a parameter list `(name: Type, ...)` starting at
|
||||
// position `pos` (which should point at LParen) and return the El parameter
|
||||
// declaration string (e.g. "a: String, b: Int") along with the new position.
|
||||
// Returns { "el": String, "pos": Int }.
|
||||
// Used by scan_fn_sigs_el for --emit-header without building full AST.
|
||||
fn scan_params_el(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
let p: Int = expect(tokens, pos, "LParen")
|
||||
let parts: [String] = native_list_empty()
|
||||
let going: Bool = true
|
||||
while going {
|
||||
let kk: String = tok_kind(tokens, p)
|
||||
if str_eq(kk, "RParen") {
|
||||
let going = false
|
||||
} else {
|
||||
if str_eq(kk, "Eof") {
|
||||
let going = false
|
||||
} else {
|
||||
let pname: String = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "Colon")
|
||||
let tr = scan_type_el(tokens, p)
|
||||
let ptype: String = tr["el"]
|
||||
let p = tr["pos"]
|
||||
el_release(tr)
|
||||
let parts = native_list_append(parts, pname + ": " + ptype)
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Comma") {
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = expect(tokens, p, "RParen")
|
||||
let el_str: String = str_join(parts, ", ")
|
||||
el_release(parts)
|
||||
{ "el": el_str, "pos": p }
|
||||
}
|
||||
|
||||
// scan_fn_sigs_el — lightweight token-level pre-scan for --emit-header.
|
||||
//
|
||||
// Like scan_fn_sigs but captures El-style type strings instead of C types.
|
||||
// Only records fn/extern_fn entries (header generation ignores lets/blocks).
|
||||
//
|
||||
// Descriptor shape:
|
||||
// { "kind": "fn"|"extern_fn", "name": String,
|
||||
// "params_el": String, <- El param list, e.g. "a: String, b: Int"
|
||||
// "ret_el": String } <- El return type, e.g. "String" or "Void"
|
||||
//
|
||||
// Peak memory: O(tokens) with no expression AST allocation.
|
||||
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(tokens) / 2
|
||||
let sigs: [Map<String, Any>] = native_list_empty()
|
||||
let pos: Int = 0
|
||||
let going: Bool = true
|
||||
while going {
|
||||
if pos >= total {
|
||||
let going = false
|
||||
} else {
|
||||
let k: String = tok_kind(tokens, pos)
|
||||
if str_eq(k, "Eof") {
|
||||
let going = false
|
||||
} else {
|
||||
// --- fn definition ---
|
||||
if str_eq(k, "Fn") {
|
||||
let p: Int = pos + 1
|
||||
let name: String = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let pr = scan_params_el(tokens, p)
|
||||
let params_el: String = pr["el"]
|
||||
let p = pr["pos"]
|
||||
el_release(pr)
|
||||
// read return type
|
||||
let ret_el: String = "Any"
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Arrow") {
|
||||
let p = p + 1
|
||||
let tr = scan_type_el(tokens, p)
|
||||
let ret_el = tr["el"]
|
||||
let p = tr["pos"]
|
||||
el_release(tr)
|
||||
}
|
||||
// skip body
|
||||
let k3: String = tok_kind(tokens, p)
|
||||
if str_eq(k3, "LBrace") {
|
||||
let p = skip_to_rbrace(tokens, p)
|
||||
}
|
||||
if !str_eq(name, "main") {
|
||||
let sigs = native_list_append(sigs, {
|
||||
"kind": "fn",
|
||||
"name": name,
|
||||
"params_el": params_el,
|
||||
"ret_el": ret_el
|
||||
})
|
||||
}
|
||||
let pos = p
|
||||
} else {
|
||||
// --- extern fn ---
|
||||
if str_eq(k, "Extern") {
|
||||
let p: Int = pos + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
if str_eq(k2, "Fn") {
|
||||
let p = p + 1
|
||||
let name: String = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let pr = scan_params_el(tokens, p)
|
||||
let params_el: String = pr["el"]
|
||||
let p = pr["pos"]
|
||||
el_release(pr)
|
||||
let ret_el: String = "Any"
|
||||
let k3: String = tok_kind(tokens, p)
|
||||
if str_eq(k3, "Arrow") {
|
||||
let p = p + 1
|
||||
let tr = scan_type_el(tokens, p)
|
||||
let ret_el = tr["el"]
|
||||
let p = tr["pos"]
|
||||
el_release(tr)
|
||||
}
|
||||
let sigs = native_list_append(sigs, {
|
||||
"kind": "extern_fn",
|
||||
"name": name,
|
||||
"params_el": params_el,
|
||||
"ret_el": ret_el
|
||||
})
|
||||
let pos = p
|
||||
} else {
|
||||
let pos = pos + 1
|
||||
}
|
||||
} else {
|
||||
// Let, Cgi, Service, Import, Type, Enum, From — skip to boundary.
|
||||
let p: Int = pos + 1
|
||||
let p = skip_expr_to_stmt_boundary(tokens, p)
|
||||
let pos = p
|
||||
}}}
|
||||
}
|
||||
}
|
||||
sigs
|
||||
}
|
||||
|
||||
// scan_params_c — scan a parameter list `(name: Type, ...)` starting at
|
||||
// position `pos` (which should point at LParen) and return the C parameter
|
||||
// declaration string along with the new position.
|
||||
|
||||
+70
-4
@@ -77,6 +77,33 @@ fn parse_manifest_entry(src: String) -> String {
|
||||
return ""
|
||||
}
|
||||
|
||||
// parse_manifest_c_sources - collect all `c_source "path"` lines from the
|
||||
// build block. Returns a flat list of path strings.
|
||||
fn parse_manifest_c_sources(src: String) -> [String] {
|
||||
let result: [String] = native_list_empty()
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "c_source ") {
|
||||
let after: String = str_slice(t, 9, str_len(t))
|
||||
let trimmed: String = str_trim(after)
|
||||
if str_starts_with(trimmed, "\"") {
|
||||
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
|
||||
let q: Int = str_index_of(inner, "\"")
|
||||
if q >= 0 {
|
||||
let path: String = str_slice(inner, 0, q)
|
||||
let result = native_list_append(result, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn parse_manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
@@ -256,6 +283,12 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
|
||||
}
|
||||
exec_command("rm -f " + err_tmp)
|
||||
|
||||
// Strip capability-violation guard #error lines injected by elc when a
|
||||
// module is compiled in isolation (utility context). These are safe to
|
||||
// remove here: the entire binary is linked under the CGI entry-point
|
||||
// declaration in soul.el, so the module-level guard is redundant.
|
||||
exec_command("sed -i.bak '/^#error \"capability violation/d' " + c_out + " && rm -f " + c_out + ".bak")
|
||||
|
||||
// Move the generated .elh (written next to the source by elc) into
|
||||
// out_dir so that #include "module.elh" lines in the generated .c
|
||||
// files resolve correctly when cc is invoked with -I <out_dir>.
|
||||
@@ -271,7 +304,25 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let parts: [String] = native_list_empty()
|
||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||
// (for module.elh cross-module forward declarations).
|
||||
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
// Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored
|
||||
// if unsupported but gcc rejects it with an error.
|
||||
let bracket_flag: String = "$(cc --version 2>&1 | grep -q clang && printf -- '-fbracket-depth=1024' || true)"
|
||||
// On macOS, OpenSSL is not on the default linker path. Detect homebrew
|
||||
// prefix and add it if present (no-op on Linux where libssl is in /usr/lib).
|
||||
let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)"
|
||||
let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
|
||||
// liboqs (post-quantum crypto) — present on macOS dev machines, not on CI
|
||||
// Linux containers. Link -loqs only when the library is available.
|
||||
let oqs_lib_flag: String = "$(brew --prefix liboqs 2>/dev/null | xargs -I{} printf -- '-L{}/lib -loqs' 2>/dev/null || true)"
|
||||
let oqs_inc_flag: String = "$(brew --prefix liboqs 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 -DHAVE_CURL " + bracket_flag + " " + ossl_inc_flag + " " + oqs_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let f: String = native_list_get(c_files, i)
|
||||
@@ -279,7 +330,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let i = i + 1
|
||||
}
|
||||
let parts = native_list_append(parts, runtime_path)
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread -lm")
|
||||
let parts = native_list_append(parts, ossl_lib_flag + " " + oqs_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
|
||||
let parts = native_list_append(parts, "-o " + out_bin)
|
||||
let cmd: String = str_join(parts, " ")
|
||||
println(" link " + out_bin)
|
||||
@@ -312,6 +363,7 @@ fn main() -> Void {
|
||||
|
||||
let pkg_name: String = parse_manifest_name(manifest_src)
|
||||
let entry: String = parse_manifest_entry(manifest_src)
|
||||
let extra_c: [String] = parse_manifest_c_sources(manifest_src)
|
||||
if str_eq(entry, "") {
|
||||
println("elb: manifest.el has no 'entry' declaration")
|
||||
exit(1)
|
||||
@@ -390,9 +442,23 @@ fn main() -> Void {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Link
|
||||
// Link — use only the entry-point .c file (which elc compiles as a
|
||||
// monolithic unit, inlining all imports). Linking all module .c files
|
||||
// together causes duplicate-symbol errors because each module's .c also
|
||||
// inlines its full import tree.
|
||||
let entry_c: String = out_dir + "/" + basename_noext(entry) + ".c"
|
||||
let link_files: [String] = native_list_empty()
|
||||
let link_files = native_list_append(link_files, entry_c)
|
||||
// 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 link_files = native_list_append(link_files, ec)
|
||||
let ei = ei + 1
|
||||
}
|
||||
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(link_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
if !linked {
|
||||
println("elb: link failed")
|
||||
exit(1)
|
||||
|
||||
+10062
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
CompileFlags:
|
||||
Add:
|
||||
- -I/opt/homebrew/Cellar/liboqs/0.15.0/include
|
||||
- -I/opt/homebrew/opt/openssl@3/include
|
||||
- -std=c11
|
||||
File diff suppressed because it is too large
Load Diff
@@ -143,6 +143,7 @@ el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_m
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_serve_async(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
@@ -429,10 +430,22 @@ el_val_t str_to_float(el_val_t s);
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_exp(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── Float arithmetic builtins (correct IEEE 754 via bit-cast round-trip) ─── */
|
||||
el_val_t float_add(el_val_t a, el_val_t b);
|
||||
el_val_t float_sub(el_val_t a, el_val_t b);
|
||||
el_val_t float_mul(el_val_t a, el_val_t b);
|
||||
el_val_t float_div(el_val_t a, el_val_t b);
|
||||
el_val_t float_gt(el_val_t a, el_val_t b);
|
||||
el_val_t float_lt(el_val_t a, el_val_t b);
|
||||
el_val_t float_eq(el_val_t a, el_val_t b);
|
||||
el_val_t float_gte(el_val_t a, el_val_t b);
|
||||
el_val_t float_lte(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
@@ -492,6 +505,7 @@ el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_joi
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_set(el_val_t list, el_val_t index, el_val_t value);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
@@ -589,6 +603,10 @@ el_val_t engram_edge_count(void);
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
el_val_t engram_load_dir(el_val_t data_dir);
|
||||
el_val_t engram_reindex_json(void);
|
||||
el_val_t engram_write_binary_el(el_val_t path);
|
||||
el_val_t engram_load_binary_el(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
@@ -600,6 +618,10 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void); /* avg wm weight of promoted nodes; float bits */
|
||||
el_val_t engram_wm_top_json(el_val_t n); /* top-N WM nodes by weight as compact JSON */
|
||||
el_val_t engram_apply_decay_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
|
||||
@@ -44,7 +44,7 @@ run_runtime_case() {
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/cal_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
|
||||
@@ -26,7 +26,7 @@ echo "==> Compiling runner.el via ${ELC}"
|
||||
|
||||
echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
|
||||
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${OUT_BIN}"
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "${OUT_BIN}"
|
||||
|
||||
echo "==> Running"
|
||||
"${OUT_BIN}"
|
||||
|
||||
@@ -42,7 +42,7 @@ run_runtime_case() {
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/text_test.cc.err; then
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/text_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/text_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
|
||||
@@ -55,7 +55,7 @@ run_runtime_case() {
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/time_test.cc.err; then
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "${out_bin}" 2>/tmp/time_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/time_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
@@ -116,7 +116,7 @@ run_typeerror_case() {
|
||||
fi
|
||||
|
||||
if cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then
|
||||
echo "FAIL ${name} — cc unexpectedly succeeded; type rule did not fire"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
|
||||
@@ -10,15 +10,6 @@
|
||||
// export PATH="$HOME/.el/bin:$PATH"
|
||||
// export EL_HOME="$HOME/.el"
|
||||
|
||||
// ── Imports ───────────────────────────────────────────────────────────────────
|
||||
|
||||
import "../../runtime/string.el"
|
||||
import "../../runtime/env.el"
|
||||
import "../../runtime/fs.el"
|
||||
import "../../runtime/exec.el"
|
||||
import "../../runtime/json.el"
|
||||
import "../../runtime/http.el"
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn gitea_releases_url() -> String {
|
||||
|
||||
Generated
-897
@@ -1,897 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,18 +0,0 @@
|
||||
[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"
|
||||
@@ -1,350 +0,0 @@
|
||||
//! 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.");
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[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]
|
||||
@@ -1,490 +0,0 @@
|
||||
//! 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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! `@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")
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
//! 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", ¶ms);
|
||||
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");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user