Compare commits
21 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 |
@@ -39,9 +39,9 @@ jobs:
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -54,9 +54,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
@@ -214,18 +214,9 @@ jobs:
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
@@ -251,7 +242,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
@@ -259,7 +250,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
@@ -267,7 +258,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.js
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
@@ -277,12 +268,6 @@ jobs:
|
||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
@@ -306,9 +291,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ jobs:
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
@@ -176,9 +176,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
@@ -212,21 +212,12 @@ jobs:
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
@@ -244,7 +235,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
@@ -252,7 +243,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
@@ -262,12 +253,6 @@ jobs:
|
||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
@@ -290,9 +275,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -47,9 +47,9 @@ jobs:
|
||||
mkdir -p dist/platform
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -62,9 +62,9 @@ jobs:
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/runtime"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
@@ -216,10 +216,8 @@ jobs:
|
||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||
cp lang/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.c dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.h dist/sdk/runtime/
|
||||
cp lang/el-compiler/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/el-compiler/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/*.el dist/sdk/runtime/
|
||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||
@@ -276,10 +274,8 @@ jobs:
|
||||
|
||||
# Per-file assets (downstream CI needs these individually)
|
||||
upload_asset lang/dist/platform/elc elc
|
||||
upload_asset lang/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/runtime/el_runtime.h el_runtime.h
|
||||
upload_asset lang/runtime/engram_store.c engram_store.c
|
||||
upload_asset lang/runtime/engram_store.h engram_store.h
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/el-compiler/runtime/el_runtime.h el_runtime.h
|
||||
|
||||
# SDK bundle and installer binary
|
||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||
@@ -292,21 +288,12 @@ jobs:
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
|
||||
@@ -332,7 +319,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.c
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
@@ -340,7 +327,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.h
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
@@ -348,7 +335,7 @@ jobs:
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.js
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
@@ -358,12 +345,6 @@ jobs:
|
||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
@@ -386,9 +367,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
LANG_DIR="$ROOT/lang"
|
||||
RUNTIME="$LANG_DIR/runtime"
|
||||
RUNTIME="$LANG_DIR/el-compiler/runtime"
|
||||
ELC="$LANG_DIR/dist/platform/elc"
|
||||
|
||||
# If elc isn't built yet, skip with a warning rather than blocking
|
||||
if [ ! -x "$ELC" ]; then
|
||||
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
|
||||
echo " Build it first: cd lang && gcc -O2 -I runtime dist/elc-bootstrap.c runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I runtime /tmp/elc.c runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
echo " Build it first: cd lang && gcc -O2 -I el-compiler/runtime dist/elc-bootstrap.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I el-compiler/runtime /tmp/elc.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# El
|
||||
|
||||
**A self-hosting, statically-typed language that compiles to C — built around a graph-native runtime instead of a database driver.**
|
||||
|
||||
El is the execution substrate for the Neuron agent runtime, the DHARMA network, and the Engram knowledge graph. This repository is the monorepo for the whole stack: the language itself, the graph memory engine it's built to talk to natively, and the tools (package manager, IDE, UI framework, diagramming) built on top of it.
|
||||
|
||||
---
|
||||
|
||||
## Why El exists
|
||||
|
||||
Every other language treats persistent, associative state as something you reach for through a driver — a SQL client, an ORM, a Redis library bolted on from outside. El inverts that: graph operations (`engram_*`) are runtime primitives, on the same footing as string or list operations. There is no separate database driver because the database is not separate.
|
||||
|
||||
El has four defining properties:
|
||||
|
||||
1. **Self-hosting compiler.** The compiler (`lexer.el`, `parser.el`, `codegen.el`, `compiler.el`) is written in El. It compiles El source to C, which `cc` compiles against a fixed runtime into a native binary. A Rust genesis compiler bootstrapped the first iteration; the self-hosted binary at `lang/dist/platform/elc` has been the canonical compiler ever since — every binary in `dist/platform/` was produced by an earlier version of itself compiling `el-compiler/src/`. The chain is auditable: source is the ground truth, not the binary. See [lang/BOOTSTRAP.md](lang/BOOTSTRAP.md) for the full recovery path if that binary is ever lost.
|
||||
2. **C compilation target.** Every compiled program is plain C11. Every El value is `el_val_t` (`int64_t`); strings are heap pointers cast through it. Functions become C functions; top-level statements become `main()`.
|
||||
3. **Graph-native runtime.** The runtime provides first-class graph operations over an in-process Engram store — no separate DB driver, no ORM.
|
||||
4. **DHARMA-aware identity.** A `cgi` block declares a program's DHARMA identity at compile time. The runtime resolves identity before user code runs, so `dharma_*` calls have a stable principal and channel surface throughout.
|
||||
|
||||
---
|
||||
|
||||
## Architecture map
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ lang │ El compiler + C runtime
|
||||
│ (El itself) │ everything below is written in it,
|
||||
└──────┬──────┘ or compiles down through it
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
┌──────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ engram │ │ epm │ │ ide │
|
||||
│ graph/mem │ │ package │ │ editor + │
|
||||
│ substrate │ │ manager │ │ LSP │
|
||||
└──────┬─────┘ └───────────┘ └───────────┘
|
||||
│
|
||||
┌───────┼────────────────┬─────────────────────┐
|
||||
│ │ │ │
|
||||
┌─────▼───┐ ┌─▼──────────┐ ┌──▼──────────┐ ┌─────▼──────┐
|
||||
│ elp │ │ ql │ │ ui │ │ arbor │
|
||||
│ NLG / │ │engram-el. │ |spreading- │ |arbor │
|
||||
│ 31 langs│ │studio+tests│ |activation UI│ |diagram lang│
|
||||
└─────────┘ └────────────┘ └─────────────┘ └────────────┘
|
||||
```
|
||||
|
||||
`lang` is the foundation — the compiler and C runtime everything else builds on. `engram` is the graph-native memory/state engine that gives El its identity (property 3 above). Everything else is either a tool for working with El (`epm`, `ide`) or a system built on top of Engram's graph model (`elp`, `ql`, `ui`, `arbor`).
|
||||
|
||||
---
|
||||
|
||||
## Repository layout
|
||||
|
||||
### [lang/](lang/) — the El language
|
||||
|
||||
The compiler and runtime. Self-hosting: `elc-cli.el` → `compiler.el` → `lexer.el` / `parser.el` / `codegen.el` / `codegen-js.el`, textually inlined and compiled in one pass. Compiles to C11 and links against `el-compiler/runtime/el_seed.c`, a hand-maintained OS-boundary layer (libcurl HTTP, pthreads, filesystem, arena allocation) — everything else in the runtime is native El (`runtime/*.el`).
|
||||
|
||||
Two layers to know: **El programs** (`.el` files — where nearly all work belongs) and **the C seed** (`el_seed.c` — edit only for genuine OS-level access; never re-implement what El can already express).
|
||||
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented. In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), the real `engram_*` and `dharma_*` runtimes (currently stubs), and libcurl-backed `http_get`/`http_post`/`http_serve`. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
|
||||
|
||||
Key docs: [AGENTS.md](lang/AGENTS.md) (agent-facing orientation), [BOOTSTRAP.md](lang/BOOTSTRAP.md) (compiler recovery from scratch), [spec/language.md](lang/spec/language.md), [spec/codegen-js.md](lang/spec/codegen-js.md).
|
||||
|
||||
### [engram/](engram/) — graph intelligence substrate
|
||||
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. Rust core (`engram-core`, `engram-ffi`) exposed to El and other languages (Kotlin, TypeScript/WASM, Go bindings).
|
||||
|
||||
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop (`strength = parent_strength × edge_weight × target_salience × cosine_sim`), gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works.
|
||||
|
||||
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — `importance × recency-decay × log(activation_count)`. Forgetting is adaptive pruning, not a bug: unreinforced memories stop competing for attention without being deleted.
|
||||
|
||||
Backed by `sled` (embedded, local-first, no daemon) with flat cosine scan for vector search — deliberately simple until scale demands an HNSW layer. Full API and design rationale in [engram/README.md](engram/README.md).
|
||||
|
||||
### [elp/](elp/) — Engram Language Protocol
|
||||
|
||||
Bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*` → `grammar` → `realizer` → `semantics` → `elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
|
||||
|
||||
### [epm/](epm/) — El Package Manager
|
||||
|
||||
Manages **vessels** (El's package unit): publish, install, resolve dependencies. Vessels are stored in Engram as graph nodes, not files in a registry index — `epm` reads the local `manifest.el`, talks to Engram over HTTP, and writes resolved vessels to `.epm/vessels/`. Source: `registry.el`, `install.el`, `update.el`, `manifest.el`.
|
||||
|
||||
### [ide/](ide/) — El IDE
|
||||
|
||||
Three vessels: **el-ide-server** (HTTP backend — file ops, build/run, LSP bridge, plugin host, settings), **el-lsp** (the language server — completion, hover, diagnostics, outline, format, type graph), and **el-plugin-host** (first-party plugin lifecycle: install/remove/enable/disable). `ide/projects/` and `ide/examples/` hold sample projects, including the canonical `hello-friends` first-program walkthrough.
|
||||
|
||||
### [ql/](ql/) — engram-el
|
||||
|
||||
The El-native integration layer for a *live* Engram server — not a library (no importable modules, no build artifact), a set of standalone `.el` programs run directly via `el run-file`. Three components: **Studio** (`studio/studio.el`, a full terminal graph explorer), a **Hebbian field-model** proof of concept, and El builtin / LLM-builtin smoke test suites. This is the reference for correct patterns when an El program uses Engram as its substrate. Spec: [ql/spec/elql.md](ql/spec/elql.md).
|
||||
|
||||
### [ui/](ui/) — el-ui
|
||||
|
||||
A frontend framework where **component state is an Engram graph and reactivity is spreading activation** — not virtual-DOM diffing (React), Proxy-based dependency tracking (Vue), or compile-time analysis (Svelte). Re-renders are activated and propagated the same way associative memory retrieval works in `engram/`.
|
||||
|
||||
~15 vessels covering the full frontend surface: `el-platform` (env/fs/network/clock abstraction), `el-config`, `el-html` (SSR emit primitives), `el-layout`, `el-style` (design tokens/themes), `el-i18n`, `el-auth` / `el-identity` (JWT, sessions, OAuth PKCE — Engram-native), `el-services` (REST/gRPC/WebSocket bindings), `el-aop` (`@authenticate`/`@authorize`/`@cache`/`@rate_limit` decorators), `el-secrets`, `el-graph` (graph rendering/editor), `el-publish` (App Store / Play Store automation), and `el-ui-compiler` (El→JS component compiler; currently a stub pending a JS backend in `elc`). Spec: [ui/spec/framework.md](ui/spec/framework.md).
|
||||
|
||||
### [arbor/](arbor/) — diagram language
|
||||
|
||||
A `.arbor` diagram language and toolchain: `arbor-core` (NodeId/shape/edge-kind types), `arbor-parse` (recursive-descent parser), `arbor-diagram` (IR + Mermaid serializer + architecture-diagram builders), `arbor-layout` (hierarchical layout — rank assignment, positioning, group bounds), `arbor-render` (SVG renderer), `arbor-cli`. (The architecture map above is the kind of diagram this is for.)
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
Install the El SDK from the latest release:
|
||||
|
||||
```bash
|
||||
bash lang/install.sh
|
||||
# EL_VERSION=v1.0.0 bash lang/install.sh # pin a specific release tag
|
||||
# EL_PREFIX=/opt/el bash lang/install.sh # custom install prefix
|
||||
```
|
||||
|
||||
Or build the compiler from source and verify the self-hosting chain:
|
||||
|
||||
```bash
|
||||
cd lang
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
|
||||
# Confirm the new binary reproduces itself exactly
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
|
||||
mv dist/platform/elc-new dist/platform/elc
|
||||
```
|
||||
|
||||
Run your first program:
|
||||
|
||||
```bash
|
||||
./lang/dist/platform/elc lang/examples/hello.el > hello.c
|
||||
cc -std=c11 -I lang/el-compiler/runtime -lcurl -lpthread \
|
||||
-o hello hello.c lang/el-compiler/runtime/el_seed.c
|
||||
./hello
|
||||
```
|
||||
|
||||
More examples in [lang/examples/](lang/examples/), including a full starter project at `lang/examples/hello-project/`.
|
||||
|
||||
If the compiler binary is ever lost or corrupted, [lang/BOOTSTRAP.md](lang/BOOTSTRAP.md) is the authoritative recovery path.
|
||||
|
||||
---
|
||||
|
||||
## Development workflow
|
||||
|
||||
Branching follows `dev → stage → main`: work lands on `dev`, promotes to `stage` for integration testing, and is promoted to `main` for release (visible directly in the git history of this repo). CI is defined per-subproject under `.gitea/workflows/` — `lang`/`epm`/`ide` share the root pipeline; `engram` and `ql` carry their own (`ci-dev`, `ci-stage`, and a release workflow each).
|
||||
|
||||
- Language/runtime specs live at `*/spec/*.md` (`lang/spec/`, `ql/spec/`, `ui/spec/`) and are the single source of truth for implemented-vs-planned status — code and docs are expected to agree with the spec's status markers, not the other way around.
|
||||
- Agent-facing orientation guides live at `*/AGENTS.md` (currently `lang/AGENTS.md`); more subprojects may grow their own as they need agent-specific conventions documented.
|
||||
- Tagged releases live under `lang/releases/`, each with its own `RELEASE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
This is an actively developed, internal monorepo — not yet published under an open license. Treat everything here as proprietary to Neuron Technologies unless told otherwise.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Reasoning Operators — Decisions & Reversal
|
||||
|
||||
**Date:** 2026-08-13
|
||||
**Branch:** `engram-tiered-storage` (worktree `/tmp/engram-tiered-wt`)
|
||||
**Status:** staged locally — NOT pushed, NOT tagged, NOT merged. Live `:8742` untouched.
|
||||
|
||||
## What this adds
|
||||
|
||||
A **REASONING layer** built as pure C compositions over the already-live §5 geometry
|
||||
OPERATORS (`engram_geometry.{h,c}`: overlap, subtract, setdiff, combine, distance,
|
||||
analogy). Where the operators are a relational algebra over neighborhood descriptors,
|
||||
these are reasoning *modes* built by chaining that algebra. New files:
|
||||
|
||||
- `lang/runtime/engram_reason.h` — public API for the five modes + a shared
|
||||
point-to-manifold fit primitive.
|
||||
- `lang/runtime/engram_reason.c` — implementations. READ-ONLY over descriptor inputs,
|
||||
`stdlib + libm` only, touches no store / index / activation. All geometry is
|
||||
delegated to `engram_geo_*`; this file only composes.
|
||||
- `engram/test/test_reason.c` + `engram/test/run_reason_tests.sh` — closed-form
|
||||
constructed tests (hand-built descriptors with known answers), PERF + ASan/UBSan.
|
||||
|
||||
El-exposure (pass-through, no self-host fold):
|
||||
- `lang/runtime/el_runtime.c` — `+#include "engram_reason.h"` and the builtin
|
||||
`engram_reason_analogy_json(a_csv,b_csv,c_csv)`.
|
||||
- `lang/runtime/el_runtime.h` — its declaration.
|
||||
- `lang/runtime/el_seed.c` — native `__engram_reason_analogy_json` wrapper (same
|
||||
C-table wiring as the §5 ops).
|
||||
|
||||
## The five modes — signatures & composition
|
||||
|
||||
| Mode | C entry point | Composes |
|
||||
|------|---------------|----------|
|
||||
| **ANALOGY** `A:B :: C:?` | `engram_reason_analogy(A,B,C,candidates,n,out)` | `engram_geo_analogy` (Procrustes R) + `engram_geo_analogy_apply` + centroid L2. Learns `R_{A→B}` = `engram_geo_analogy(B,A)` (that op returns R with `apply(R, Y-axis)≈X-axis`), reconstructs the residual translation `t = c_B − R·c_A`, maps `mapped = R·c_C + t`, ranks candidates by distance. |
|
||||
| **INDUCTION** `{E_i}→rule` | `engram_reason_induce(examples,n,top_axes,ext_floor,out)` + `engram_reason_membership` | `engram_geo_combine` folded left→right → pooled "rule" descriptor; shared subspace surfaces as the dominant pooled axes. Membership = point-to-manifold fit. |
|
||||
| **ABDUCTION** `x→best H` | `engram_reason_abduce(obs,dim,hyps,n,ext_floor,out)` | shared `engram_reason_point_fit` against each hypothesis; argmax fit score; full ranking. |
|
||||
| **CAUSAL** `x?y \| Z,t` | `engram_reason_causal(x,y,confounders,nZ,t_x,t_y,drop_frac,out)` | centroid cosine (raw correlation) + `engram_geo_subtract` residual-centroid (control for each confounder, take the strongest single explainer) + temporal precedence. Verdict `DIRECTED` / `CONFOUNDED` / `NONE` + a `confounded` flag. |
|
||||
| **PLANNING** `start→goal` | `engram_reason_plan(nodes,n,start,goal,radius,use_w,out)` | `engram_geo_distance` as edge weights over neighborhoods within `radius`; O(n²) Dijkstra → discrete geodesic path. |
|
||||
|
||||
Shared primitive `engram_reason_point_fit` splits `(x − centroid)` into an in-subspace
|
||||
Mahalanobis distance (scaled by axis extents) and an orthogonal off-model residual;
|
||||
it is the single engine under INDUCTION's membership test and ABDUCTION's ranking.
|
||||
|
||||
## Proof (DONE-WITH-PROOF)
|
||||
|
||||
`engram/test/run_reason_tests.sh`: **33/33 checks, 0 failures** on BOTH passes
|
||||
(PERF -O2, and ASan+UBSan). macOS `leaks --atExit`: **0 leaks / 0 total leaked bytes**.
|
||||
|
||||
Per-mode closed-form assertions actually exercised:
|
||||
- **ANALOGY** — A→B = +90° rotation in e0-e1 plane + a +5 shift in e2; Procrustes
|
||||
residual `~0`; predicted point `(0,2,5,0)` recovered exactly; nearest candidate =
|
||||
the planted true D (index 1), distance `~0`.
|
||||
- **INDUCTION** — 3 examples sharing span(e0,e1) (extents 1.0 / 0.8) each with a small
|
||||
idiosyncratic axis (e2 or e3); induced top-2 axes lie in span(e0,e1) (extents
|
||||
recovered ~1.0 / ~0.8); held-out in-plane point fits (membership 0.885), off-subspace
|
||||
point rejected (0.100), in-plane-but-far point rejected (0.039).
|
||||
- **ABDUCTION** — observation planted inside H1 among {H0,H1,H2}; best = H1, rank[0] = H1,
|
||||
H1 smallest distance.
|
||||
- **CAUSAL** — chain A→B→C along e0 (t 1<2<3) + confounder Z(e1) that leaks into A and
|
||||
drives D(t=4): A→B and B→C flagged `DIRECTED` with correct precedence and association
|
||||
that survives control; A–D `CONFOUNDED` (raw |cos|=0.707 collapses to 0.0 under
|
||||
control) with `confounded=1`; B–D `NONE` (no association).
|
||||
- **PLANNING** — 6 neighborhoods on a semicircle (r=10); `neighbor_radius=7` admits only
|
||||
consecutive hops; plan = `[0,1,2,3,4,5]` (the arc), cost `30.90` (> the 20-unit chord,
|
||||
confirming it is the geodesic through the manifold, not a straight jump); a too-small
|
||||
radius correctly yields `reached=0`.
|
||||
|
||||
## El-exposure status
|
||||
|
||||
- **ANALOGY is el-callable** via the same pass-through the §5 operators use. Proof: a
|
||||
container-capped fold (`capfold.sh`, peak ~0 GB) of a demo `.el` through the shipped
|
||||
`lang/dist/platform/elc` emits a *direct C call* `engram_reason_analogy_json(A,B,A)`
|
||||
(no registration, no self-host fold); the generated C links against `el_runtime.c` +
|
||||
`engram_reason.c` + geometry/store/vindex and runs end-to-end. (The standalone demo's
|
||||
store copy boots 0 nodes — a pre-existing quirk that hits the *shipped geo demo
|
||||
identically* — so the call returns `{"error":"geometry unavailable"}`; this still proves
|
||||
the compiled El → C reasoning symbol → JSON chain executes. Numeric correctness on real
|
||||
data is covered by the C test.) This compile also confirms `el_runtime.c` +
|
||||
`engram_reason.c` compile and link clean.
|
||||
- **INDUCTION / ABDUCTION / CAUSAL / PLANNING are C-layer only for now.** Their inputs are
|
||||
candidate *sets*, raw *points*, and *timestamps* that do not map to the flat comma-
|
||||
separated-seed El ABI. A richer marshalling surface would touch the codegen/registration
|
||||
path and risk an uncapped fold — explicitly deferred per the hard rail. The C functions
|
||||
are fully proven and callable from any C caller today.
|
||||
|
||||
## Build wiring (for the later cutover/durability pass)
|
||||
|
||||
`engram_reason.c` must be added to the engram server link line **alongside**
|
||||
`engram_geometry.c` (the heavy-runtime path `cc dist/engram.c el_runtime.c
|
||||
engram_store.c engram_geometry.c engram_vindex.c …`). `el_runtime.c` now
|
||||
`#include`s `engram_reason.h` and references `engram_reason_analogy_json`, so a build
|
||||
that omits `engram_reason.c` will fail to link that symbol. One-line addition, same as
|
||||
how `engram_geometry.c` was originally added.
|
||||
|
||||
## Reversal
|
||||
|
||||
Fully additive; nothing existing was modified in behavior. To revert:
|
||||
|
||||
1. Delete `lang/runtime/engram_reason.h`, `lang/runtime/engram_reason.c`,
|
||||
`engram/test/test_reason.c`, `engram/test/run_reason_tests.sh`, and this doc.
|
||||
2. In `lang/runtime/el_runtime.c`: remove `#include "engram_reason.h"` and the
|
||||
`engram_reason_analogy_json` function.
|
||||
3. In `lang/runtime/el_runtime.h`: remove the `engram_reason_analogy_json` declaration.
|
||||
4. In `lang/runtime/el_seed.c`: remove the `__engram_reason_analogy_json` wrapper.
|
||||
5. Remove `engram_reason.c` from any server link line if the cutover added it.
|
||||
|
||||
No store, schema, config, WAL, or on-disk format was touched; no data migration exists,
|
||||
so reversal is a pure code removal with no state to undo.
|
||||
@@ -1,128 +0,0 @@
|
||||
# Verifier Layer — Grounding + Consistency (decisions + reversal)
|
||||
|
||||
**Date:** 2026-08-13
|
||||
**Branch:** `engram-tiered-storage` (worktree `/tmp/engram-tiered-wt`), atop `a3358df`
|
||||
**Scope:** additive, read-only, staged. No push, no tag, no merge. Live `:8742` untouched.
|
||||
|
||||
## What this adds
|
||||
|
||||
The VERIFIER layer — the "disposes" half of the propose→verify loop. The geometry
|
||||
PROPOSES (cheap, creative, sometimes wrong); the verifier DISPOSES, catching the
|
||||
class of failure no grammar check sees: a fluent, confident, WRONG output — the
|
||||
**plausible lie**.
|
||||
|
||||
Motivating failure (tonight's PT translation): a deleted negation turned
|
||||
"you never fought" into "you argued" — reassurance inverted into accusation,
|
||||
grammatical and invisible, catchable only by the geometry.
|
||||
|
||||
Two checks, both pure C11 (stdlib + libm), read-only over their inputs, touching no
|
||||
store / index / activation. Every geometry op is delegated to the already-shipped
|
||||
reasoning + §5 operator primitives; this layer only composes and thresholds.
|
||||
|
||||
### Files added
|
||||
- `lang/runtime/engram_verify.h` — API + design contract.
|
||||
- `lang/runtime/engram_verify.c` — implementation.
|
||||
- `engram/test/test_verify.c` — closed-form constructed cases (29 checks).
|
||||
- `engram/test/run_verify_tests.sh` — two-pass runner (PERF, then ASan/UBSan).
|
||||
|
||||
No existing file was modified.
|
||||
|
||||
## C signatures + how each composes the existing primitives
|
||||
|
||||
### GROUNDING (anti-hallucination)
|
||||
```c
|
||||
int engram_verify_grounding(const float* claim, int dim,
|
||||
const GeoDescriptor* const* evidence, int n_evidence,
|
||||
double ext_floor, double ground_threshold,
|
||||
GeoGrounding* out);
|
||||
```
|
||||
Fits the claim POINT against every real evidence neighborhood via
|
||||
`engram_reason_point_fit` (in-distribution Mahalanobis + off-model orthogonal
|
||||
residual) and keeps the BEST supporter. Grounded iff best fit score ≥
|
||||
`ground_threshold`. Deliberately an ABSOLUTE-THRESHOLD gate, distinct from ABDUCTION
|
||||
(which always ranks and picks a winner): grounding asks the prior question — "is there
|
||||
any real support at all?" — and may answer no. The off-model `best_ortho` residual is
|
||||
the sharpest hallucination signal: energy in a direction the manifold does not span.
|
||||
|
||||
### CONSISTENCY (contradiction detection)
|
||||
```c
|
||||
int engram_verify_consistency(const float* claim, int dim,
|
||||
const GeoDescriptor* context,
|
||||
const GeoDescriptor* pole_pos, const GeoDescriptor* pole_neg,
|
||||
const GeoDescriptor* forbidden,
|
||||
double ext_floor, double deadzone_frac,
|
||||
double forbidden_thresh, double max_distance,
|
||||
GeoConsistency* out);
|
||||
```
|
||||
Two independent sub-checks (either can fire; both flags reported):
|
||||
|
||||
- **(a) POLARITY / negation inversion** — the reassurance→accusation catch.
|
||||
A polarity axis `p = (c_pos − c_neg)/‖·‖` is defined by two REAL poles (affirm vs
|
||||
negate), midpoint `o = ½(c_pos + c_neg)`. Signed sides: `claim_side = p·(claim − o)`,
|
||||
`ref_side = p·(c_context − o)`. If they have OPPOSITE sign and both clear the neutral
|
||||
deadzone (`deadzone_frac·½‖c_pos−c_neg‖`), the claim asserts the polarity opposite to
|
||||
the grounded truth → inversion flagged. Pure dot products / projections over the same
|
||||
centroids the geometry already computes.
|
||||
- **(b) GEOMETRIC contradiction** — claim sits INSIDE a `forbidden` region it must be
|
||||
far from (`engram_reason_point_fit` score ≥ `forbidden_thresh`), OR violates a
|
||||
max-distance constraint to `context` (`L2 > max_distance`).
|
||||
|
||||
## Proof (constructed cases — demonstrate, not declare)
|
||||
|
||||
`./engram/test/run_verify_tests.sh` → **29 checks, 0 failures** in BOTH passes
|
||||
(PERF -O2, and ASan+UBSan -O1). macOS `leaks --atExit`: **0 leaks for 0 total leaked
|
||||
bytes**.
|
||||
|
||||
Key demonstrated numbers:
|
||||
- Grounding IN (claim inside E0): score 0.885, grounded=1, ortho≈0.
|
||||
- Grounding OUT (claim floating along unmodeled e2): score 0.0004, grounded=0,
|
||||
ortho=50.0 (the hallucination signal), nearest centroid L2=50.
|
||||
- **Negation inversion (the catch):** truth "never fought" ref_side=−5.0, lie
|
||||
"you argued" claim_side=+4.0 → opposite poles → `inverted=1`, verdict=POLARITY,
|
||||
consistency=0. Faithful claim (−4.0, same pole) → inverted=0, verdict=OK,
|
||||
consistency=1. Neutral claim inside deadzone → not triggered.
|
||||
- Geometric: claim inside forbidden region → geo_violation=1 (forb_fit 0.99);
|
||||
claim beyond max_distance → geo_violation=1 (ctx_dist 8.0 > 3.0).
|
||||
- **Combined (the whole point):** a claim that is GROUNDED in real vocabulary
|
||||
(grounded=1, score 1.0) yet polarity-inverted is PASSED by grounding and CAUGHT
|
||||
only by consistency (verdict=POLARITY). Grounding alone is insufficient; consistency
|
||||
is the catch.
|
||||
|
||||
## el-exposure — DEFERRED (matches reasoning-agent precedent)
|
||||
|
||||
Not exposed as el builtins this pass. The reasoning agent exposed ONLY `analogy`
|
||||
(three seed-identified neighborhoods → the clean fixed-arity seed-CSV→descriptor JSON
|
||||
pattern) and deferred its point-input / variadic-set modes (abduction, induction,
|
||||
causal, planning). The verifier's grounding (claim POINT + variadic evidence SET) and
|
||||
consistency (claim POINT + context + two poles + forbidden + scalar thresholds) are
|
||||
exactly those shapes: no clean fixed-arity seed-CSV JSON mapping exists, and adding one
|
||||
would require new JSON list-of-lists + point-vector marshaling absent from the codebase,
|
||||
risking an elc rebuild/fold (violates the capped-fold-only rail). A claim is also an
|
||||
arbitrary proposed POINT, not necessarily an existing node — so the point-native C API
|
||||
is the correct primitive. Deferred deliberately; the C layer is complete and proven.
|
||||
|
||||
When exposed later, follow the same additive pass-through pattern used for the geo
|
||||
operators: native `engram_verify_*_json(el_val_t ...)` in `el_runtime.c` (heavy runtime)
|
||||
+ a `__engram_verify_*_json` wrapper in `el_seed.c`, resolving seed CSVs → descriptors
|
||||
and marshaling the claim vector — no fold needed for callability (shipped elc passes
|
||||
unknown-ident builtin calls straight through to the heavy-runtime C symbols).
|
||||
|
||||
## Reach checks (FORMAL / CAUSAL / PREDICTIVE) — NOT STARTED
|
||||
|
||||
Honestly not started this pass; the two tractable-now checks (grounding + consistency)
|
||||
were driven to done-with-proof first as specified.
|
||||
- **CAUSAL** already exists as a REASONING operator (`engram_reason_causal`,
|
||||
intervention/temporal-precedence over the typed causal graph); a verifier wrapper that
|
||||
checks "does the claimed cause actually precede/influence" would compose it — not built.
|
||||
- **FORMAL** (logical consistency of a claim SET) needs an external checker (SMT/proof
|
||||
kernel) — the non-geometric seam; not built.
|
||||
- **PREDICTIVE** (commit a prediction, check vs outcome, restructure on error) is the CGI
|
||||
research frontier; not built.
|
||||
|
||||
## Reversal
|
||||
|
||||
Fully additive. To reverse: delete the four added files
|
||||
(`lang/runtime/engram_verify.{h,c}`, `engram/test/test_verify.c`,
|
||||
`engram/test/run_verify_tests.sh`) or `git revert` this commit. Nothing else references
|
||||
them; no build wiring, no el registration, no store schema, no runtime path was changed.
|
||||
Live `:8742` was never touched.
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_get(json: String, key: String) -> String
|
||||
extern fn generate_frame(frame: [String]) -> String
|
||||
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
|
||||
extern fn generate_frame(frame: Any) -> String
|
||||
extern fn generate_frame_lang(frame: Any, lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> Any
|
||||
extern fn generate(semantic_form_json: String) -> String
|
||||
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
|
||||
|
||||
+28
-28
@@ -1,22 +1,22 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn slots_get(slots: [String], key: String) -> String
|
||||
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
|
||||
extern fn make_slots(k0: String, v0: String) -> [String]
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
|
||||
extern fn rule_id(rule: [String]) -> String
|
||||
extern fn rule_lhs(rule: [String]) -> String
|
||||
extern fn rule_rhs_len(rule: [String]) -> Int
|
||||
extern fn rule_rhs(rule: [String], idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
|
||||
extern fn build_rules() -> [[String]]
|
||||
extern fn get_rules() -> [[String]]
|
||||
extern fn find_rule(rule_id_str: String) -> [String]
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn slots_get(slots: Any, key: String) -> String
|
||||
extern fn slots_set(slots: Any, key: String, val: String) -> Any
|
||||
extern fn make_slots(k0: String, v0: String) -> Any
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> Any
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> Any
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> Any
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> Any
|
||||
extern fn rule_id(rule: Any) -> String
|
||||
extern fn rule_lhs(rule: Any) -> String
|
||||
extern fn rule_rhs_len(rule: Any) -> Int
|
||||
extern fn rule_rhs(rule: Any, idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> Any
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> Any
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> Any
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> Any
|
||||
extern fn build_rules() -> Any
|
||||
extern fn get_rules() -> Any
|
||||
extern fn find_rule(rule_id_str: String) -> Any
|
||||
extern fn make_leaf(label: String, word: String) -> String
|
||||
extern fn make_node1(label: String, child0: String) -> String
|
||||
extern fn make_node2(label: String, child0: String, child1: String) -> String
|
||||
@@ -24,15 +24,15 @@ extern fn make_node3(label: String, child0: String, child1: String, child2: Stri
|
||||
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
|
||||
extern fn nlg_is_ws(c: String) -> Bool
|
||||
extern fn skip_ws(s: String, pos: Int) -> Int
|
||||
extern fn scan_token(s: String, start: Int) -> [String]
|
||||
extern fn scan_token(s: String, start: Int) -> Any
|
||||
extern fn render_tree(tree: String) -> String
|
||||
extern fn gram_word_order(profile: [String]) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
|
||||
extern fn gram_question_strategy(profile: [String]) -> String
|
||||
extern fn gram_word_order(profile: Any) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: Any) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: Any) -> String
|
||||
extern fn gram_question_strategy(profile: Any) -> String
|
||||
extern fn is_pronoun(word: String) -> Bool
|
||||
extern fn build_np(referent: String, slots: [String]) -> String
|
||||
extern fn build_np(referent: String, slots: Any) -> String
|
||||
extern fn build_pp(loc: String) -> String
|
||||
extern fn build_vp_body(slots: [String]) -> String
|
||||
extern fn build_vp_from_slots(slots: [String]) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
|
||||
extern fn build_vp_body(slots: Any) -> String
|
||||
extern fn build_vp_from_slots(slots: Any) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: Any) -> String
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn agent_person(agent: String) -> String
|
||||
extern fn agent_number(agent: String) -> String
|
||||
extern fn realize_np(referent: String, number: String) -> String
|
||||
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String]
|
||||
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String
|
||||
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: Any) -> Any
|
||||
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: Any) -> String
|
||||
extern fn capitalize_first(s: String) -> String
|
||||
extern fn add_punct(s: String, intent: String) -> String
|
||||
extern fn realize_lang(form: [String], profile: [String]) -> String
|
||||
extern fn realize(form: [String]) -> String
|
||||
extern fn realize_lang(form: Any, profile: Any) -> String
|
||||
extern fn realize(form: Any) -> String
|
||||
|
||||
+15
-15
@@ -1,18 +1,18 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String]
|
||||
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String]
|
||||
extern fn sem_frame_simple(intent: String, subject: String) -> [String]
|
||||
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String]
|
||||
extern fn sem_intent(frame: [String]) -> String
|
||||
extern fn sem_subject(frame: [String]) -> String
|
||||
extern fn sem_object(frame: [String]) -> String
|
||||
extern fn sem_modifiers(frame: [String]) -> String
|
||||
extern fn sem_lang(frame: [String]) -> String
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> Any
|
||||
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> Any
|
||||
extern fn sem_frame_simple(intent: String, subject: String) -> Any
|
||||
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> Any
|
||||
extern fn sem_intent(frame: Any) -> String
|
||||
extern fn sem_subject(frame: Any) -> String
|
||||
extern fn sem_object(frame: Any) -> String
|
||||
extern fn sem_modifiers(frame: Any) -> String
|
||||
extern fn sem_lang(frame: Any) -> String
|
||||
extern fn sem_first_modifier(mods: String) -> String
|
||||
extern fn sem_intent_to_realize(intent: String) -> String
|
||||
extern fn sem_to_spec(frame: [String]) -> [String]
|
||||
extern fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String]
|
||||
extern fn sem_to_spec(frame: Any) -> Any
|
||||
extern fn sem_to_spec_full(frame: Any, verb: String, tense: String, aspect: String) -> Any
|
||||
extern fn sem_realize_greet(subject: String) -> String
|
||||
extern fn sem_realize(frame: [String]) -> String
|
||||
extern fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String
|
||||
extern fn sem_realize_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn sem_realize(frame: Any) -> String
|
||||
extern fn sem_realize_full(frame: Any, verb: String, tense: String, aspect: String) -> String
|
||||
extern fn sem_realize_lang(frame: Any, lang_code: String) -> String
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)/el}"
|
||||
ELC="${ELC:-${EL_HOME}/dist/platform/elc}"
|
||||
RUNTIME_DIR="${EL_HOME}/runtime"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
SRC_DIR="$(cd .. && pwd)/src"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -49,12 +49,6 @@ jobs:
|
||||
echo "Downloading el_runtime.h..."
|
||||
curl -fsSL "${RELEASE_BASE}/el_runtime.h" -o /usr/local/lib/el/el_runtime.h
|
||||
|
||||
echo "Downloading engram_store.c..."
|
||||
curl -fsSL "${RELEASE_BASE}/engram_store.c" -o /usr/local/lib/el/engram_store.c
|
||||
|
||||
echo "Downloading engram_store.h..."
|
||||
curl -fsSL "${RELEASE_BASE}/engram_store.h" -o /usr/local/lib/el/engram_store.h
|
||||
|
||||
echo "El SDK installed:"
|
||||
elc --version || true
|
||||
|
||||
@@ -68,12 +62,11 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
/usr/local/lib/el/el_runtime.c \
|
||||
/usr/local/lib/el/engram_store.c \
|
||||
-lcurl -lpthread
|
||||
echo "Linked dist/engram"
|
||||
ls -lh dist/engram
|
||||
|
||||
+2
-5
@@ -1,6 +1,3 @@
|
||||
.DS_Store
|
||||
*.db
|
||||
*.elc
|
||||
*.elh
|
||||
dist/
|
||||
target/
|
||||
*.db
|
||||
.DS_Store
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+780
-234
File diff suppressed because it is too large
Load Diff
+827
-593
File diff suppressed because it is too large
Load Diff
@@ -1,149 +0,0 @@
|
||||
/* bench_discrimination.c — M9 REFINEMENT bench: measures whether mean-centering
|
||||
* the anisotropic nomic-embed-text space sharpens the §5 geometry operators on
|
||||
* REAL data. Read-only over a COPY of the live store (never the live file).
|
||||
*
|
||||
* usage: bench_discrimination [store.egm]
|
||||
* (or set ENGRAM_BENCH_STORE). If no store is given/openable it prints
|
||||
* SKIP and exits 0 — so it is safe in CI without live data.
|
||||
*
|
||||
* It picks two semantically distinct cohorts by keyword (domain A vs domain B),
|
||||
* computes the global mean over the embed-eligible set (via engram_geo_mean_build
|
||||
* — the same offset the descriptor uses), then reports BEFORE (raw unit space)
|
||||
* vs AFTER (mean-centered space):
|
||||
* - cross-centroid cosine (lower = better separated)
|
||||
* - cross-centroid Euclid dist (translation-invariant: a control)
|
||||
* - intra-cohesion per domain (member cos to own centroid)
|
||||
* - overlap operator (cross_cos / sqrt(intraA*intraB): ~1 = domains
|
||||
* indistinguishable, ~0 = cleanly separated)
|
||||
* - angular separation ratio z (centroid angle / summed angular spread)
|
||||
* - mean pairwise cosine sample (the anisotropy headline; ~0.55 raw -> ~0 ctr)
|
||||
*
|
||||
* Pure C11; links engram_store.c + engram_geometry.c; -lm.
|
||||
*/
|
||||
#include "engram_store.h"
|
||||
#include "engram_geometry.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <math.h>
|
||||
|
||||
#define CAP_DOMAIN 400
|
||||
#define CAP_SAMPLE 800
|
||||
|
||||
typedef struct { float** v; int n, cap, dim; } VecSet;
|
||||
static void vs_init(VecSet* s){ s->v=NULL; s->n=0; s->cap=0; s->dim=0; }
|
||||
static void vs_push(VecSet* s, const float* e, int dim, int cap){
|
||||
if(s->n>=cap) return;
|
||||
if(s->dim==0) s->dim=dim;
|
||||
if(s->n==s->cap){ int nc=s->cap?s->cap*2:64; s->v=realloc(s->v,(size_t)nc*sizeof*s->v); s->cap=nc; }
|
||||
float* c=malloc((size_t)dim*sizeof(float));
|
||||
double nn=0; for(int d=0;d<dim;d++) nn+=(double)e[d]*e[d]; nn=sqrt(nn);
|
||||
if(nn<1e-12){ free(c); return; }
|
||||
for(int d=0;d<dim;d++) c[d]=(float)(e[d]/nn); /* L2-normalized copy */
|
||||
s->v[s->n++]=c;
|
||||
}
|
||||
static void vs_free(VecSet* s){ for(int i=0;i<s->n;i++) free(s->v[i]); free(s->v); }
|
||||
|
||||
typedef struct { VecSet A, B, S; long idx; } Coh;
|
||||
static int has(const char* h, const char* n){ return h && strcasestr(h,n)!=NULL; }
|
||||
static void cb(const StoreNode* n, void* ctx){
|
||||
Coh* c=ctx;
|
||||
if(!(n->emb && n->emb_dim>0)) return;
|
||||
/* every 5th embedded node -> isotropy sample */
|
||||
if((c->idx++ % 5)==0) vs_push(&c->S, n->emb, n->emb_dim, CAP_SAMPLE);
|
||||
const char* t=n->content; const char* g=n->tags;
|
||||
int A = has(t,"quantiz")||has(g,"quantiz")||has(t,"lorablation")||has(t,"70B")||has(t,"LoRA merge");
|
||||
int B = has(t,"kubernetes")||has(t,"terraform")||has(t,"argo")||has(g,"infrastructure")||has(t,"vault")||has(t,"cloudflare");
|
||||
if(A && !B) vs_push(&c->A, n->emb, n->emb_dim, CAP_DOMAIN);
|
||||
else if(B && !A) vs_push(&c->B, n->emb, n->emb_dim, CAP_DOMAIN);
|
||||
}
|
||||
|
||||
/* mean of a VecSet into out (dim doubles). */
|
||||
static void mean_of(const VecSet* s, const float* gm, double* out){
|
||||
int dim=s->dim; for(int d=0;d<dim;d++) out[d]=0;
|
||||
for(int i=0;i<s->n;i++) for(int d=0;d<dim;d++) out[d]+=(double)s->v[i][d]-(gm?gm[d]:0.0);
|
||||
if(s->n) for(int d=0;d<dim;d++) out[d]/=s->n;
|
||||
}
|
||||
static double dnorm(const double* a, int dim){ double s=0; for(int d=0;d<dim;d++) s+=a[d]*a[d]; return sqrt(s); }
|
||||
static double dcos(const double* a, const double* b, int dim){
|
||||
double na=dnorm(a,dim), nb=dnorm(b,dim); if(na<1e-12||nb<1e-12) return 0;
|
||||
double s=0; for(int d=0;d<dim;d++) s+=a[d]*b[d]; double c=s/(na*nb);
|
||||
if(c>1)c=1; if(c<-1)c=-1; return c;
|
||||
}
|
||||
static double deuclid(const double* a, const double* b, int dim){
|
||||
double s=0; for(int d=0;d<dim;d++){ double x=a[d]-b[d]; s+=x*x; } return sqrt(s);
|
||||
}
|
||||
/* mean cosine of members (minus gm) to centroid c (already gm-subtracted). */
|
||||
static double cohesion(const VecSet* s, const float* gm, const double* c){
|
||||
int dim=s->dim; double nc=dnorm(c,dim); if(nc<1e-12||s->n==0) return 0;
|
||||
double acc=0; for(int i=0;i<s->n;i++){
|
||||
double dot=0, nv=0;
|
||||
for(int d=0;d<dim;d++){ double v=(double)s->v[i][d]-(gm?gm[d]:0.0); dot+=v*c[d]; nv+=v*v; }
|
||||
nv=sqrt(nv); if(nv<1e-12) continue; double cc=dot/(nv*nc);
|
||||
if(cc>1)cc=1; if(cc<-1)cc=-1; acc+=cc;
|
||||
}
|
||||
return acc/s->n;
|
||||
}
|
||||
/* mean pairwise cosine over a sample (isotropy metric). */
|
||||
static double mean_pairwise_cos(const VecSet* s, const float* gm){
|
||||
int dim=s->dim; if(s->n<2) return 0; double acc=0; long np=0;
|
||||
for(int i=0;i<s->n;i++) for(int j=i+1;j<s->n;j++){
|
||||
double dot=0, na=0, nb=0;
|
||||
for(int d=0;d<dim;d++){ double a=(double)s->v[i][d]-(gm?gm[d]:0.0), b=(double)s->v[j][d]-(gm?gm[d]:0.0);
|
||||
dot+=a*b; na+=a*a; nb+=b*b; }
|
||||
na=sqrt(na); nb=sqrt(nb); if(na<1e-12||nb<1e-12) continue;
|
||||
double c=dot/(na*nb); if(c>1)c=1; if(c<-1)c=-1; acc+=c; np++;
|
||||
}
|
||||
return np? acc/np : 0;
|
||||
}
|
||||
|
||||
static void report(const char* label, Coh* c, const float* gm){
|
||||
int dim=c->A.dim; double* ca=malloc((size_t)dim*sizeof(double)); double* cb=malloc((size_t)dim*sizeof(double));
|
||||
mean_of(&c->A, gm, ca); mean_of(&c->B, gm, cb);
|
||||
double xcos=dcos(ca,cb,dim), xeuc=deuclid(ca,cb,dim);
|
||||
double cohA=cohesion(&c->A,gm,ca), cohB=cohesion(&c->B,gm,cb);
|
||||
double overlap = (cohA>0&&cohB>0)? xcos/sqrt(cohA*cohB) : xcos;
|
||||
double theta = acos(xcos<-1?-1:(xcos>1?1:xcos));
|
||||
double sig = acos(cohA<-1?-1:(cohA>1?1:cohA)) + acos(cohB<-1?-1:(cohB>1?1:cohB));
|
||||
double z = (sig>1e-9)? theta/sig : 0;
|
||||
double mpc = mean_pairwise_cos(&c->S, gm);
|
||||
printf(" [%s]\n", label);
|
||||
printf(" cross-centroid cosine = %+.4f (lower = better separated)\n", xcos);
|
||||
printf(" cross-centroid Euclid = %.4f (translation-invariant control)\n", xeuc);
|
||||
printf(" intra-cohesion A / B = %.4f / %.4f\n", cohA, cohB);
|
||||
printf(" OVERLAP operator = %.4f (~1 = indistinguishable, ~0 = clean)\n", overlap);
|
||||
printf(" angular separation z = %.3f (centroid-angle / summed spread; >1 = separated)\n", z);
|
||||
printf(" mean pairwise cosine = %+.4f (isotropy: ~0.55 anisotropic -> ~0 isotropic)\n", mpc);
|
||||
free(ca); free(cb);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv){
|
||||
const char* path = (argc>1)? argv[1] : getenv("ENGRAM_BENCH_STORE");
|
||||
if(!path){ printf("SKIP: no store path (arg or ENGRAM_BENCH_STORE)\n"); return 0; }
|
||||
EngramPagedStore* st=store_open(path);
|
||||
if(!st){ printf("SKIP: could not open %s\n", path); return 0; }
|
||||
|
||||
Coh c; vs_init(&c.A); vs_init(&c.B); vs_init(&c.S); c.idx=0;
|
||||
store_scan_nodes(st, cb, &c);
|
||||
printf("=== two-domain discrimination bench (real store copy) ===\n");
|
||||
printf("domain A (quantization) n=%d ; domain B (infrastructure) n=%d ; sample n=%d ; dim=%d\n",
|
||||
c.A.n, c.B.n, c.S.n, c.A.dim);
|
||||
if(c.A.n<3 || c.B.n<3){ printf("SKIP: a cohort is too small to be meaningful\n");
|
||||
vs_free(&c.A); vs_free(&c.B); vs_free(&c.S); store_close(st); return 0; }
|
||||
|
||||
GeoMeanCache* mc=engram_geo_mean_build(st);
|
||||
const float* gm=engram_geo_mean_vec(mc);
|
||||
printf("global-mean cache: dim=%d over %llu embedded nodes\n\n",
|
||||
engram_geo_mean_dim(mc), (unsigned long long)engram_geo_mean_count(mc));
|
||||
|
||||
printf("BEFORE (raw anisotropic unit space):\n");
|
||||
report("RAW", &c, NULL);
|
||||
printf("\nAFTER (mean-centered isotropic space):\n");
|
||||
report("CENTERED", &c, gm);
|
||||
|
||||
engram_geo_mean_free(mc);
|
||||
vs_free(&c.A); vs_free(&c.B); vs_free(&c.S);
|
||||
store_close(st);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M4 demand-paging buffer-pool gate. Pure C (NOT elb/elc). Writes only under /tmp.
|
||||
# Runs the suite twice: an -O2 correctness build and an ASan+UBSan build.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SRC="$HERE/../../lang/runtime/engram_store.c"
|
||||
TST="$HERE/test_bufpool.c"
|
||||
|
||||
echo "== compiling (gcc -O2): test_bufpool.c engram_store.c =="
|
||||
BIN="/tmp/test_bufpool.$$"
|
||||
gcc -O2 -Wall -Wextra -std=c11 "$TST" "$SRC" -o "$BIN"
|
||||
"$BIN"; rc=$?
|
||||
rm -f "$BIN"; rm -rf /tmp/engram-bufpool-test-*
|
||||
[ $rc -ne 0 ] && exit $rc
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan build (memory-error + UB checks; LSan unavailable on macOS) =="
|
||||
ABIN="/tmp/test_bufpool_asan.$$"
|
||||
gcc -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -std=c11 "$TST" "$SRC" -o "$ABIN"
|
||||
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 "$ABIN"; rc=$?
|
||||
rm -f "$ABIN"; rm -rf /tmp/engram-bufpool-test-*
|
||||
exit $rc
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M5 online-compaction + background-checkpointer gate. Pure C (NOT elb/elc).
|
||||
# Writes only under /tmp. Runs an -O2 correctness build then an ASan+UBSan build.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SRC="$HERE/../../lang/runtime/engram_store.c"
|
||||
TST="$HERE/test_compaction.c"
|
||||
|
||||
echo "== compiling (gcc -O2): test_compaction.c engram_store.c =="
|
||||
BIN="/tmp/test_compaction.$$"
|
||||
gcc -O2 -Wall -Wextra -std=c11 "$TST" "$SRC" -o "$BIN"
|
||||
"$BIN"; rc=$?
|
||||
rm -f "$BIN"; rm -rf /tmp/engram-compact-test-*
|
||||
[ $rc -ne 0 ] && exit $rc
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan build (memory-error + UB checks; LSan unavailable on macOS) =="
|
||||
ABIN="/tmp/test_compaction_asan.$$"
|
||||
gcc -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -std=c11 "$TST" "$SRC" -o "$ABIN"
|
||||
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 "$ABIN"; rc=$?
|
||||
rm -f "$ABIN"; rm -rf /tmp/engram-compact-test-*
|
||||
exit $rc
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the M9 FOUNDATION geometry-descriptor tests. Pure C11 (gcc/cc),
|
||||
# stdlib + libm only. Standalone module — NOT folded through elb/elc. Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
|
||||
# 2. SAFETY — ASan + UBSan on the same suite (memory-safety is size-independent).
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_geometry.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -o "$TMP/safe"
|
||||
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
|
||||
# PASS 3 (OPTIONAL): mean-centering discrimination bench on a COPY of a real
|
||||
# store. Skips cleanly unless ENGRAM_BENCH_STORE points at a store .egm — never
|
||||
# touches the live store. Read-only; not part of the pass/fail gate.
|
||||
echo
|
||||
echo "### PASS 3: DISCRIMINATION BENCH (optional; set ENGRAM_BENCH_STORE)"
|
||||
BSRC="$HERE/bench_discrimination.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
$CC $WARN -O2 -I"$RT" $BSRC -lm -o "$TMP/bench"
|
||||
"$TMP/bench" "${ENGRAM_BENCH_STORE:-}"
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P0 gate: engram_scan_nodes_emb_json read-only builtin.
|
||||
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p0-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile (plain) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p0" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
D="$WORK/d"; mkdir -p "$D"
|
||||
"$WORK/p0" "$D" || { echo "FAIL: run"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== assertions =="
|
||||
python3 - "$D" <<'PY'
|
||||
import json, sys, os
|
||||
d = sys.argv[1]
|
||||
def load(n):
|
||||
with open(os.path.join(d,n)) as f: return json.load(f)
|
||||
rc = 0
|
||||
def check(c,m):
|
||||
global rc
|
||||
print((" PASS: " if c else " FAIL: ")+m)
|
||||
if not c: rc=1
|
||||
|
||||
alln = load("emb_all.json")
|
||||
check(len(alln)==3, f"emb dump returns all 3 nodes (got {len(alln)})")
|
||||
# salience-sorted: high, mid, low
|
||||
labels=[n["label"] for n in alln]
|
||||
check(labels==["emb-high","emb-mid","noemb-low"], f"salience-sorted order {labels}")
|
||||
for n in alln:
|
||||
L=len(n["emb"])
|
||||
check(L==n["emb_dim"], f"{n['label']}: len(emb)={L} == emb_dim={n['emb_dim']}")
|
||||
check(alln[0]["emb_dim"]==16 and alln[1]["emb_dim"]==16, "embedded nodes report dim 16")
|
||||
check(alln[2]["emb_dim"]==0 and alln[2]["emb"]==[], "un-embedded node -> emb_dim 0, emb []")
|
||||
# first emb value round-trips ~0.10
|
||||
check(abs(alln[0]["emb"][0]-0.10)<1e-3, f"emb[0] round-trips (~0.10, got {alln[0]['emb'][0]})")
|
||||
|
||||
pg0=load("emb_pg0.json"); pg1=load("emb_pg1.json")
|
||||
check(len(pg0)==1 and len(pg1)==1, "pagination: one node per page")
|
||||
check(pg0[0]["id"]=="n-high" and pg1[0]["id"]=="n-mid", f"pages disjoint & ordered ({pg0[0]['id']},{pg1[0]['id']})")
|
||||
|
||||
plain=load("plain.json")
|
||||
check(len(plain)==3, "existing scan_nodes_json still returns 3")
|
||||
check(all("emb" not in n for n in plain), "existing scan_nodes_json carries NO emb (behavior-neutral)")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== latency (one 256-page over the 3-node copy) =="
|
||||
python3 - "$D" <<'PY'
|
||||
import os
|
||||
# timing was measured inside C not here; report emb payload size as a proxy
|
||||
sz=os.path.getsize(os.path.join(os.sys.argv[1] if False else __import__('sys').argv[1],"emb_all.json"))
|
||||
print(f" emb_all.json payload = {sz} bytes for 3 nodes")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p0.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -20 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p0.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/ds"; mkdir -p "$DS"
|
||||
"$WORK/p0.san" "$DS" >/dev/null 2>"$WORK/san_run.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san_run.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P0 EMB-ENDPOINT GATE: PASS ======"; else echo "====== P0 EMB-ENDPOINT GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P1 gate: two-threshold consolidation (ENGRAM_CONSOLIDATION).
|
||||
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p1-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
unset ENGRAM_STORE ENGRAM_CONSOLIDATION ENGRAM_CONSOL_CONN_MIN ENGRAM_CONSOL_PERM_MIN ENGRAM_CONSOL_WM_TOPK
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p1" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
echo
|
||||
echo "== (a) HEADLINE: hebb accrual curve over N co-activations (flag OFF, pure trunk) =="
|
||||
D="$WORK/a"; mkdir -p "$D"
|
||||
( unset ENGRAM_CONSOLIDATION; "$WORK/p1" accrual "$D" ) >"$WORK/accrual.txt" 2>&1 || { echo "FAIL accrual run"; fail=1; }
|
||||
python3 - "$WORK/accrual.txt" <<'PY'
|
||||
import json,sys,re
|
||||
rows=[]
|
||||
for line in open(sys.argv[1]):
|
||||
m=re.match(r'SAMPLE (\d+) (\{.*\})',line.strip())
|
||||
if not m: continue
|
||||
n=int(m.group(1)); j=json.loads(m.group(2))
|
||||
hm=j.get("hebb_max",0.0); hc=j.get("hebb_cand_max",0.0)
|
||||
rows.append((n,hm,hc))
|
||||
print(" N hebb_max 1-0.9999^N (predicted EWMA)")
|
||||
rc=0
|
||||
for n,hm,hc in rows:
|
||||
pred=1-0.9999**n
|
||||
print(f" {n:<7} {hm:<12.6g} {pred:.6g}")
|
||||
# assertions: monotonic rise, starts near ETA, tracks EWMA prediction
|
||||
first=rows[0]; last=rows[-1]
|
||||
def check(c,m):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+m);
|
||||
if not c: rc=1
|
||||
check(abs(first[1]-0.0001)<5e-5, f"first sample hebb ~= ETA 0.0001 (got {first[1]:.6g})")
|
||||
check(all(rows[i][1] <= rows[i+1][1]+1e-9 for i in range(len(rows)-1)), "hebb_max is monotonically non-decreasing over N")
|
||||
check(last[1] > first[1]*50, f"hebb accrues substantially by N={last[0]} (got {last[1]:.4g} vs {first[1]:.4g})")
|
||||
# EWMA fit: measured should be within 25% of 1-0.9999^N at the mid samples
|
||||
mid=[r for r in rows if 100<=r[0]<=2000]
|
||||
ok=all(abs(hm-(1-0.9999**n))/(1-0.9999**n) < 0.25 for n,hm,hc in mid)
|
||||
check(ok, "measured curve tracks the 1-0.9999^N EWMA prediction within 25% (co-activation P~1)")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== (b) CONNECTION threshold: strong ISE wires to wm_top, weak ISE wires nothing (flag ON) =="
|
||||
D="$WORK/b"; mkdir -p "$D"
|
||||
( export ENGRAM_CONSOLIDATION=1; "$WORK/p1" connect "$D" ) >"$WORK/connect.txt" 2>&1 || { echo "FAIL connect run"; fail=1; }
|
||||
cat "$WORK/connect.txt" | sed 's/^/ /'
|
||||
python3 - "$WORK/connect.txt" "$D/connect.json" <<'PY'
|
||||
import json,sys,re
|
||||
txt=open(sys.argv[1]).read()
|
||||
g=json.load(open(sys.argv[2]))
|
||||
def field(k):
|
||||
m=re.search(rf'{k} (\S+)',txt); return m.group(1) if m else None
|
||||
sid=field("ISE_STRONG_ID"); wid=field("ISE_WEAK_ID")
|
||||
m=re.search(r'EDGES before=(\d+) after_strong=(\d+) after_weak=(\d+)',txt)
|
||||
before,aftS,aftW=int(m.group(1)),int(m.group(2)),int(m.group(3))
|
||||
rc=0
|
||||
def check(c,mm):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+mm)
|
||||
if not c: rc=1
|
||||
strong_edges=[e for e in g["edges"] if e["from_id"]==sid and e["relation"]=="hebbian-associate"]
|
||||
weak_edges=[e for e in g["edges"] if e["from_id"]==wid]
|
||||
check(aftS>before, f"strong ISE formed connection edges ({before} -> {aftS})")
|
||||
check(aftW==aftS, f"weak ISE formed NO edges ({aftS} -> {aftW})")
|
||||
check(len(strong_edges)>=1, f"strong ISE has {len(strong_edges)} hebbian-associate edge(s) to wm_top")
|
||||
check(all('consolidated-from-ISE' in (e.get('metadata') or '') for e in strong_edges),
|
||||
"connection edges are provenance-tagged consolidated-from-ISE (reversible)")
|
||||
check(len(weak_edges)==0, "weak ISE (below connection bar) has zero outgoing edges")
|
||||
# targets must be the WM-top nodes (hebb-a / hebb-b), not distractors
|
||||
tgt_labels=set()
|
||||
byid={n["id"]:n for n in g["nodes"]}
|
||||
for e in strong_edges:
|
||||
t=byid.get(e["to_id"]);
|
||||
if t: tgt_labels.add(t.get("label"))
|
||||
print(f" connection targets: {sorted(tgt_labels)}")
|
||||
check(tgt_labels.issubset({"hebb-a","hebb-b"}) and len(tgt_labels)>=1,
|
||||
f"connections point at the wm_top nodes {sorted(tgt_labels)}")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== (c) PERMANENCE threshold: promoted node survives 48h prune, ephemeral is swept (flag ON) =="
|
||||
D="$WORK/c"; mkdir -p "$D"
|
||||
( export ENGRAM_CONSOLIDATION=1 ENGRAM_CONSOL_PERM_MIN=-1000; "$WORK/p1" perm "$D" ) >"$WORK/perm.txt" 2>&1 || { echo "FAIL perm run"; fail=1; }
|
||||
cat "$WORK/perm.txt" | sed 's/^/ /'
|
||||
python3 - "$WORK/perm.txt" <<'PY'
|
||||
import sys,re,json
|
||||
txt=open(sys.argv[1]).read()
|
||||
rc=0
|
||||
def check(c,m):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+m)
|
||||
if not c: rc=1
|
||||
prom=int(re.search(r'PROMOTED (\d+)',txt).group(1))
|
||||
m=re.search(r'NODES before=(\d+) after=(\d+) removed=(\d+)',txt)
|
||||
before,after,removed=int(m.group(1)),int(m.group(2)),int(m.group(3))
|
||||
dur=re.search(r'DURABLE_NODE (\{.*\})',txt).group(1)
|
||||
eph=re.search(r'EPHEMERAL_NODE (\{.*\})',txt).group(1)
|
||||
durj=json.loads(dur); ephj=json.loads(eph)
|
||||
check(prom==1, "engram_consolidate_permanence promoted the node (returned 1)")
|
||||
check(before==2 and after==1 and removed==1, f"exactly one node pruned ({before}->{after}, removed={removed})")
|
||||
check(durj.get("id")=="ise-durable", "durable node SURVIVED the 48h telemetry prune")
|
||||
check('consolidated-from-ISE' in (durj.get("metadata") or ''), "durable node carries reversible provenance marker")
|
||||
check(ephj=={} or not ephj.get("id"), "ephemeral (non-permanent) ISE was swept")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== (d) OFF path byte-identical: ISE creation forms no edges, permanence is a no-op =="
|
||||
D="$WORK/d"; mkdir -p "$D"
|
||||
( unset ENGRAM_CONSOLIDATION; "$WORK/p1" offcheck "$D" ) >"$WORK/off.txt" 2>&1
|
||||
rcoff=$?
|
||||
cat "$WORK/off.txt" | sed 's/^/ /'
|
||||
[ $rcoff -eq 0 ] && echo " PASS: flag OFF — ISE creation added 0 edges and permanence returned 0" \
|
||||
|| { echo " FAIL: OFF path changed behavior"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan (connect + perm + accrual-short) =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p1.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p1.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/san"; mkdir -p "$DS"
|
||||
( export ENGRAM_CONSOLIDATION=1 ENGRAM_CONSOL_PERM_MIN=-1000; "$WORK/p1.san" connect "$DS" ) >/dev/null 2>"$WORK/san_run.log"
|
||||
( export ENGRAM_CONSOLIDATION=1 ENGRAM_CONSOL_PERM_MIN=-1000; "$WORK/p1.san" perm "$DS" ) >/dev/null 2>>"$WORK/san_run.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san_run.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P1 CONSOLIDATION GATE: PASS ======"; else echo "====== P1 CONSOLIDATION GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P2 gate: chronoception (ENGRAM_CHRONOCEPTION).
|
||||
# Throwaway HOME + /tmp only. TC defaults to 3600s; we pin it for the math.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p2-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
export ENGRAM_CHRONO_TC=3600 # pin cooling time-constant for the math
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p2" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
sum_wm(){ python3 -c "import json,sys; g=json.load(open('$1')); print(sum(n.get('working_memory_weight',0) for n in g['nodes']))"; }
|
||||
|
||||
echo
|
||||
echo "== (a) cooling scales with dt (flag ON) =="
|
||||
for DT in 600000 1800000 3600000 7200000; do # 600s,1800s,3600s,7200s at TC=3600
|
||||
D="$WORK/dt$DT"; mkdir -p "$D"
|
||||
( export ENGRAM_CHRONOCEPTION=1; "$WORK/p2" once "$D" "$DT" ) >"$D/out.txt" 2>&1
|
||||
MAG=$(grep MAGNITUDE "$D/out.txt" | awk '{print $2}')
|
||||
WM=$(sum_wm "$D/field.json")
|
||||
PRED=$(python3 -c "import math; print(round(1-math.exp(-$DT/1000/3600),6))")
|
||||
echo " dt=${DT}ms magnitude=$MAG predicted 1-exp(-dt/TC)=$PRED field_wm_sum=$WM"
|
||||
python3 -c "import sys; m=float('$MAG'); p=float('$PRED'); sys.exit(0 if abs(m-p)<1e-4 else 1)" \
|
||||
&& echo " PASS: magnitude matches exp cooling" || { echo " FAIL"; fail=1; }
|
||||
done
|
||||
|
||||
echo
|
||||
echo "== (b) SCALE-INVARIANCE: age(dt) once == age(dt/N) N times (field within float tol) =="
|
||||
DT=3600000
|
||||
for N in 2 10 100; do
|
||||
DA="$WORK/inv_once_$N"; DB="$WORK/inv_split_$N"; mkdir -p "$DA" "$DB"
|
||||
( export ENGRAM_CHRONOCEPTION=1; "$WORK/p2" once "$DA" "$DT" ) >/dev/null 2>&1
|
||||
( export ENGRAM_CHRONOCEPTION=1; "$WORK/p2" split "$DB" "$DT" "$N" ) >/dev/null 2>&1
|
||||
WA=$(sum_wm "$DA/field.json"); WB=$(sum_wm "$DB/field.json")
|
||||
echo " N=$N once_wm=$WA split_wm=$WB |delta|=$(python3 -c "print(abs($WA-$WB))")"
|
||||
python3 -c "import sys; sys.exit(0 if abs($WA-$WB)<1e-9 else 1)" \
|
||||
&& echo " PASS: scale-invariant within 1e-9" || { echo " FAIL: not scale-invariant"; fail=1; }
|
||||
done
|
||||
|
||||
echo
|
||||
echo "== (c) REBOOT catch-up: one-shot cooling from persisted last-tick, reports MAGNITUDE not seconds =="
|
||||
D="$WORK/catch"; mkdir -p "$D"
|
||||
GAP=3600000 # 1h unconscious
|
||||
( export ENGRAM_CHRONOCEPTION=1 ENGRAM_DATA_DIR="$D"; "$WORK/p2" catchup "$D" "$GAP" ) >"$D/out.txt" 2>&1
|
||||
CMAG=$(grep CATCHUP_MAGNITUDE "$D/out.txt" | awk '{print $2}')
|
||||
CWM=$(sum_wm "$D/field.json")
|
||||
PRED=$(python3 -c "import math; print(round(1-math.exp(-$GAP/1000/3600),4))")
|
||||
echo " gap=${GAP}ms catchup_magnitude=$CMAG predicted=$PRED field_wm_sum=$CWM (was 0.6)"
|
||||
python3 -c "import sys; sys.exit(0 if abs(float('$CMAG')-float('$PRED'))<1e-2 else 1)" \
|
||||
&& echo " PASS: one-shot catch-up cooled by the elapsed gap, surfaced as a magnitude" \
|
||||
|| { echo " FAIL"; fail=1; }
|
||||
# honesty rail: magnitude is bounded [0,1), NOT an elapsed-seconds number
|
||||
python3 -c "import sys; m=float('$CMAG'); sys.exit(0 if 0<=m<1 else 1)" \
|
||||
&& echo " PASS: magnitude is a bounded drift signal in [0,1), never elapsed seconds" \
|
||||
|| { echo " FAIL: magnitude out of [0,1)"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== (d) OFF path: flag unset -> age & catchup return 0, field untouched =="
|
||||
D="$WORK/off"; mkdir -p "$D"
|
||||
( unset ENGRAM_CHRONOCEPTION; export ENGRAM_DATA_DIR="$D"; "$WORK/p2" offcheck "$D" 3600000 ) >"$D/out.txt" 2>&1
|
||||
cat "$D/out.txt" | sed 's/^/ /'
|
||||
OFFWM=$(sum_wm "$D/field.json")
|
||||
# loaded field wm sum = (1.0+0.8+0.6)*0.5 halving = 1.2 ; must be UNCHANGED
|
||||
echo " field_wm_sum=$OFFWM (expected 1.2, unchanged)"
|
||||
python3 -c "import sys; sys.exit(0 if abs($OFFWM-1.2)<1e-9 else 1)" \
|
||||
&& echo " PASS: OFF path leaves the field byte-identical (no aging)" \
|
||||
|| { echo " FAIL: OFF path modified the field"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p2.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p2.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/san"; mkdir -p "$DS"
|
||||
( export ENGRAM_CHRONOCEPTION=1 ENGRAM_DATA_DIR="$DS"; "$WORK/p2.san" once "$DS" 3600000 ) >/dev/null 2>"$WORK/san.log"
|
||||
( export ENGRAM_CHRONOCEPTION=1 ENGRAM_DATA_DIR="$DS"; "$WORK/p2.san" catchup "$DS" 3600000 ) >/dev/null 2>>"$WORK/san.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P2 CHRONOCEPTION GATE: PASS ======"; else echo "====== P2 CHRONOCEPTION GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P3 gate: drift-sensor primitive engram_geo_displacement.
|
||||
# Read-only pure primitive; no store, no flag. Throwaway /tmp only.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p3-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p3" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
"$WORK/p3" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||
cat "$WORK/out.txt" | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "== assertions =="
|
||||
python3 - "$WORK/out.txt" <<'PY'
|
||||
import sys,re
|
||||
rows={}
|
||||
for line in open(sys.argv[1]):
|
||||
m=re.match(r'(\w+) (.*)',line.strip())
|
||||
if not m: continue
|
||||
tag=m.group(1); kv=dict(re.findall(r'(\w+)=([-\d.]+)',m.group(2)))
|
||||
rows[tag]={k:float(v) for k,v in kv.items()}
|
||||
rc=0
|
||||
def check(c,msg):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+msg)
|
||||
if not c: rc=1
|
||||
g=rows["GROWTH"]; c=rows["CORRUPTION"]; i=rows["IDENTITY"]
|
||||
check(g["core_disp"]<0.05, f"GROWTH: core displacement ~0 (core fixed) = {g['core_disp']}")
|
||||
check(g["periph_disp"]>0.30, f"GROWTH: periphery extended = {g['periph_disp']}")
|
||||
check(g["centroid_sep"]<1e-6, f"GROWTH: centroid unmoved = {g['centroid_sep']}")
|
||||
check(abs(g["radius_delta"]-0.4)<1e-4, f"GROWTH: radius grew by ~0.4 = {g['radius_delta']}")
|
||||
check(c["core_disp"]>0.40, f"CORRUPTION: core displaced strongly = {c['core_disp']}")
|
||||
check(c["periph_disp"]<0.05, f"CORRUPTION: periphery fixed = {c['periph_disp']}")
|
||||
check(c["centroid_sep"]>0.1, f"CORRUPTION: centroid moved = {c['centroid_sep']}")
|
||||
check(c["core_disp"] > 8*g["core_disp"]+0.3,
|
||||
f"SENSOR DISCRIMINATES: corruption core_disp ({c['core_disp']}) >> growth core_disp ({g['core_disp']})")
|
||||
check(i["core_disp"]==0 and i["periph_disp"]==0 and i["centroid_sep"]<1e-6,
|
||||
"IDENTITY: A vs A -> zero drift")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p3.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p3.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
"$WORK/p3.san" >/dev/null 2>"$WORK/san.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P3 DRIFT-SENSOR GATE: PASS ======"; else echo "====== P3 DRIFT-SENSOR GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P4 gate: afferent input counters in act-stats (additive).
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p4-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p4" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
"$WORK/p4" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||
grep -oE 'aff_[a-z_]+":[0-9]+' "$WORK/out.txt" | sed 's/^/ /' | head -30
|
||||
|
||||
echo
|
||||
echo "== assertions =="
|
||||
python3 - "$WORK/out.txt" <<'PY'
|
||||
import sys,re,json
|
||||
S={}
|
||||
for line in open(sys.argv[1]):
|
||||
m=re.match(r'(STATS\d) (\{.*\})',line.strip())
|
||||
if m: S[m.group(1)]=json.loads(m.group(2))
|
||||
rc=0
|
||||
def check(c,msg):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+msg)
|
||||
if not c: rc=1
|
||||
s0,s1,s2=S["STATS0"],S["STATS1"],S["STATS2"]
|
||||
# after creation, before any query
|
||||
check(s0["aff_node_creates"]==5, f"node_creates==5 (got {s0['aff_node_creates']})")
|
||||
check(s0["aff_ise_ingests"]==2, f"ise_ingests==2 (got {s0['aff_ise_ingests']})")
|
||||
check(s0["aff_edge_creates"]==2, f"edge_creates==2 (got {s0['aff_edge_creates']})")
|
||||
check(s0["aff_queries"]==0 and s0["aff_activations"]==0, "queries/activations start at 0")
|
||||
# after 4 queries
|
||||
check(s1["aff_queries"]==4, f"queries==4 (got {s1['aff_queries']})")
|
||||
check(s1["aff_activations"]==4, f"activations==4 (got {s1['aff_activations']})")
|
||||
check(s1["aff_node_creates"]==5 and s1["aff_ise_ingests"]==2 and s1["aff_edge_creates"]==2,
|
||||
"create counters unchanged by queries")
|
||||
# after 3 more queries — monotonic
|
||||
check(s2["aff_queries"]==7, f"queries==7 monotonic (got {s2['aff_queries']})")
|
||||
check(s2["aff_activations"]==7, f"activations==7 monotonic (got {s2['aff_activations']})")
|
||||
check(s2["aff_queries"]>s1["aff_queries"]>s0["aff_queries"], "queries strictly monotonic across readings")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p4.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p4.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
"$WORK/p4.san" >/dev/null 2>"$WORK/san.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P4 AFFERENT-COUNTERS GATE: PASS ======"; else echo "====== P4 AFFERENT-COUNTERS GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M-INTEROCEPTION P5 gate: dream-recall builtin engram_dreams_json (honesty rail).
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-p5-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p5" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
|
||||
D="$WORK/d"; mkdir -p "$D"
|
||||
"$WORK/p5" "$D" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||
cat "$WORK/out.txt" | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "== assertions =="
|
||||
python3 - "$WORK/out.txt" <<'PY'
|
||||
import sys,re,json
|
||||
L={}
|
||||
for line in open(sys.argv[1]):
|
||||
line=line.strip()
|
||||
m=re.match(r'(BEFORE|AFTER) (\[.*\])',line)
|
||||
if m: L[m.group(1)]=json.loads(m.group(2)); continue
|
||||
m=re.match(r'PRUNED (\d+)',line)
|
||||
if m: L['PRUNED']=int(m.group(1)); continue
|
||||
m=re.match(r'SINCE (\d+) (\[.*\])',line)
|
||||
if m: L['SINCE']=json.loads(m.group(2))
|
||||
rc=0
|
||||
def check(c,msg):
|
||||
global rc; print((" PASS: " if c else " FAIL: ")+msg)
|
||||
if not c: rc=1
|
||||
before_ids={d["id"] for d in L["BEFORE"]}
|
||||
after_ids={d["id"] for d in L["AFTER"]}
|
||||
since_ids={d["id"] for d in L["SINCE"]}
|
||||
check(before_ids=={"cur_old","cur_mid","cur_recent"}, f"before prune: all 3 curiosity_scan, heartbeat excluded (got {sorted(before_ids)})")
|
||||
check("hb_recent" not in before_ids, "heartbeat ISE never appears (not a dream)")
|
||||
check(L["PRUNED"]==1, f"prune rotated out exactly the ancient ISE (pruned={L['PRUNED']})")
|
||||
check(after_ids=={"cur_mid","cur_recent"}, f"after prune: rotated-out cur_old is ABSENT, not confabulated (got {sorted(after_ids)})")
|
||||
check("cur_old" not in after_ids, "honesty rail: pruned dream is gone = 'I don't remember', never synthesized")
|
||||
check(since_ids=={"cur_recent"}, f"since filter returns only events after the cutoff (got {sorted(since_ids)})")
|
||||
# no fabrication: every returned id was one we seeded
|
||||
seeded={"cur_old","cur_mid","cur_recent","hb_recent"}
|
||||
allret=before_ids|after_ids|since_ids
|
||||
check(allret<=seeded, f"no fabricated entries — every returned id was seeded ({sorted(allret)})")
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p5.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
if [ -x "$WORK/p5.san" ]; then
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/ds"; mkdir -p "$DS"
|
||||
"$WORK/p5.san" "$DS" >/dev/null 2>"$WORK/san.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1
|
||||
else echo " ok: ASan+UBSan clean"; fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "====== P5 DREAM-RECALL GATE: PASS ======"; else echo "====== P5 DREAM-RECALL GATE: FAIL ======"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,158 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M3.5 PRE-FLIP GATE. Pure C harness (NOT elb/elc): links the real el_runtime.c
|
||||
# native engram builtins + engram_store.c and proves activation-time field
|
||||
# mutations (edge hebb, node activation_count, WM weight) persist through a
|
||||
# checkpoint and survive a reboot from neuron.egm with snapshot.json DELETED.
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-m35-XXXXXX)"
|
||||
BIN="$WORK/m35"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME" # never touch real ~/.neuron
|
||||
export ENGRAM_WAL_SYNC=always
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m35_hebb_persist.c) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$BIN" 2>"$WORK/cc.log"
|
||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
||||
|
||||
echo
|
||||
echo "== 0) flag-OFF: seed+activate+checkpoint must NOT touch the store =="
|
||||
DOFF="$WORK/off"; mkdir -p "$DOFF"
|
||||
( unset ENGRAM_STORE; "$BIN" offcheck "$DOFF" )
|
||||
[ $? -ne 0 ] && { echo "FAIL: offcheck"; fail=1; }
|
||||
[ -e "$DOFF/neuron.egm" ] && { echo "FAIL: neuron.egm created while flag OFF"; fail=1; } \
|
||||
|| echo " ok: no neuron.egm created with flag OFF"
|
||||
|
||||
echo
|
||||
echo "== 1) POSITIVE: ENGRAM_STORE=1 seed -> activate -> checkpoint(field-persist) -> close =="
|
||||
DPOS="$WORK/pos"; mkdir -p "$DPOS"
|
||||
ENGRAM_STORE=1 "$BIN" pos_seed "$DPOS" || { echo "FAIL: pos_seed"; fail=1; }
|
||||
[ -e "$DPOS/neuron.egm" ] && echo " ok: neuron.egm created" || { echo "FAIL: neuron.egm missing"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 2) reboot from neuron.egm with snapshot.json DELETED (must never read JSON) =="
|
||||
rm -f "$DPOS/snapshot.json"
|
||||
ENGRAM_STORE=1 "$BIN" pos_reboot "$DPOS" || { echo "FAIL: pos_reboot"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 3) NEGATIVE CONTROL: seed -> activate -> close WITHOUT the field-persist checkpoint =="
|
||||
DNEG="$WORK/neg"; mkdir -p "$DNEG"
|
||||
ENGRAM_STORE=1 "$BIN" neg_seed "$DNEG" || { echo "FAIL: neg_seed"; fail=1; }
|
||||
rm -f "$DNEG/snapshot.json"
|
||||
ENGRAM_STORE=1 "$BIN" neg_reboot "$DNEG" || { echo "FAIL: neg_reboot"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 4) assertions (python over the JSON exports) =="
|
||||
python3 - "$DPOS" "$DNEG" <<'PY'
|
||||
import json, sys, os
|
||||
WM_FLOOR = 0.05
|
||||
HEBB_MIN = 1e-6
|
||||
|
||||
def load(d, name):
|
||||
with open(os.path.join(d, name)) as f: return json.load(f)
|
||||
|
||||
def node_by_label(g, label):
|
||||
for n in g["nodes"]:
|
||||
if n.get("label") == label: return n
|
||||
return None
|
||||
|
||||
def edge_between(g, a_id, b_id):
|
||||
for e in g["edges"]:
|
||||
if e.get("from_id") == a_id and e.get("to_id") == b_id:
|
||||
return e
|
||||
return None
|
||||
|
||||
rc = 0
|
||||
def check(cond, msg):
|
||||
global rc
|
||||
if cond: print(f" PASS: {msg}")
|
||||
else: print(f" FAIL: {msg}"); rc = 1
|
||||
|
||||
dpos, dneg = sys.argv[1], sys.argv[2]
|
||||
pre = load(dpos, "pre_reboot.json")
|
||||
rebt = load(dpos, "reboot.json")
|
||||
|
||||
pa, pb = node_by_label(pre, "hebb-a"), node_by_label(pre, "hebb-b")
|
||||
ra = node_by_label(rebt, "hebb-a")
|
||||
assert pa and pb and ra, "target nodes missing"
|
||||
pe = edge_between(pre, pa["id"], pb["id"])
|
||||
re = edge_between(rebt, pa["id"], pb["id"])
|
||||
assert pe and re, "target edge missing"
|
||||
|
||||
pre_hebb = pe.get("hebb", 0.0)
|
||||
rebt_hebb = re.get("hebb", 0.0)
|
||||
pre_ac = pa.get("activation_count", 0)
|
||||
rebt_ac = ra.get("activation_count", 0)
|
||||
pre_wm = pa.get("working_memory_weight", 0.0)
|
||||
rebt_wm = ra.get("working_memory_weight", 0.0)
|
||||
|
||||
print(f" edge hebb-a->hebb-b : pre={pre_hebb!r} reboot={rebt_hebb!r}")
|
||||
print(f" node hebb-a act_cnt : pre={pre_ac!r} reboot={rebt_ac!r}")
|
||||
print(f" node hebb-a wm : pre={pre_wm!r} reboot={rebt_wm!r} (halved+floored expected)")
|
||||
|
||||
# --- learning actually happened this run (else the test proves nothing) ---
|
||||
check(pre_hebb > HEBB_MIN, f"activation raised edge hebb above 0 (pre={pre_hebb})")
|
||||
check(pre_ac >= 1, f"activation reinforced node activation_count (pre={pre_ac})")
|
||||
check(pre_wm > 0.0, f"activation promoted node to working memory (pre_wm={pre_wm})")
|
||||
|
||||
# --- the load-bearing survival assertions after a real delete-JSON reboot ---
|
||||
check(abs(rebt_hebb - pre_hebb) < 1e-12,
|
||||
f"edge hebb SURVIVED reboot unchanged ({rebt_hebb} == {pre_hebb})")
|
||||
check(rebt_ac == pre_ac,
|
||||
f"node activation_count SURVIVED reboot unchanged ({rebt_ac} == {pre_ac})")
|
||||
|
||||
# --- WM weight: must equal the JSON path's boot transform exactly (halve+floor) ---
|
||||
expected_wm = pre_wm * 0.5
|
||||
if expected_wm < WM_FLOOR: expected_wm = 0.0
|
||||
check(abs(rebt_wm - expected_wm) < 1e-9,
|
||||
f"node WM weight SURVIVED with the SAME boot transform as JSON path "
|
||||
f"(reboot={rebt_wm} == halve+floor(pre)={expected_wm})")
|
||||
check(expected_wm > 0.0,
|
||||
f"WM survival is observable (halved weight stays above floor: {expected_wm} > {WM_FLOOR})")
|
||||
|
||||
# --- NEGATIVE CONTROL: without the field-persist step the learning is LOST ---
|
||||
npre = load(dneg, "neg_pre.json")
|
||||
nrebt = load(dneg, "neg_reboot.json")
|
||||
na_pre = node_by_label(npre, "hebb-a")
|
||||
na_rebt = node_by_label(nrebt, "hebb-a")
|
||||
ne_pre = edge_between(npre, na_pre["id"], node_by_label(npre, "hebb-b")["id"])
|
||||
ne_rebt = edge_between(nrebt, na_rebt["id"], node_by_label(nrebt, "hebb-b")["id"])
|
||||
print(f" [neg] edge hebb : pre={ne_pre.get('hebb',0.0)!r} reboot={ne_rebt.get('hebb',0.0)!r}")
|
||||
print(f" [neg] node act_cnt : pre={na_pre.get('activation_count',0)!r} reboot={na_rebt.get('activation_count',0)!r}")
|
||||
check(ne_pre.get("hebb", 0.0) > HEBB_MIN,
|
||||
f"[neg] activation DID raise hebb in RAM (pre={ne_pre.get('hebb',0.0)})")
|
||||
check(ne_rebt.get("hebb", 0.0) == 0.0,
|
||||
"[neg] WITHOUT checkpoint field-persist, edge hebb is LOST on reboot (==0) — fix is load-bearing")
|
||||
check(na_rebt.get("activation_count", 0) == 0,
|
||||
"[neg] WITHOUT checkpoint field-persist, activation_count is LOST on reboot (==0)")
|
||||
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== 5) ASan+UBSan build, exercise the full persist+reboot flow (leaks off — harness intentionally leaks el_strdup) =="
|
||||
SANBIN="$WORK/m35.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DSAN="$WORK/san"; mkdir -p "$DSAN"
|
||||
ENGRAM_STORE=1 "$SANBIN" pos_seed "$DSAN" >/dev/null 2>"$WORK/san_run.log" && \
|
||||
{ rm -f "$DSAN/snapshot.json"; ENGRAM_STORE=1 "$SANBIN" pos_reboot "$DSAN" >/dev/null 2>>"$WORK/san_run.log"; }
|
||||
if grep -qiE 'runtime error|AddressSanitizer|UndefinedBehavior|ERROR: ' "$WORK/san_run.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
|
||||
else
|
||||
echo " ok: ASan+UBSan clean across pos_seed/checkpoint/reboot (field-persist, boot laundering)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "================ M3.5 HEBB-PERSIST GATE: PASS ================"; else echo "================ M3.5 HEBB-PERSIST GATE: FAIL ================"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M3 JSON-parity gate. Pure C harness (NOT elb/elc): links the real el_runtime.c
|
||||
# native engram builtins + engram_store.c and drives ENGRAM_STORE on vs off.
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME + ENGRAM_DATA_DIR.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-m3-XXXXXX)"
|
||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||
BIN="$WORK/m3"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME" # never touch real ~/.neuron
|
||||
export ENGRAM_DATA_DIR="$DATA"
|
||||
export ENGRAM_WAL_SYNC=always
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m3_parity.c) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m3_parity.c" "$RT" "$ST" -lcurl -o "$BIN" 2>"$WORK/cc.log"
|
||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
||||
grep -i warning "$WORK/cc.log" | grep -iE 'engram_store|eg_store|eg_load|scan_nodes|scan_edges' && echo "(warnings in M3 code above)" || true
|
||||
|
||||
echo
|
||||
echo "== 0) default-OFF: flag unset leaves the store untouched =="
|
||||
( unset ENGRAM_STORE; "$BIN" offcheck "$DATA" )
|
||||
[ $? -ne 0 ] && { echo "FAIL: offcheck"; fail=1; }
|
||||
[ -e "$DATA/neuron.egm" ] && { echo "FAIL: neuron.egm created while flag OFF"; fail=1; } \
|
||||
|| echo " ok: no neuron.egm created with flag OFF"
|
||||
|
||||
echo
|
||||
echo "== 1) seed (ENGRAM_STORE unset): build graph, save snapshot.json, activate =="
|
||||
( unset ENGRAM_STORE; "$BIN" seed "$DATA" ) || { echo "FAIL: seed"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 2) on (ENGRAM_STORE=1): import snapshot.json ONCE -> neuron.egm, resident-load, activate =="
|
||||
ENGRAM_STORE=1 "$BIN" on "$DATA" || { echo "FAIL: on"; fail=1; }
|
||||
[ -e "$DATA/neuron.egm" ] && echo " ok: neuron.egm created by import" || { echo "FAIL: neuron.egm missing"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 3) reboot (ENGRAM_STORE=1, snapshot.json DELETED): must load from neuron.egm, never JSON =="
|
||||
rm -f "$DATA/snapshot.json"
|
||||
ENGRAM_STORE=1 "$BIN" reboot "$DATA" || { echo "FAIL: reboot"; fail=1; }
|
||||
|
||||
echo
|
||||
echo "== 4) parity comparison (modulo ordering) =="
|
||||
python3 - "$DATA" <<'PY'
|
||||
import json, sys, os
|
||||
d = sys.argv[1]
|
||||
def load(name):
|
||||
with open(os.path.join(d, name)) as f: return json.load(f)
|
||||
def norm_graph(g):
|
||||
nodes = sorted(g.get("nodes", []), key=lambda n: n.get("id",""))
|
||||
edges = sorted(g.get("edges", []), key=lambda e: e.get("id",""))
|
||||
layers= sorted(g.get("layers", []), key=lambda l: l.get("layer_id",0))
|
||||
return {"nodes":nodes, "edges":edges, "layers":layers}
|
||||
def act_ids(a):
|
||||
# list of (node id, promoted); robust set + ordered list
|
||||
seq = [(e.get("node",{}).get("id",""), int(e.get("promoted",0))) for e in a]
|
||||
return seq
|
||||
|
||||
rc = 0
|
||||
snap = norm_graph(load("snapshot.json") if os.path.exists(os.path.join(d,"snapshot.json")) else load("off_graph.json"))
|
||||
off = norm_graph(load("off_graph.json"))
|
||||
on = norm_graph(load("on_graph.json"))
|
||||
rebt = norm_graph(load("reboot_graph.json"))
|
||||
|
||||
def cmp(label, a, b):
|
||||
global rc
|
||||
if a == b:
|
||||
print(f" PASS: {label} (nodes={len(a['nodes'])} edges={len(a['edges'])} layers={len(a['layers'])})")
|
||||
else:
|
||||
rc = 1
|
||||
print(f" FAIL: {label}")
|
||||
for k in ("nodes","edges","layers"):
|
||||
if a[k] != b[k]:
|
||||
print(f" {k}: {len(a[k])} vs {len(b[k])}")
|
||||
for x,y in zip(a[k], b[k]):
|
||||
if x != y:
|
||||
print(f" first diff:\n A={json.dumps(x)[:300]}\n B={json.dumps(y)[:300]}")
|
||||
break
|
||||
|
||||
cmp("graph: ENGRAM_STORE=1 (export) == ENGRAM_STORE=0 (JSON path)", on, off)
|
||||
cmp("round-trip: snapshot.json seed == store export (on_graph)", on, off) # off_graph==snapshot save
|
||||
cmp("reboot from neuron.egm (no JSON) == on-path store", rebt, on)
|
||||
|
||||
offa = act_ids(load("off_act.json"))
|
||||
ona = act_ids(load("on_act.json"))
|
||||
if set(offa) == set(ona):
|
||||
print(f" PASS: activation result set identical (off={len(offa)} on={len(ona)} entries)")
|
||||
if offa == ona:
|
||||
print(" (and identical ordering/promotion sequence)")
|
||||
else:
|
||||
print(" (same set; ordering differs only where scores tie — reporting honestly)")
|
||||
else:
|
||||
rc = 1
|
||||
print(" FAIL: activation result set differs")
|
||||
print(f" off-only: {set(offa)-set(ona)}")
|
||||
print(f" on-only: {set(ona)-set(offa)}")
|
||||
|
||||
sys.exit(rc)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== 5) ASan+UBSan build, exercise M3 scan/boot/hooks (leaks off — harness intentionally leaks el_strdup) =="
|
||||
SANBIN="$WORK/m3.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m3_parity.c" "$RT" "$ST" -lcurl -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DATA2="$WORK/data2"; mkdir -p "$DATA2"
|
||||
( unset ENGRAM_STORE; "$SANBIN" seed "$DATA2" ) >/dev/null 2>"$WORK/san_run.log" && \
|
||||
ENGRAM_STORE=1 "$SANBIN" on "$DATA2" >/dev/null 2>>"$WORK/san_run.log" && \
|
||||
{ rm -f "$DATA2/snapshot.json"; ENGRAM_STORE=1 "$SANBIN" reboot "$DATA2" >/dev/null 2>>"$WORK/san_run.log"; }
|
||||
if grep -qiE 'runtime error|AddressSanitizer|UndefinedBehavior|ERROR: ' "$WORK/san_run.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
|
||||
else
|
||||
echo " ok: ASan+UBSan clean across seed/on/reboot (scan, boot, resident-load, mutation hooks)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "================ M3 PARITY GATE: PASS ================"; else echo "================ M3 PARITY GATE: FAIL ================"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,137 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M7 index-driven-traversal gate. Pure C harness (NOT elb/elc): links the real
|
||||
# el_runtime.c engram builtins + engram_store.c and drives ENGRAM_STORE off vs on.
|
||||
# Proves (1) byte-identical activation parity flag-on == flag-off across a
|
||||
# mutating query sequence, and (2) the O(E)-rebuild cost is eliminated flag-on.
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
INC="$HERE/../../lang/runtime"
|
||||
WORK="$(mktemp -d /tmp/engram-m7-XXXXXX)"
|
||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||
BIN="$WORK/m7"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME" # never touch real ~/.neuron
|
||||
# Hermetic: point the embedder at a guaranteed-refused endpoint so eg_embed_fetch
|
||||
# fails fast, the circuit breaker opens, and cosq is deterministically absent in
|
||||
# EVERY run (no dependence on whether a dev Ollama happens to be listening). This
|
||||
# makes the byte-identical parity comparison reproducible and non-flaky.
|
||||
export EL_EMBED_URL="http://127.0.0.1:1/api/embeddings"
|
||||
unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m7_traversal.c) =="
|
||||
gcc -O2 -std=c11 -I "$INC" "$HERE/test_m7_traversal.c" "$RT" "$ST" -lcurl -lm -o "$BIN" 2>"$WORK/cc.log"
|
||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
||||
echo " ok: compiled"
|
||||
|
||||
echo
|
||||
echo "== 1) PARITY: index-driven (M7 incremental) activation must be IDENTICAL to the"
|
||||
echo " full-rebuild scan path — proven under one identical ENGRAM_STORE=1 state,"
|
||||
echo " so the ONLY variable is how per-node adjacency is maintained."
|
||||
echo " (compared on deterministic fields: node label + activation_strength +"
|
||||
echo " working_memory_weight + epistemic_confidence + hops + promoted, IN ORDER;"
|
||||
echo " node id/timestamps are per-run random and are intentionally excluded.)"
|
||||
( unset ENGRAM_STORE; "$BIN" parity-off "$DATA" ) || { echo "FAIL: parity-off run"; fail=1; }
|
||||
ENGRAM_STORE=1 "$BIN" parity-on-rebuild "$DATA" || { echo "FAIL: parity-on-rebuild run"; fail=1; }
|
||||
ENGRAM_STORE=1 "$BIN" parity-on-incr "$DATA" || { echo "FAIL: parity-on-incr run"; fail=1; }
|
||||
python3 - "$DATA" <<'PY' || fail=1
|
||||
import json, sys, os
|
||||
d = sys.argv[1]
|
||||
def proj(prefix, i):
|
||||
a = json.load(open(os.path.join(d, f"{prefix}_act{i}.json")))
|
||||
out = []
|
||||
for e in a:
|
||||
n = e.get("node", {})
|
||||
out.append([n.get("label",""),
|
||||
e.get("activation_strength"), e.get("working_memory_weight"),
|
||||
e.get("epistemic_confidence"), e.get("hops"), e.get("promoted")])
|
||||
return out
|
||||
def compare(label, pa, pb, gate):
|
||||
rc = 0
|
||||
for i in (1,2,3,4):
|
||||
a, b = proj(pa, i), proj(pb, i)
|
||||
if a == b:
|
||||
print(f" #{i} identical (entries={len(a)}, promoted={sum(1 for r in a if r[5])})")
|
||||
else:
|
||||
if gate: rc = 1
|
||||
print(f" #{i} DIFFERS ({'FAIL' if gate else 'note'})")
|
||||
for x,y in zip(a,b):
|
||||
if x != y:
|
||||
print(f" first diff:\n {pa}={x}\n {pb}={y}"); break
|
||||
if len(a) != len(b): print(f" length: {pa}={len(a)} {pb}={len(b)}")
|
||||
print(f" {'PASS' if rc==0 else 'FAIL'}: {label}")
|
||||
return rc
|
||||
|
||||
print(" [CORE M7 GATE] flag-on incremental index == flag-on forced full rebuild:")
|
||||
rc1 = compare("index-driven activation == full-rebuild scan (same flag state)",
|
||||
"onincr", "onrb", gate=True)
|
||||
print(" [context] flag-on incremental index vs flag-off scan path (today's behavior):")
|
||||
rc2 = compare("M7 (flag-on) == flag-off scan path", "onincr", "off", gate=False)
|
||||
print(" [context] flag-off scan vs flag-on forced rebuild (isolates any pre-existing")
|
||||
print(" flag-on/off float difference, INDEPENDENT of M7's incremental path):")
|
||||
rc3 = compare("flag-off == flag-on (both rebuild path)", "off", "onrb", gate=False)
|
||||
sys.exit(rc1) # only the core M7 equivalence gates the result
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== 2) PERF: ~13k nodes / 43k edges, 200 (add-edge + activate) iterations =="
|
||||
NODES=13000; EDGES=43000; ITERS=120
|
||||
( unset ENGRAM_STORE; "$BIN" perf off "$DATA" "$NODES" "$EDGES" "$ITERS" ) | tee "$WORK/perf_off.txt"
|
||||
[ ${PIPESTATUS[0]} -ne 0 ] && { echo "FAIL: perf off"; fail=1; }
|
||||
ENGRAM_STORE=1 "$BIN" perf on "$DATA" "$NODES" "$EDGES" "$ITERS" | tee "$WORK/perf_on.txt"
|
||||
[ ${PIPESTATUS[0]} -ne 0 ] && { echo "FAIL: perf on"; fail=1; }
|
||||
python3 - "$WORK/perf_off.txt" "$WORK/perf_on.txt" <<'PY'
|
||||
import re, sys
|
||||
def parse(f):
|
||||
t = open(f).read()
|
||||
def g(k):
|
||||
m = re.search(k+r'=([\d.]+)', t); return float(m.group(1)) if m else 0.0
|
||||
return {'rw': g('rebuild_edge_work'), 'rb': g('rebuilds'), 'ap': g('incr_appends'),
|
||||
'loop_s': g('loop='), 'maint': g('adj_maint'),
|
||||
'perq': g('per_query')}
|
||||
off, on = parse(sys.argv[1]), parse(sys.argv[2])
|
||||
def ratio(a,b): return (a/b) if b else float('inf')
|
||||
print()
|
||||
print(f" ADJACENCY TRAVERSAL COST (the metric M7 changes):")
|
||||
print(f" edge-touches in rebuilds: off={off['rw']:.0f} on={on['rw']:.0f} "
|
||||
f"({ratio(off['rw'],on['rw']):.0f}x fewer on)")
|
||||
print(f" full O(E) rebuilds: off={off['rb']:.0f} on={on['rb']:.0f}")
|
||||
print(f" incremental O(1) appends: off={off['ap']:.0f} on={on['ap']:.0f}")
|
||||
print(f" adjacency-maint wall-time: off={off['maint']:.4f}s on={on['maint']:.4f}s "
|
||||
f"({ratio(off['maint'],on['maint']):.1f}x faster on)")
|
||||
print(f" END-TO-END per-query time: off={off['perq']:.2f}ms on={on['perq']:.2f}ms")
|
||||
print(f" (per-query is dominated by activation's O(N) node scoring over 13k nodes,")
|
||||
print(f" which M7 does not touch; the delta is the eliminated rebuild time.)")
|
||||
ok = on['rw'] < off['rw'] and on['maint'] < off['maint'] and on['rb'] < off['rb']
|
||||
print(" PASS: flag-on eliminates the O(E) per-query rebuild (fewer edge-touches, less maint time)"
|
||||
if ok else " FAIL: expected fewer edge-touches AND less adjacency-maint time on flag-on")
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
[ $? -ne 0 ] && fail=1
|
||||
|
||||
echo
|
||||
echo "== 3) ASan+UBSan clean across parity + a small perf loop (leaks off — harness intentionally leaks el_strdup) =="
|
||||
SANBIN="$WORK/m7.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m7_traversal.c" "$RT" "$ST" -lcurl -lm -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
D2="$WORK/data2"; mkdir -p "$D2"
|
||||
( unset ENGRAM_STORE; "$SANBIN" parity-off "$D2" ) >/dev/null 2>"$WORK/san_run.log" && \
|
||||
ENGRAM_STORE=1 "$SANBIN" parity-on-rebuild "$D2" >/dev/null 2>>"$WORK/san_run.log" && \
|
||||
ENGRAM_STORE=1 "$SANBIN" parity-on-incr "$D2" >/dev/null 2>>"$WORK/san_run.log" && \
|
||||
( unset ENGRAM_STORE; "$SANBIN" perf off "$D2" 1500 5000 40 ) >/dev/null 2>>"$WORK/san_run.log" && \
|
||||
ENGRAM_STORE=1 "$SANBIN" perf on "$D2" 1500 5000 40 >/dev/null 2>>"$WORK/san_run.log"
|
||||
if grep -qiE 'runtime error|AddressSanitizer|UndefinedBehavior|ERROR: ' "$WORK/san_run.log"; then
|
||||
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
|
||||
else
|
||||
echo " ok: ASan+UBSan clean across parity + perf (rebuild + incremental append + BFS)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -eq 0 ]; then echo "================ M7 TRAVERSAL GATE: PASS ================"; else echo "================ M7 TRAVERSAL GATE: FAIL ================"; fi
|
||||
rm -rf "$WORK"
|
||||
exit $fail
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the REASONING-layer tests (engram_reason.c): closed-form constructed
|
||||
# cases for ANALOGY / INDUCTION / ABDUCTION / CAUSAL / PLANNING, each composing the
|
||||
# §5 geometry OPERATORS (engram_geometry.c). Pure C11 (stdlib + libm). Standalone —
|
||||
# NOT folded through elc. Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
|
||||
# 2. SAFETY — ASan + UBSan on the same suite (memory-safety is size-independent).
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_reason.c $RT/engram_reason.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -o "$TMP/safe"
|
||||
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M1 paged-store gate. Pure C (NOT elb/elc). Writes only under /tmp.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SRC="$HERE/../../lang/runtime/engram_store.c"
|
||||
BIN="/tmp/test_store.$$"
|
||||
echo "compiling: gcc test_store.c engram_store.c"
|
||||
gcc -O2 -Wall -Wextra -std=c11 "$HERE/test_store.c" "$SRC" -o "$BIN"
|
||||
"$BIN"
|
||||
rc=$?
|
||||
rm -f "$BIN"
|
||||
rm -rf /tmp/engram-store-test-*
|
||||
exit $rc
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the VERIFIER-layer tests (engram_verify.c): closed-form constructed
|
||||
# cases for GROUNDING (anti-hallucination) and CONSISTENCY (polarity/negation
|
||||
# inversion + geometric contradiction), each composing the reasoning point-fit
|
||||
# (engram_reason.c) and the §5 geometry OPERATORS (engram_geometry.c). Pure C11
|
||||
# (stdlib + libm). Standalone — NOT folded through elc. Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
|
||||
# 2. SAFETY — ASan + UBSan on the same suite (memory-safety is size-independent).
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_verify.c $RT/engram_verify.c $RT/engram_reason.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -o "$TMP/safe"
|
||||
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the M8 HNSW vector-index tests. Pure C11 (gcc/cc), stdlib + libm
|
||||
# only. This is a standalone C module — NOT folded through elb/elc.
|
||||
#
|
||||
# Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the real recall@10 gate + speedup
|
||||
# numbers at full size (N=5000 recall, N=5000/20000 speedup).
|
||||
# 2. SAFETY — ASan + UBSan on the same suite at reduced size (VINDEX_QUICK=1);
|
||||
# memory-safety is size-independent, so this stays fast.
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_vindex.c $RT/engram_vindex.c $RT/engram_store.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — recall gate + speedup"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan, reduced size)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -o "$TMP/safe"
|
||||
VINDEX_QUICK=1 ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# M2 WAL + checkpoint + recovery gate. Pure C (NOT elb/elc). Writes only under /tmp.
|
||||
# Recovery tests use ENGRAM_WAL_SYNC=always so every WAL record is durable at crash.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SRC="$HERE/../../lang/runtime/engram_store.c"
|
||||
BIN="/tmp/test_wal_store.$$"
|
||||
echo "compiling: gcc test_wal_store.c engram_store.c"
|
||||
gcc -O2 -Wall -Wextra -std=c11 "$HERE/test_wal_store.c" "$SRC" -o "$BIN"
|
||||
ENGRAM_WAL_SYNC=always "$BIN"
|
||||
rc=$?
|
||||
rm -f "$BIN"
|
||||
rm -rf /tmp/engram-wal-test-*
|
||||
exit $rc
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# WAL unit + integration + crash-fuzz gate. Throwaway HOME/dirs only.
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
REL="$HERE/../../lang/runtime"
|
||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" \
|
||||
"$HERE/test_wal.c" -lcurl -lpthread -o /tmp/test_wal
|
||||
HOME=/tmp/engram-throwaway-home /tmp/test_wal
|
||||
# Fail-loud data-dir check (must exit 1 with a FATAL line):
|
||||
cat > /tmp/test_failloud.c <<'C'
|
||||
#include "el_runtime.c"
|
||||
int main(void){ unsetenv("ENGRAM_DATA_DIR"); unsetenv("HOME");
|
||||
engram_resolve_data_dir(); printf("REACHED\n"); return 0; }
|
||||
C
|
||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" /tmp/test_failloud.c -lcurl -lpthread -o /tmp/test_failloud
|
||||
if env -u HOME -u ENGRAM_DATA_DIR /tmp/test_failloud; then echo "FAIL: should have exited"; exit 1; else echo "[PASS] fail-loud exit on unresolvable HOME"; fi
|
||||
@@ -1,496 +0,0 @@
|
||||
/* test_bufpool.c — M4 gate for the demand-paging BUFFER POOL (engram_store.{c,h}).
|
||||
*
|
||||
* Pure C. Build: gcc -O2 test_bufpool.c ../../lang/runtime/engram_store.c -o t
|
||||
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
|
||||
*
|
||||
* Proves the M4 pool preserves every M1/M2 invariant when the pool is SMALLER
|
||||
* than the store (pages evict + re-fault): small-pool round-trip correctness,
|
||||
* LRU eviction policy (hot resident / cold evicted / no dirty stolen), pinned
|
||||
* residency (superblocks, index roots, explicit page + hot-layer pins), bounded
|
||||
* read-ahead, and crash safety (WAL replay + checkpoint-crash) under paging.
|
||||
*/
|
||||
#include "../../lang/runtime/engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond){
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_dir[512];
|
||||
static void mk_dir(void){
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/engram-bufpool-test-%d", (int)getpid());
|
||||
mkdir(g_dir, 0700);
|
||||
}
|
||||
static void path_in(char* out, size_t cap, const char* name){
|
||||
snprintf(out, cap, "%s/%s", g_dir, name);
|
||||
}
|
||||
|
||||
/* ── deterministic generators (bit-exact regeneration for oracles) ─────────── */
|
||||
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
|
||||
static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); }
|
||||
static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); }
|
||||
static char* rnd_str(uint64_t* st, size_t len){
|
||||
char* s = (char*)malloc(len + 1);
|
||||
for (size_t i=0;i<len;i++) s[i] = (char)(33 + (xs(st) % 94));
|
||||
s[len] = 0; return s;
|
||||
}
|
||||
|
||||
#define NODE_COUNT 5000
|
||||
#define EDGE_COUNT 20000
|
||||
#define EMB_DIM 768
|
||||
#define CK_NODES 300
|
||||
|
||||
static void noop_node_cb(const StoreNode* n, void* ctx){ (void)n; (void)ctx; }
|
||||
|
||||
static void gen_node(int i, StoreNode* n){
|
||||
memset(n, 0, sizeof *n);
|
||||
uint64_t st = node_seed(i);
|
||||
char id[32]; snprintf(id, sizeof id, "node-%d", i);
|
||||
n->id = strdup(id);
|
||||
size_t clen = (i % 500 == 0) ? (size_t)(17000 + (xs(&st) % 6000)) : (size_t)(xs(&st) % 300);
|
||||
n->content = rnd_str(&st, clen);
|
||||
n->node_type = rnd_str(&st, 4 + (xs(&st) % 8));
|
||||
n->label = (i % 2) ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL;
|
||||
n->tier = rnd_str(&st, 4 + (xs(&st) % 6));
|
||||
n->tags = rnd_str(&st, xs(&st) % 40);
|
||||
n->metadata = (i % 3) ? rnd_str(&st, xs(&st) % 60) : NULL;
|
||||
n->salience = (double)(xs(&st) % 1000000) / 997.0;
|
||||
n->importance = (double)(xs(&st) % 1000000) / 131.0;
|
||||
n->confidence = (double)(xs(&st) % 1000000) / 733.0;
|
||||
n->temporal_decay_rate = (double)(xs(&st) % 1000000) / 101.0;
|
||||
n->activation_count = (int64_t)(xs(&st) % 100000);
|
||||
n->last_activated = (int64_t)xs(&st);
|
||||
n->created_at = (int64_t)(1600000000000LL + i);
|
||||
n->updated_at = (int64_t)xs(&st);
|
||||
n->background_activation = (double)(xs(&st) % 1000000) / 17.0;
|
||||
n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0;
|
||||
n->suppression_count = (int32_t)(xs(&st) % 50);
|
||||
n->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
for (int k=0;k<STORE_BLL_K;k++) n->access_ts[k] = (int64_t)xs(&st);
|
||||
n->access_head = (int32_t)(xs(&st) % STORE_BLL_K);
|
||||
n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1));
|
||||
n->wm_anchor = (double)(xs(&st) % 1000000) / 3.0;
|
||||
n->emb = (float*)malloc(EMB_DIM * sizeof(float));
|
||||
for (int k=0;k<EMB_DIM;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k], &u, 4); }
|
||||
n->emb_dim = EMB_DIM;
|
||||
}
|
||||
static void gen_edge(int i, StoreEdge* e){
|
||||
memset(e, 0, sizeof *e);
|
||||
uint64_t st = edge_seed(i);
|
||||
char id[32], from[32], to[32];
|
||||
snprintf(id, sizeof id, "edge-%d", i);
|
||||
snprintf(from, sizeof from, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
||||
snprintf(to, sizeof to, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
||||
e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
|
||||
e->relation = rnd_str(&st, 3 + (xs(&st) % 12));
|
||||
e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL;
|
||||
e->weight = (double)(xs(&st) % 1000000) / 111.0;
|
||||
e->hebb = (double)(xs(&st) % 1000000) / 1000000.0;
|
||||
e->confidence = (double)(xs(&st) % 1000000) / 777.0;
|
||||
e->created_at = (int64_t)(1600000000000LL + i);
|
||||
e->updated_at = (int64_t)xs(&st);
|
||||
e->last_fired = (int64_t)xs(&st);
|
||||
e->inhibitory = (int32_t)(xs(&st) % 2);
|
||||
e->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
}
|
||||
static int streq(const char* a, const char* b){
|
||||
if (!a && !b) return 1;
|
||||
if (!a || !b) return 0;
|
||||
return strcmp(a,b)==0;
|
||||
}
|
||||
static int cmp_node(const StoreNode* a, const StoreNode* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->content,b->content) ||
|
||||
!streq(a->node_type,b->node_type) || !streq(a->label,b->label) ||
|
||||
!streq(a->tier,b->tier) || !streq(a->tags,b->tags) ||
|
||||
!streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->salience!=b->salience || a->importance!=b->importance ||
|
||||
a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate ||
|
||||
a->activation_count!=b->activation_count || a->last_activated!=b->last_activated ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->background_activation!=b->background_activation ||
|
||||
a->working_memory_weight!=b->working_memory_weight ||
|
||||
a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id ||
|
||||
a->access_head!=b->access_head || a->access_filled!=b->access_filled ||
|
||||
a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0;
|
||||
for (int k=0;k<STORE_BLL_K;k++) if (a->access_ts[k]!=b->access_ts[k]) return 0;
|
||||
if ((a->emb==NULL) != (b->emb==NULL)) return 0;
|
||||
if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0;
|
||||
return 1;
|
||||
}
|
||||
static int cmp_edge(const StoreEdge* a, const StoreEdge* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) ||
|
||||
!streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory ||
|
||||
a->layer_id!=b->layer_id) return 0;
|
||||
return 1;
|
||||
}
|
||||
static void free_node_fields(StoreNode* n){
|
||||
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
||||
free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown);
|
||||
}
|
||||
static void free_edge_fields(StoreEdge* e){
|
||||
free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); free(e->unknown);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 1 — SMALL-POOL CORRECTNESS: full M1 workload (5k nodes / 20k edges) with
|
||||
* a frame budget FAR smaller than the store → constant eviction + re-fault, yet
|
||||
* every read is bit-exact and the pool stays bounded.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_small_pool_roundtrip(void){
|
||||
printf("\n== 1) small-pool correctness: %d nodes + %d edges, cap=%d frames ==\n",
|
||||
NODE_COUNT, EDGE_COUNT, 32);
|
||||
char path[600]; path_in(path, sizeof path, "small.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
ok("store_create", s != NULL);
|
||||
if (!s) return;
|
||||
store__set_pool_frames(s, 32); /* pool << store */
|
||||
|
||||
for (int i=0;i<NODE_COUNT;i++){
|
||||
StoreNode n; gen_node(i,&n);
|
||||
if (store_put_node(s,&n)!=0){ ok("put_node", 0); free_node_fields(&n); store_close(s); return; }
|
||||
free_node_fields(&n);
|
||||
if ((i%500)==499) store_sync(s); /* checkpoint: dirty→clean so frames evictable */
|
||||
}
|
||||
for (int i=0;i<EDGE_COUNT;i++){
|
||||
StoreEdge e; gen_edge(i,&e);
|
||||
if (store_put_edge(s,&e)!=0){ ok("put_edge", 0); free_edge_fields(&e); store_close(s); return; }
|
||||
free_edge_fields(&e);
|
||||
if ((i%1000)==999) store_sync(s);
|
||||
}
|
||||
store_sync(s);
|
||||
|
||||
StorePoolStats st; store_pool_stats(s, &st);
|
||||
printf(" pages=%llu pool: cap=%zu resident=%zu pinned=%zu dirty=%zu evictions=%llu\n",
|
||||
(unsigned long long)store_page_count(s), st.cap, st.resident, st.pinned,
|
||||
st.dirty, (unsigned long long)st.evictions);
|
||||
ok("eviction actually fired (store exceeded the pool)", st.evictions > 0);
|
||||
ok("pool stayed bounded (resident <= cap)", st.resident <= st.cap);
|
||||
ok("no dirty frames after checkpoint", st.dirty == 0);
|
||||
|
||||
/* read back EVERY node bit-exact despite constant eviction/re-fault */
|
||||
int bad = 0;
|
||||
for (int i=0;i<NODE_COUNT;i++){
|
||||
StoreNode want; gen_node(i,&want);
|
||||
StoreNode got; int hit = store_get_node(s, want.id, &got);
|
||||
if (hit!=1 || !cmp_node(&want,&got)) bad++;
|
||||
if (hit==1) store_node_free(&got);
|
||||
free_node_fields(&want);
|
||||
}
|
||||
ok("all 5000 nodes bit-exact under eviction", bad==0);
|
||||
|
||||
/* sample 4000 edges bit-exact */
|
||||
int ebad = 0;
|
||||
for (int i=0;i<EDGE_COUNT;i+=5){
|
||||
StoreEdge want; gen_edge(i,&want);
|
||||
StoreEdge got; int hit = store_get_edge(s, want.id, &got);
|
||||
if (hit!=1 || !cmp_edge(&want,&got)) ebad++;
|
||||
if (hit==1) store_edge_free(&got);
|
||||
free_edge_fields(&want);
|
||||
}
|
||||
ok("sampled 4000 edges bit-exact under eviction", ebad==0);
|
||||
ok("store_check crc clean under paging", store_check(s, STORE_CHECK_CRC)==0);
|
||||
|
||||
store_pool_stats(s, &st);
|
||||
printf(" after reads: resident=%zu (<= cap=%zu) hits=%llu misses=%llu evictions=%llu\n",
|
||||
st.resident, st.cap, (unsigned long long)st.hits,
|
||||
(unsigned long long)st.misses, (unsigned long long)st.evictions);
|
||||
ok("still bounded after full read-back", st.resident <= st.cap);
|
||||
store_close(s);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 2 — EVICTION POLICY: a repeatedly-touched HOT set stays resident (0 extra
|
||||
* faults) while a streaming COLD set is evicted; and a dirty-heavy write burst
|
||||
* proves dirty pages are NEVER stolen before a checkpoint (no-steal).
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_eviction_policy(void){
|
||||
printf("\n== 2) eviction policy: hot resident, cold evicted, no dirty stolen ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "evict.store");
|
||||
unlink(path);
|
||||
|
||||
/* ---- part A: hot vs cold ---- */
|
||||
EngramPagedStore* s = store_create(path);
|
||||
if (!s){ ok("store_create", 0); return; }
|
||||
const int N = 1500;
|
||||
for (int i=0;i<N;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n);
|
||||
if ((i%400)==399) store_sync(s); }
|
||||
store_sync(s);
|
||||
store__set_pool_frames(s, 64);
|
||||
|
||||
const int HOT = 8;
|
||||
/* warm the hot set */
|
||||
for (int h=0;h<HOT;h++){ char id[32]; snprintf(id,sizeof id,"node-%d",h);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
|
||||
StorePoolStats a,b;
|
||||
uint64_t hot_faults = 0, cold_faults = 0;
|
||||
int cold = 200; /* streaming cold ids well outside hot set */
|
||||
for (int r=0;r<150;r++){
|
||||
for (int h=0;h<HOT;h++){
|
||||
char id[32]; snprintf(id,sizeof id,"node-%d",h);
|
||||
store_pool_stats(s,&a);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g);
|
||||
store_pool_stats(s,&b);
|
||||
hot_faults += (b.misses - a.misses);
|
||||
}
|
||||
for (int c=0;c<3;c++){
|
||||
char id[32]; snprintf(id,sizeof id,"node-%d",cold++);
|
||||
if (cold>=N) cold=200;
|
||||
store_pool_stats(s,&a);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g);
|
||||
store_pool_stats(s,&b);
|
||||
cold_faults += (b.misses - a.misses);
|
||||
}
|
||||
}
|
||||
printf(" hot re-get faults (post-warm)=%llu cold stream faults=%llu\n",
|
||||
(unsigned long long)hot_faults, (unsigned long long)cold_faults);
|
||||
ok("HOT pages stay resident (0 faults on re-access)", hot_faults == 0);
|
||||
ok("COLD pages get evicted + re-faulted", cold_faults > 0);
|
||||
store_pool_stats(s,&b);
|
||||
double hr = (double)b.hits / (double)(b.hits + b.misses);
|
||||
printf(" overall hit-rate = %.3f (hits=%llu misses=%llu)\n",
|
||||
hr, (unsigned long long)b.hits, (unsigned long long)b.misses);
|
||||
ok("hit-rate is sane (> 0.5)", hr > 0.5);
|
||||
store_close(s);
|
||||
unlink(path);
|
||||
|
||||
/* ---- part B: no-steal (dirty pages never evicted before checkpoint) ---- */
|
||||
EngramPagedStore* s2 = store_create(path);
|
||||
if (!s2){ ok("store_create(2)", 0); return; }
|
||||
store__set_pool_frames(s2, 8); /* tiny budget */
|
||||
for (int i=0;i<1200;i++){ StoreNode n; gen_node(i,&n); store_put_node(s2,&n); free_node_fields(&n); }
|
||||
/* NO sync: every mutated page is dirty and, by no-steal, unevictable */
|
||||
StorePoolStats d; store_pool_stats(s2,&d);
|
||||
printf(" tiny cap=%zu, unsynced burst: resident=%zu dirty=%zu evictions=%llu\n",
|
||||
d.cap, d.resident, d.dirty, (unsigned long long)d.evictions);
|
||||
ok("dirty pages pinned in RAM beyond budget (no-steal)", d.dirty > d.cap && d.resident > d.cap);
|
||||
/* a just-written node is served correctly from its dirty in-RAM page */
|
||||
{ StoreNode want; gen_node(777,&want); StoreNode got; int hit=store_get_node(s2,want.id,&got);
|
||||
ok("read served correctly from dirty (un-flushed) page", hit==1 && cmp_node(&want,&got));
|
||||
if (hit==1) store_node_free(&got); free_node_fields(&want); }
|
||||
store_sync(s2); /* checkpoint → dirty become clean/evictable */
|
||||
store_pool_stats(s2,&d);
|
||||
ok("checkpoint cleared all dirty frames", d.dirty == 0);
|
||||
/* durability across reopen after the no-steal burst */
|
||||
store_close(s2);
|
||||
EngramPagedStore* s3 = store_open(path);
|
||||
store__set_pool_frames(s3, 8);
|
||||
int miss=0; for (int i=0;i<1200;i++){ StoreNode want; gen_node(i,&want);
|
||||
StoreNode got; int hit=store_get_node(s3,want.id,&got);
|
||||
if (hit!=1 || !cmp_node(&want,&got)) miss++;
|
||||
if (hit==1) store_node_free(&got); free_node_fields(&want); }
|
||||
ok("all 1200 survive reopen, bit-exact, tiny pool", miss==0);
|
||||
store_close(s3);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 3 — PINNED RESIDENCY: superblocks + index roots never evicted under heavy
|
||||
* thrash; an explicitly pinned page stays until unpinned; a pinned hot layer's
|
||||
* pages stay resident and are released on unpin.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_pinning(void){
|
||||
printf("\n== 3) pinned residency: superblocks / index roots / page / layer ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "pin.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
if (!s){ ok("store_create", 0); return; }
|
||||
const int N = 1500;
|
||||
for (int i=0;i<N;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n);
|
||||
if ((i%400)==399) store_sync(s); }
|
||||
store_sync(s);
|
||||
store_close(s);
|
||||
|
||||
s = store_open(path); /* reopen: SBs + roots auto-pinned */
|
||||
store__set_pool_frames(s, 24);
|
||||
|
||||
uint64_t P = store_page_count(s) / 2; /* an arbitrary interior page to pin */
|
||||
store_pin_page(s, P);
|
||||
|
||||
/* thrash: stream a large cold working set to force heavy eviction */
|
||||
for (int pass=0; pass<3; pass++)
|
||||
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
|
||||
ok("superblock page 0 never evicted", store_pool_resident(s,0)==1);
|
||||
ok("superblock mirror page 1 never evicted", store_pool_resident(s,1)==1);
|
||||
ok("explicitly pinned page stayed resident under thrash", store_pool_resident(s,P)==1);
|
||||
|
||||
StorePoolStats st; store_pool_stats(s,&st);
|
||||
printf(" after thrash: resident=%zu pinned=%zu evictions=%llu\n",
|
||||
st.resident, st.pinned, (unsigned long long)st.evictions);
|
||||
ok("structural + explicit pins counted (>=4: 2 SB + 2 roots)", st.pinned >= 4);
|
||||
|
||||
/* unpin the page → it becomes evictable and is dropped under further thrash */
|
||||
store_unpin_page(s, P);
|
||||
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
ok("unpinned page becomes evictable (dropped)", store_pool_resident(s,P)==0);
|
||||
|
||||
/* hot-layer pin: layer 3 is used by ~1/5 of the nodes */
|
||||
int npin = store_pin_layer(s, 3);
|
||||
printf(" store_pin_layer(3) pinned %d page(s)\n", npin);
|
||||
ok("pin_layer pinned a non-empty page set", npin > 0);
|
||||
store_pool_stats(s,&st);
|
||||
size_t pinned_with_layer = st.pinned;
|
||||
for (int pass=0; pass<3; pass++)
|
||||
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
store_pool_stats(s,&st);
|
||||
ok("hot-layer pages stay resident under thrash", st.pinned >= pinned_with_layer);
|
||||
ok("layer pin holds >= npin extra frames", st.pinned >= (size_t)npin + 4);
|
||||
|
||||
store_unpin_layer(s, 3);
|
||||
store_pool_stats(s,&st);
|
||||
size_t after_unpin_max = st.pinned;
|
||||
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
store_pool_stats(s,&st);
|
||||
printf(" pinned frames: with-layer=%zu after-unpin=%zu\n", pinned_with_layer, st.pinned);
|
||||
ok("unpin_layer released the layer's pins", st.pinned < pinned_with_layer && after_unpin_max <= pinned_with_layer);
|
||||
|
||||
store_close(s);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 4 — PREFETCH: a sequential scan faults far fewer times with read-ahead on
|
||||
* than off (each cold cache; identical store).
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_prefetch(void){
|
||||
printf("\n== 4) prefetch: sequential scan faults fewer with read-ahead ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "prefetch.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
if (!s){ ok("store_create", 0); return; }
|
||||
for (int i=0;i<2000;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n);
|
||||
if ((i%400)==399) store_sync(s); }
|
||||
store_sync(s);
|
||||
store_close(s);
|
||||
|
||||
/* prefetch OFF — cold cache */
|
||||
EngramPagedStore* a = store_open(path);
|
||||
store__set_pool_frames(a, 0); /* unlimited: isolate prefetch, no eviction */
|
||||
store__set_prefetch(a, 0);
|
||||
StorePoolStats o0, o1; store_pool_stats(a,&o0);
|
||||
int na = store_scan_nodes(a, noop_node_cb, NULL); /* walk + fault every page */
|
||||
(void)na;
|
||||
store_pool_stats(a,&o1);
|
||||
uint64_t faults_off = o1.misses - o0.misses;
|
||||
store_close(a);
|
||||
|
||||
/* prefetch ON — cold cache (fresh open) */
|
||||
EngramPagedStore* b = store_open(path);
|
||||
store__set_pool_frames(b, 0);
|
||||
store__set_prefetch(b, 16);
|
||||
StorePoolStats p0, p1; store_pool_stats(b,&p0);
|
||||
int nb = store_scan_nodes(b, noop_node_cb, NULL);
|
||||
(void)nb;
|
||||
store_pool_stats(b,&p1);
|
||||
uint64_t faults_on = p1.misses - p0.misses;
|
||||
uint64_t pref_reads = p1.prefetch_reads - p0.prefetch_reads;
|
||||
store_close(b);
|
||||
|
||||
printf(" scan demand-faults: prefetch OFF=%llu ON=%llu (read-ahead brought in %llu pages)\n",
|
||||
(unsigned long long)faults_off, (unsigned long long)faults_on,
|
||||
(unsigned long long)pref_reads);
|
||||
ok("prefetch reduced demand faults", faults_on < faults_off);
|
||||
ok("read-ahead actually ran", pref_reads > 0);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 5 — CRASH SAFETY UNDER PAGING: WAL replay and checkpoint-crash recovery
|
||||
* with a tiny pool (pages evict + re-fault during replay).
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_crash_under_paging(void){
|
||||
printf("\n== 5) crash safety under a tiny pool (ENGRAM_POOL_FRAMES=16) ==\n");
|
||||
setenv("ENGRAM_POOL_FRAMES", "16", 1); /* every engram_open() below is paged */
|
||||
setenv("ENGRAM_WAL_SYNC", "always", 1);
|
||||
|
||||
/* ---- 5a: power-loss → WAL replay ---- */
|
||||
char dir[600]; path_in(dir, sizeof dir, "crash_wal"); mkdir(dir, 0700);
|
||||
EngramPagedStore* s = engram_open(dir);
|
||||
if (!s){ ok("engram_open", 0); return; }
|
||||
const int M = 400;
|
||||
for (int i=0;i<M;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n); }
|
||||
store__crash(s); /* abandon RAM (dirty pages lost); WAL fsync'd */
|
||||
s = engram_open(dir); /* replay WAL under 16-frame pool */
|
||||
ok("reopened after crash (WAL replay, tiny pool)", s!=NULL);
|
||||
int bad=0; for (int i=0;i<M;i++){ StoreNode want; gen_node(i,&want);
|
||||
StoreNode got; int hit=store_get_node(s,want.id,&got);
|
||||
if (hit!=1 || !cmp_node(&want,&got)) bad++;
|
||||
if (hit==1) store_node_free(&got); free_node_fields(&want); }
|
||||
ok("all 400 nodes recovered bit-exact via WAL replay under paging", bad==0);
|
||||
ok("store_check crc clean post-recovery", store_check(s, STORE_CHECK_CRC)==0);
|
||||
engram_close(s);
|
||||
|
||||
/* ---- 5b: checkpoint-crash at each phase ---- */
|
||||
for (int phase=0; phase<=4; phase++){
|
||||
char cdir[620]; snprintf(cdir, sizeof cdir, "%s/ck%d", g_dir, phase); mkdir(cdir,0700);
|
||||
EngramPagedStore* c = engram_open(cdir);
|
||||
for (int i=0;i<CK_NODES;i++){ StoreNode n; gen_node(i,&n); store_put_node(c,&n); free_node_fields(&n); }
|
||||
store__checkpoint_crashat(c, phase); /* crash mid-checkpoint (frees c) */
|
||||
EngramPagedStore* r = engram_open(cdir); /* heal + replay under tiny pool */
|
||||
int miss=0; for (int i=0;i<CK_NODES;i++){ StoreNode want; gen_node(i,&want);
|
||||
StoreNode got; int hit=store_get_node(r,want.id,&got);
|
||||
if (hit!=1 || !cmp_node(&want,&got)) miss++;
|
||||
if (hit==1) store_node_free(&got); free_node_fields(&want); }
|
||||
char nm[64]; snprintf(nm,sizeof nm,"checkpoint-crash phase %d: all recovered (paged)", phase);
|
||||
ok(nm, miss==0);
|
||||
engram_close(r);
|
||||
}
|
||||
unsetenv("ENGRAM_POOL_FRAMES");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 6 — DEFAULT POOL == PHASE 1: with the default (large) budget, no eviction
|
||||
* ever fires; the whole store is resident, exactly the pre-M4 behaviour.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_default_is_phase1(void){
|
||||
printf("\n== 6) default (large) pool == Phase-1 resident (no eviction) ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "default.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path); /* default cap, no override */
|
||||
if (!s){ ok("store_create", 0); return; }
|
||||
for (int i=0;i<1500;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n); }
|
||||
store_sync(s);
|
||||
for (int i=0;i<1500;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1) store_node_free(&g); }
|
||||
StorePoolStats st; store_pool_stats(s,&st);
|
||||
printf(" cap=%zu resident=%zu evictions=%llu (pages=%llu)\n",
|
||||
st.cap, st.resident, (unsigned long long)st.evictions,
|
||||
(unsigned long long)store_page_count(s));
|
||||
ok("default budget is large", st.cap >= (size_t)(1u<<20));
|
||||
ok("no eviction ever fired at default budget", st.evictions == 0);
|
||||
ok("whole store resident (every page cached)", st.resident == store_page_count(s));
|
||||
store_close(s);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
int main(void){
|
||||
mk_dir();
|
||||
printf("engram M4 buffer-pool gate — dir=%s\n", g_dir);
|
||||
test_small_pool_roundtrip();
|
||||
test_eviction_policy();
|
||||
test_pinning();
|
||||
test_prefetch();
|
||||
test_crash_under_paging();
|
||||
test_default_is_phase1();
|
||||
printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
/* test_compaction.c — M5 gate: ONLINE COMPACTION + background checkpointer.
|
||||
*
|
||||
* Pure C. Build: gcc -O2 test_compaction.c ../../lang/runtime/engram_store.c -o t
|
||||
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
|
||||
*
|
||||
* Proves:
|
||||
* 1) RECLAIM — tombstone/forget a large fraction of nodes + re-put many edges
|
||||
* (dead versions) + orphan large-record overflow chains, then compact:
|
||||
* page count AND file size drop, yet EVERY live record survives bit-exact and
|
||||
* the id + adjacency indexes resolve correctly at the relocated positions.
|
||||
* 2) CRASH-DURING-COMPACTION — kill at phases 0/1/2; recovery is always a
|
||||
* consistent store (crc clean, every live record intact), never corrupt.
|
||||
* 3) BACKGROUND CHECKPOINTER — a low ops / WAL-bytes threshold fires a checkpoint
|
||||
* automatically on the write path; the WAL prefix is reclaimed; recovery works.
|
||||
* 4) POOL COOPERATION — compaction under a tiny ENGRAM_POOL_FRAMES stays correct
|
||||
* with no stale frame surviving for a relocated page.
|
||||
*/
|
||||
#include "../../lang/runtime/engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond){
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_dir[512];
|
||||
static int g_dseq = 0;
|
||||
static void mk_dir(void){
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/engram-compact-test-%d-%d", (int)getpid(), g_dseq++);
|
||||
mkdir(g_dir, 0700);
|
||||
}
|
||||
static void egm_path(char* out, size_t cap){ snprintf(out, cap, "%s/neuron.egm", g_dir); }
|
||||
static void wal_path(char* out, size_t cap){ snprintf(out, cap, "%s/neuron.wal", g_dir); }
|
||||
static long file_size(const char* p){ struct stat st; return stat(p,&st)==0 ? (long)st.st_size : -1; }
|
||||
|
||||
/* ── deterministic generators (bit-exact regeneration for oracles) ─────────── */
|
||||
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
|
||||
static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); }
|
||||
static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); }
|
||||
static char* rnd_str(uint64_t* st, size_t len){
|
||||
char* s = (char*)malloc(len + 1);
|
||||
for (size_t i=0;i<len;i++) s[i] = (char)(33 + (xs(st) % 94));
|
||||
s[len] = 0; return s;
|
||||
}
|
||||
|
||||
#define N_NODES 1500
|
||||
#define N_DEAD 1200 /* forget node-0 .. node-1199 (1200 dead / 300 live) */
|
||||
#define N_EDGES 3000
|
||||
#define EDGE_REPUT 2000 /* re-put edge-0 .. edge-1999 to version 3 */
|
||||
#define EMB_DIM 96
|
||||
|
||||
static int node_is_live(int i){ return i >= N_DEAD; }
|
||||
static int edge_live_version(int i){ return (i < EDGE_REPUT) ? 3 : 0; }
|
||||
|
||||
static void gen_node(int i, StoreNode* n){
|
||||
memset(n, 0, sizeof *n);
|
||||
uint64_t st = node_seed(i);
|
||||
char id[32]; snprintf(id, sizeof id, "node-%d", i);
|
||||
n->id = strdup(id);
|
||||
/* every 7th record is large → its own overflow chain (orphaned when it dies) */
|
||||
size_t clen = (i % 7 == 0) ? (size_t)(18000 + (xs(&st) % 4000)) : (size_t)(xs(&st) % 200);
|
||||
n->content = rnd_str(&st, clen);
|
||||
n->node_type = rnd_str(&st, 4 + (xs(&st) % 8));
|
||||
n->label = (i % 2) ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL;
|
||||
n->tier = rnd_str(&st, 4 + (xs(&st) % 6));
|
||||
n->tags = rnd_str(&st, xs(&st) % 40);
|
||||
n->metadata = (i % 3) ? rnd_str(&st, xs(&st) % 60) : NULL;
|
||||
n->salience = (double)(xs(&st) % 1000000) / 997.0;
|
||||
n->importance = (double)(xs(&st) % 1000000) / 131.0;
|
||||
n->confidence = (double)(xs(&st) % 1000000) / 733.0;
|
||||
n->temporal_decay_rate = (double)(xs(&st) % 1000000) / 101.0;
|
||||
n->activation_count = (int64_t)(xs(&st) % 100000);
|
||||
n->last_activated = (int64_t)xs(&st);
|
||||
n->created_at = (int64_t)(1600000000000LL + i);
|
||||
n->updated_at = (int64_t)xs(&st);
|
||||
n->background_activation = (double)(xs(&st) % 1000000) / 17.0;
|
||||
n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0;
|
||||
n->suppression_count = (int32_t)(xs(&st) % 50);
|
||||
n->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
for (int k=0;k<STORE_BLL_K;k++) n->access_ts[k] = (int64_t)xs(&st);
|
||||
n->access_head = (int32_t)(xs(&st) % STORE_BLL_K);
|
||||
n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1));
|
||||
n->wm_anchor = (double)(xs(&st) % 1000000) / 3.0;
|
||||
n->emb = (float*)malloc(EMB_DIM * sizeof(float));
|
||||
for (int k=0;k<EMB_DIM;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k], &u, 4); }
|
||||
n->emb_dim = EMB_DIM;
|
||||
}
|
||||
/* version alters weight/hebb/last_fired so a re-put is a distinct payload. */
|
||||
static void gen_edge(int i, int version, StoreEdge* e){
|
||||
memset(e, 0, sizeof *e);
|
||||
uint64_t st = edge_seed(i);
|
||||
char id[32], from[32], to[32];
|
||||
snprintf(id, sizeof id, "edge-%d", i);
|
||||
/* connect live nodes so adjacency queries on live nodes are meaningful */
|
||||
snprintf(from, sizeof from, "node-%d", N_DEAD + (int)(xs(&st) % (N_NODES - N_DEAD)));
|
||||
snprintf(to, sizeof to, "node-%d", N_DEAD + (int)(xs(&st) % (N_NODES - N_DEAD)));
|
||||
e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
|
||||
e->relation = rnd_str(&st, 3 + (xs(&st) % 12));
|
||||
e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL;
|
||||
e->weight = (double)(xs(&st) % 1000000) / 7.0 + version * 100.0;
|
||||
e->hebb = (double)(xs(&st) % 1000000) / 13.0 + version * 3.0;
|
||||
e->confidence = (double)(xs(&st) % 1000000) / 5.0;
|
||||
e->created_at = (int64_t)(1600000000000LL + i);
|
||||
e->updated_at = (int64_t)xs(&st) + version;
|
||||
e->last_fired = (int64_t)xs(&st) + version * 1000;
|
||||
e->inhibitory = (int32_t)(xs(&st) % 2);
|
||||
e->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
}
|
||||
|
||||
static int streq(const char* a, const char* b){
|
||||
if (!a && !b) return 1; if (!a || !b) return 0; return strcmp(a,b)==0;
|
||||
}
|
||||
static int cmp_node(const StoreNode* a, const StoreNode* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->content,b->content) ||
|
||||
!streq(a->node_type,b->node_type) || !streq(a->label,b->label) ||
|
||||
!streq(a->tier,b->tier) || !streq(a->tags,b->tags) ||
|
||||
!streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->salience!=b->salience || a->importance!=b->importance ||
|
||||
a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate ||
|
||||
a->activation_count!=b->activation_count || a->last_activated!=b->last_activated ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->background_activation!=b->background_activation ||
|
||||
a->working_memory_weight!=b->working_memory_weight ||
|
||||
a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id ||
|
||||
a->access_head!=b->access_head || a->access_filled!=b->access_filled ||
|
||||
a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0;
|
||||
for (int k=0;k<STORE_BLL_K;k++) if (a->access_ts[k]!=b->access_ts[k]) return 0;
|
||||
if ((a->emb==NULL) != (b->emb==NULL)) return 0;
|
||||
if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0;
|
||||
return 1;
|
||||
}
|
||||
static int cmp_edge(const StoreEdge* a, const StoreEdge* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) ||
|
||||
!streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory ||
|
||||
a->layer_id!=b->layer_id) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Populate a durable store with dead space: all nodes/edges, then forget the first
|
||||
* N_DEAD nodes and re-put the first EDGE_REPUT edges three times. */
|
||||
static void populate_with_dead_space(EngramPagedStore* s){
|
||||
for (int i=0;i<N_NODES;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
for (int i=0;i<N_EDGES;i++){ StoreEdge e; gen_edge(i,0,&e); store_put_edge(s,&e); store_edge_free(&e); }
|
||||
/* re-put (in-place field mutation) → prior versions become dead records */
|
||||
for (int v=1; v<=3; v++)
|
||||
for (int i=0;i<EDGE_REPUT;i++){ StoreEdge e; gen_edge(i,v,&e); store_put_edge(s,&e); store_edge_free(&e); }
|
||||
/* forget the cold nodes (tombstone; their large overflow chains orphan) */
|
||||
for (int i=0;i<N_DEAD;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i); store_forget(s,id); }
|
||||
}
|
||||
|
||||
/* Assert every live node/edge is present + bit-exact via point reads. */
|
||||
static int verify_live_set(EngramPagedStore* s){
|
||||
int bad = 0;
|
||||
for (int i=0;i<N_NODES;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode got; int hit = store_get_node(s, id, &got);
|
||||
if (node_is_live(i)){
|
||||
StoreNode want; gen_node(i,&want);
|
||||
if (hit!=1 || !cmp_node(&want,&got)) bad++;
|
||||
if (hit==1) store_node_free(&got);
|
||||
store_node_free(&want);
|
||||
} else {
|
||||
if (hit!=0) bad++; /* forgotten → must be absent */
|
||||
if (hit==1) store_node_free(&got);
|
||||
}
|
||||
}
|
||||
for (int i=0;i<N_EDGES;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"edge-%d",i);
|
||||
StoreEdge got; int hit = store_get_edge(s, id, &got);
|
||||
StoreEdge want; gen_edge(i, edge_live_version(i), &want);
|
||||
if (hit!=1 || !cmp_edge(&want,&got)) bad++;
|
||||
if (hit==1) store_edge_free(&got);
|
||||
store_edge_free(&want);
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 1 — RECLAIM: dead space is reclaimed; live records + indexes survive.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_reclaim(void){
|
||||
printf("\n== 1) reclaim: forget %d nodes + re-put %d edges x3, then compact ==\n",
|
||||
N_DEAD, EDGE_REPUT);
|
||||
mk_dir();
|
||||
char egm[600]; egm_path(egm, sizeof egm);
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
ok("engram_open", s != NULL);
|
||||
if (!s) return;
|
||||
|
||||
populate_with_dead_space(s);
|
||||
engram_checkpoint(s); /* flush so file size reflects state */
|
||||
|
||||
uint64_t pc_before = store_page_count(s);
|
||||
uint64_t free_before = store_free_page_count(s);
|
||||
long sz_before = file_size(egm);
|
||||
printf(" BEFORE: page_count=%llu free_pages=%llu file=%ld bytes (live records intact?)\n",
|
||||
(unsigned long long)pc_before, (unsigned long long)free_before, sz_before);
|
||||
ok("pre-compaction live set intact", verify_live_set(s)==0);
|
||||
|
||||
/* capture adjacency for a sample of live from-ids to compare post-compaction */
|
||||
#define NSAMP 12
|
||||
char samp[NSAMP][32]; size_t pre_cnt[NSAMP];
|
||||
for (int k=0;k<NSAMP;k++){
|
||||
snprintf(samp[k], sizeof samp[k], "node-%d", N_DEAD + k*20);
|
||||
StoreEdge* arr=NULL; size_t cnt=0;
|
||||
store_get_edges_from(s, samp[k], &arr, &cnt);
|
||||
pre_cnt[k]=cnt; store_edges_free(arr,cnt);
|
||||
}
|
||||
|
||||
int rc = store_compact(s);
|
||||
ok("store_compact returns 0", rc==0);
|
||||
|
||||
uint64_t pc_after = store_page_count(s);
|
||||
uint64_t free_after = store_free_page_count(s);
|
||||
long sz_after = file_size(egm);
|
||||
printf(" AFTER : page_count=%llu free_pages=%llu file=%ld bytes\n",
|
||||
(unsigned long long)pc_after, (unsigned long long)free_after, sz_after);
|
||||
printf(" RECLAIMED: %llu pages, %ld bytes (%.1f%% of file)\n",
|
||||
(unsigned long long)(pc_before - pc_after), sz_before - sz_after,
|
||||
sz_before ? 100.0*(sz_before-sz_after)/sz_before : 0.0);
|
||||
|
||||
ok("page count dropped (dead pages reclaimed)", pc_after < pc_before);
|
||||
ok("file size dropped (store physically shrank)", sz_after < sz_before);
|
||||
ok("store_check crc clean after compaction", store_check(s, STORE_CHECK_CRC)==0);
|
||||
ok("every LIVE record present + bit-exact at new locations", verify_live_set(s)==0);
|
||||
|
||||
/* adjacency index correct at relocated positions */
|
||||
int adj_bad = 0;
|
||||
for (int k=0;k<NSAMP;k++){
|
||||
StoreEdge* arr=NULL; size_t cnt=0;
|
||||
store_get_edges_from(s, samp[k], &arr, &cnt);
|
||||
if (cnt != pre_cnt[k]) adj_bad++;
|
||||
for (size_t j=0;j<cnt;j++){
|
||||
if (!streq(arr[j].from_id, samp[k])) { adj_bad++; break; }
|
||||
/* the returned edge must be the canonical latest live edge, bit-exact */
|
||||
int idx = atoi(arr[j].id + 5);
|
||||
StoreEdge want; gen_edge(idx, edge_live_version(idx), &want);
|
||||
if (!cmp_edge(&want,&arr[j])) adj_bad++;
|
||||
store_edge_free(&want);
|
||||
}
|
||||
store_edges_free(arr,cnt);
|
||||
}
|
||||
ok("adjacency (get_edges_from) correct + bit-exact post-compaction", adj_bad==0);
|
||||
|
||||
/* second compaction is a near no-op (no new dead space) and stays correct */
|
||||
uint64_t pc2_before = store_page_count(s);
|
||||
ok("compact again returns 0", store_compact(s)==0);
|
||||
ok("idempotent-ish: no growth on re-compact", store_page_count(s) <= pc2_before);
|
||||
ok("live set still intact after 2nd compaction", verify_live_set(s)==0);
|
||||
|
||||
engram_close(s);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 2 — CRASH DURING COMPACTION: kill at phases 0/1/2 → consistent recovery.
|
||||
* Live set is identical whether we recover pre- or post-compaction, so the same
|
||||
* oracle must hold, and crc must always be clean (never corrupt).
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_crash_during_compaction(void){
|
||||
printf("\n== 2) crash during compaction at phases 0,1,2 → consistent store ==\n");
|
||||
for (int phase=0; phase<=2; phase++){
|
||||
mk_dir();
|
||||
char egm[600]; egm_path(egm, sizeof egm);
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
if (!s){ ok("engram_open", 0); continue; }
|
||||
populate_with_dead_space(s);
|
||||
engram_close(s); /* durable baseline on disk */
|
||||
|
||||
uint64_t pc_pre = 0;
|
||||
{ EngramPagedStore* p = engram_open(g_dir); pc_pre = store_page_count(p); engram_close(p); }
|
||||
|
||||
EngramPagedStore* c = engram_open(g_dir);
|
||||
store__compact_crashat(c, phase); /* crashes mid-compaction (frees c) */
|
||||
|
||||
EngramPagedStore* r = engram_open(g_dir); /* recover */
|
||||
char nm[80];
|
||||
snprintf(nm, sizeof nm, "phase %d: recovers, crc clean", phase);
|
||||
ok(nm, r && store_check(r, STORE_CHECK_CRC)==0);
|
||||
snprintf(nm, sizeof nm, "phase %d: every live record intact (not corrupt)", phase);
|
||||
ok(nm, r && verify_live_set(r)==0);
|
||||
if (r){
|
||||
uint64_t pc_now = store_page_count(r);
|
||||
if (phase < 2){
|
||||
snprintf(nm, sizeof nm, "phase %d: recovered PRE-compaction image", phase);
|
||||
ok(nm, pc_now == pc_pre);
|
||||
} else {
|
||||
snprintf(nm, sizeof nm, "phase %d: recovered POST-compaction (shrunk)", phase);
|
||||
ok(nm, pc_now < pc_pre);
|
||||
}
|
||||
/* store stays writable + durable after recovery */
|
||||
StoreNode n; gen_node(N_NODES+phase, &n); free(n.id);
|
||||
n.id = strdup("post-recovery-node");
|
||||
store_put_node(r, &n); store_node_free(&n);
|
||||
StoreNode g; int hit = store_get_node(r, "post-recovery-node", &g);
|
||||
snprintf(nm, sizeof nm, "phase %d: store writable after recovery", phase);
|
||||
ok(nm, hit==1);
|
||||
if (hit==1) store_node_free(&g);
|
||||
engram_close(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 3 — BACKGROUND CHECKPOINTER: a low threshold fires checkpoints on the
|
||||
* write path, reclaiming the WAL prefix automatically; recovery still correct.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_background_checkpointer(void){
|
||||
printf("\n== 3) background checkpointer: auto-checkpoint on threshold ==\n");
|
||||
|
||||
/* (a) ops trigger */
|
||||
{
|
||||
mk_dir();
|
||||
char wal[600]; wal_path(wal, sizeof wal);
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
if (!s){ ok("engram_open", 0); return; }
|
||||
store_set_checkpoint_policy(s, /*ops*/50, /*dirty*/0, /*wal_bytes*/0, /*ms*/0);
|
||||
uint64_t ckpt0 = engram_last_checkpoint_lsn(s);
|
||||
for (int i=0;i<600;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
uint64_t ckpt1 = engram_last_checkpoint_lsn(s);
|
||||
long wsz = file_size(wal);
|
||||
printf(" ops-trigger: ckpt_lsn %llu -> %llu, WAL=%ld bytes after 600 puts\n",
|
||||
(unsigned long long)ckpt0, (unsigned long long)ckpt1, wsz);
|
||||
ok("ops trigger fired an automatic checkpoint", ckpt1 > ckpt0);
|
||||
ok("WAL prefix reclaimed (WAL stays small)", wsz >= 0 && wsz < 200000);
|
||||
/* crash (abandon RAM) then recover — everything durable via WAL+checkpoint */
|
||||
store__crash(s);
|
||||
EngramPagedStore* r = engram_open(g_dir);
|
||||
int bad=0;
|
||||
for (int i=0;i<600;i++){ char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
StoreNode w; gen_node(i,&w); StoreNode g; int hit=store_get_node(r,id,&g);
|
||||
if (hit!=1 || !cmp_node(&w,&g)) bad++; if(hit==1) store_node_free(&g); store_node_free(&w); }
|
||||
ok("recovery correct after auto-checkpoints (ops)", r && bad==0);
|
||||
ok("crc clean after recovery (ops)", r && store_check(r,STORE_CHECK_CRC)==0);
|
||||
if (r) engram_close(r);
|
||||
}
|
||||
/* (b) WAL-bytes trigger */
|
||||
{
|
||||
mk_dir();
|
||||
char wal[600]; wal_path(wal, sizeof wal);
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
if (!s){ ok("engram_open", 0); return; }
|
||||
store_set_checkpoint_policy(s, /*ops*/0, /*dirty*/0, /*wal_bytes*/64*1024, /*ms*/0);
|
||||
uint64_t ckpt0 = engram_last_checkpoint_lsn(s);
|
||||
for (int i=0;i<600;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
uint64_t ckpt1 = engram_last_checkpoint_lsn(s);
|
||||
long wsz = file_size(wal);
|
||||
printf(" wal-bytes-trigger: ckpt_lsn %llu -> %llu, WAL=%ld bytes\n",
|
||||
(unsigned long long)ckpt0, (unsigned long long)ckpt1, wsz);
|
||||
ok("wal-bytes trigger fired an automatic checkpoint", ckpt1 > ckpt0);
|
||||
ok("WAL kept bounded by byte threshold", wsz >= 0 && wsz < 2*1024*1024);
|
||||
engram_close(s);
|
||||
}
|
||||
/* (c) dirty-frames trigger (under a bounded pool) */
|
||||
{
|
||||
mk_dir();
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
if (!s){ ok("engram_open", 0); return; }
|
||||
store_set_checkpoint_policy(s, /*ops*/0, /*dirty*/16, /*wal_bytes*/0, /*ms*/0);
|
||||
uint64_t ckpt0 = engram_last_checkpoint_lsn(s);
|
||||
for (int i=0;i<400;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
uint64_t ckpt1 = engram_last_checkpoint_lsn(s);
|
||||
ok("dirty-frames trigger fired an automatic checkpoint", ckpt1 > ckpt0);
|
||||
engram_close(s);
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* TEST 4 — POOL COOPERATION: compact under a tiny frame budget (constant eviction
|
||||
* + re-fault); correctness holds and no stale frame survives a relocated page.
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_pool_cooperation(void){
|
||||
printf("\n== 4) compaction under a small buffer pool (forced eviction) ==\n");
|
||||
setenv("ENGRAM_POOL_FRAMES", "24", 1); /* pool << store, and the temp build too */
|
||||
mk_dir();
|
||||
char egm[600]; egm_path(egm, sizeof egm);
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
ok("engram_open (24-frame pool)", s != NULL);
|
||||
if (!s){ unsetenv("ENGRAM_POOL_FRAMES"); return; }
|
||||
store__set_pool_frames(s, 24);
|
||||
|
||||
populate_with_dead_space(s);
|
||||
engram_checkpoint(s);
|
||||
uint64_t pc_before = store_page_count(s);
|
||||
|
||||
int rc = store_compact(s);
|
||||
ok("store_compact under tiny pool returns 0", rc==0);
|
||||
|
||||
StorePoolStats st; store_pool_stats(s, &st);
|
||||
printf(" post-compaction pool: cap=%zu resident=%zu pinned=%zu dirty=%zu\n",
|
||||
st.cap, st.resident, st.pinned, st.dirty);
|
||||
ok("pool respected budget after compaction (resident<=cap)", st.resident <= st.cap);
|
||||
ok("page count dropped under small pool", store_page_count(s) < pc_before);
|
||||
ok("crc clean under small pool", store_check(s, STORE_CHECK_CRC)==0);
|
||||
/* If any relocated page had a stale frame, a read would return wrong bytes. */
|
||||
ok("every live record bit-exact under small pool (no stale frames)", verify_live_set(s)==0);
|
||||
|
||||
engram_close(s);
|
||||
unsetenv("ENGRAM_POOL_FRAMES");
|
||||
}
|
||||
|
||||
int main(void){
|
||||
printf("=== M5 COMPACTION + BACKGROUND CHECKPOINTER GATE ===\n");
|
||||
test_reclaim();
|
||||
test_crash_during_compaction();
|
||||
test_background_checkpointer();
|
||||
test_pool_cooperation();
|
||||
printf("\n=== RESULT: %d passed, %d failed ===\n", g_pass, g_fail);
|
||||
/* cleanup */
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/* test_geometry.c — build + RUN gate for the M9 FOUNDATION geometry descriptor
|
||||
* (engram_geometry.{c,h}). Self-contained: synthesizes a store with two KNOWN
|
||||
* embedding clusters + intra-cluster hebb edges, then verifies the descriptor
|
||||
* recovers the shape — centroid near the seeded cluster, skeleton = the strong
|
||||
* intra-cluster edges, membership gradient, radius, positive co-registration.
|
||||
*
|
||||
* Pure C11; links engram_geometry.c + engram_store.c + engram_vindex.c; -lm.
|
||||
* ASan/UBSan clean. Needs no live data.
|
||||
*/
|
||||
#include "engram_geometry.h"
|
||||
#include "engram_store.h"
|
||||
#include "engram_vindex.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DIM 64
|
||||
static int g_fail=0;
|
||||
#define CHECK(c,m) do{ if(!(c)){printf(" FAIL: %s\n",m); g_fail=1;} else printf(" ok: %s\n",m);}while(0)
|
||||
|
||||
static uint64_t rs=0x1234abcdULL;
|
||||
static uint64_t xr(void){ uint64_t z=(rs+=0x9E3779B97F4A7C15ULL);
|
||||
z=(z^(z>>30))*0xBF58476D1CE4E5B9ULL; z=(z^(z>>27))*0x94D049BB133111EBULL; return z^(z>>31); }
|
||||
static float jitter(void){ return (float)(((double)(xr()>>11)*(1.0/9007199254740992.0))-0.5)*0.15f; }
|
||||
|
||||
/* two clusters: A centered on axis 0, B centered on axis 1. NA+NB nodes. */
|
||||
#define NA 40
|
||||
#define NB 40
|
||||
|
||||
int main(void){
|
||||
printf("=== engram_geometry (M9 foundation) test suite ===\n");
|
||||
char path[256]; snprintf(path,sizeof path,"/tmp/geo_test_store_%d.egm",(int)getpid());
|
||||
unlink(path);
|
||||
EngramPagedStore* st=store_create(path);
|
||||
if(!st){ printf("FAIL: store_create\n"); return 1; }
|
||||
|
||||
char aids[NA][16], bids[NB][16];
|
||||
/* cluster A: near +e0 ; cluster B: near +e1 */
|
||||
for(int i=0;i<NA;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
snprintf(aids[i],16,"A%d",i); n.id=aids[i]; n.node_type="Concept"; n.tier="Semantic";
|
||||
n.content="cluster-A"; n.salience=0.5+0.01*i;
|
||||
float v[DIM]; for(int d=0;d<DIM;d++) v[d]=jitter(); v[0]=1.0f+jitter();
|
||||
n.emb=v; n.emb_dim=DIM; store_put_node(st,&n);
|
||||
}
|
||||
for(int i=0;i<NB;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
snprintf(bids[i],16,"B%d",i); n.id=bids[i]; n.node_type="Concept"; n.tier="Semantic";
|
||||
n.content="cluster-B"; n.salience=0.3;
|
||||
float v[DIM]; for(int d=0;d<DIM;d++) v[d]=jitter(); v[1]=1.0f+jitter();
|
||||
n.emb=v; n.emb_dim=DIM; store_put_node(st,&n);
|
||||
}
|
||||
/* strong intra-A hebb edges (a chain + hub), weaker cross edges A0<->B0 */
|
||||
int ei=0;
|
||||
for(int i=1;i<NA;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e); char id[24]; snprintf(id,24,"eA%d",ei++);
|
||||
e.id=id; e.from_id=aids[0]; e.to_id=aids[i]; e.relation="assoc"; e.weight=0.9; e.hebb=0.4;
|
||||
store_put_edge(st,&e);
|
||||
}
|
||||
for(int i=1;i<NB;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e); char id[24]; snprintf(id,24,"eB%d",ei++);
|
||||
e.id=id; e.from_id=bids[0]; e.to_id=bids[i]; e.relation="assoc"; e.weight=0.9; e.hebb=0.4;
|
||||
store_put_edge(st,&e);
|
||||
}
|
||||
{ StoreEdge e; memset(&e,0,sizeof e); e.id=(char*)"eX"; e.from_id=aids[0]; e.to_id=bids[0];
|
||||
e.relation="assoc"; e.weight=0.5; e.hebb=0.0; store_put_edge(st,&e); }
|
||||
store_close(st);
|
||||
|
||||
VIndex* ix=vindex_create(DIM,0,0);
|
||||
char** ids=NULL; int nids=0;
|
||||
int ins=vindex_build_from_store(ix, path, &ids, &nids);
|
||||
CHECK(ins==NA+NB, "vindex built over all embedded nodes");
|
||||
|
||||
GeoParams P; engram_geo_default_params(&P); P.ann_k=20; P.max_members=0;
|
||||
|
||||
/* seed inside cluster A -> expect an A-dominated neighborhood */
|
||||
st=store_open(path);
|
||||
/* global-mean cache over the embedded set: the centering offset */
|
||||
GeoMeanCache* mc=engram_geo_mean_build(st);
|
||||
const float* gm=engram_geo_mean_vec(mc);
|
||||
CHECK(mc!=NULL && engram_geo_mean_dim(mc)==DIM, "global-mean cache built over embedded set");
|
||||
CHECK(engram_geo_mean_count(mc)==(uint64_t)(NA+NB), "global mean averaged all embedded nodes");
|
||||
const char* seeds[1]={aids[0]};
|
||||
/* CENTERED descriptor: pass the global mean so geometry runs in isotropic space */
|
||||
GeoDescriptor* g=engram_geometry_descriptor(st, ix, ids, nids, seeds, 1, &P, gm);
|
||||
CHECK(g!=NULL, "descriptor computed");
|
||||
if(g){
|
||||
printf(" members=%d embedded=%d edges=%d k_core=%d radius=%.4f co_reg=%.3f n_axes=%d\n",
|
||||
g->n_members,g->n_embedded,g->n_edges,g->k_core,g->radius,g->co_registration,g->n_axes);
|
||||
|
||||
/* geometry ran in CENTERED space: g->centroid is the centered centroid,
|
||||
* g->global_mean the applied offset. Reconstruct the raw prototype
|
||||
* (centroid + global_mean) and check it sits on cluster-A's axis. */
|
||||
CHECK(g->global_mean!=NULL, "descriptor recorded the centering offset (centered mode)");
|
||||
int argmax=0; float best=-1.f;
|
||||
for(int d=0;d<g->dim;d++){ float raw=g->centroid[d]+(g->global_mean?g->global_mean[d]:0.f);
|
||||
if(fabsf(raw)>best){ best=fabsf(raw); argmax=d; } }
|
||||
printf(" raw-prototype dominant axis = %d (expect 0); centered c[0]=%.3f c[1]=%.3f\n",
|
||||
argmax, g->centroid[0], g->centroid[1]);
|
||||
CHECK(argmax==0, "raw prototype sits on cluster-A's axis (near members)");
|
||||
/* centering pushes A off cluster-B's axis: centered c[0] > c[1] */
|
||||
CHECK(g->centroid[0] > g->centroid[1], "centered centroid leans off B's axis (isotropy)");
|
||||
|
||||
/* hub should be A0 (the intra-A hub with NA-1 strong edges) */
|
||||
CHECK(g->hub_id && strcmp(g->hub_id,"A0")==0, "hub = the relational center A0");
|
||||
|
||||
/* membership: seed A0 == 1.0; A-members strong, B-members (if any) weaker */
|
||||
double seedw=-1, minA=2, maxB=-1; int na=0,nb=0;
|
||||
for(int i=0;i<g->n_members;i++){
|
||||
const char* id=g->members[i].id; double w=g->members[i].membership;
|
||||
if(strcmp(id,"A0")==0) seedw=w;
|
||||
if(id[0]=='A'){ na++; if(w<minA)minA=w; }
|
||||
if(id[0]=='B'){ nb++; if(w>maxB)maxB=w; }
|
||||
}
|
||||
printf(" A-members=%d B-members=%d seedw=%.3f\n", na,nb,seedw);
|
||||
CHECK(fabs(seedw-1.0)<1e-9, "seed membership == 1.0");
|
||||
CHECK(na>=NA-1, "neighborhood recovers cluster A");
|
||||
|
||||
/* skeleton = the strong intra-A edges: every edge eff_weight>=threshold,
|
||||
* and edges connect A-nodes (co-registration should be positive: wired
|
||||
* pairs are semantically near). */
|
||||
int allstrong=1, allA=1;
|
||||
for(int e=0;e<g->n_edges;e++){
|
||||
if(g->edges[e].eff_weight < P.edge_min_weight) allstrong=0;
|
||||
const char* a=g->members[g->edges[e].a].id, *b=g->members[g->edges[e].b].id;
|
||||
if(!(a[0]=='A'&&b[0]=='A')) { /* the lone eX cross edge is allowed */
|
||||
if(!((strcmp(a,"A0")==0&&strcmp(b,"B0")==0)||(strcmp(a,"B0")==0&&strcmp(b,"A0")==0))) allA=0; }
|
||||
}
|
||||
CHECK(allstrong, "skeleton holds only above-threshold (strong) edges");
|
||||
CHECK(allA, "skeleton backbone is the intra-cluster wiring");
|
||||
CHECK(g->co_registration>0.0, "co-registration positive (wired pairs are semantically near)");
|
||||
|
||||
/* principal axes: extents strictly non-increasing */
|
||||
int mono=1; for(int i=1;i<g->n_axes;i++) if(g->axes[i].extent>g->axes[i-1].extent+1e-9) mono=0;
|
||||
CHECK(g->n_axes>0 && mono, "principal axes sorted by descending extent");
|
||||
CHECK(g->radius>0, "radius positive");
|
||||
}
|
||||
engram_geo_free(g);
|
||||
|
||||
/* edge cases: NULL store, no seeds, relational-only (NULL vindex) */
|
||||
CHECK(engram_geometry_descriptor(NULL,ix,ids,nids,seeds,1,&P,gm)==NULL, "NULL store -> NULL");
|
||||
CHECK(engram_geometry_descriptor(st,ix,ids,nids,seeds,0,&P,gm)==NULL, "zero seeds -> NULL");
|
||||
GeoDescriptor* g2=engram_geometry_descriptor(st, NULL, NULL, 0, seeds, 1, &P, gm);
|
||||
CHECK(g2!=NULL && g2->n_members>=NA-1, "relational-only path (no vindex) works");
|
||||
engram_geo_free(g2);
|
||||
/* raw (uncentered) mode still supported: global_mean=NULL -> no offset recorded */
|
||||
GeoDescriptor* g3=engram_geometry_descriptor(st, ix, ids, nids, seeds, 1, &P, NULL);
|
||||
CHECK(g3!=NULL && g3->global_mean==NULL, "raw mode (global_mean=NULL) leaves offset unset");
|
||||
engram_geo_free(g3);
|
||||
|
||||
engram_geo_mean_free(mc);
|
||||
for(int i=0;i<nids;i++) free(ids[i]); free(ids);
|
||||
vindex_free(ix); store_close(st); unlink(path);
|
||||
printf("\n=== %s ===\n", g_fail?"FAILURES PRESENT":"ALL TESTS PASSED");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/* test_interoception_p0_emb.c — M-INTEROCEPTION Priority 0.
|
||||
*
|
||||
* Verifies the new READ-ONLY builtin engram_scan_nodes_emb_json(limit,offset):
|
||||
* - every emitted node carries emb_dim and an emb JSON array of that length,
|
||||
* - nodes without an embedding emit emb_dim:0 / emb:[],
|
||||
* - pagination (limit/offset) is honoured,
|
||||
* - the count matches engram_node_count,
|
||||
* - the EXISTING engram_scan_nodes_json path is byte-unchanged (no emb field),
|
||||
* i.e. the addition is purely additive / behavior-neutral.
|
||||
*
|
||||
* Pure-C harness (no elc). We craft a snapshot with real emb vectors, load it
|
||||
* (engram_load parses "emb" comma-lists into node->emb via eg_parse_emb), then
|
||||
* dump via both scan paths. Assertions live in run_interoception_p0.sh.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
|
||||
/* 16-d embedding as a comma list (>=8 required by eg_parse_emb). */
|
||||
static void emb_list(char* out, size_t cap, int dim, double base){
|
||||
size_t o = 0;
|
||||
for (int i = 0; i < dim; i++){
|
||||
o += snprintf(out+o, cap-o, "%s%.4f", i?",":"", base + 0.01*i);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv){
|
||||
if (argc < 2){ fprintf(stderr, "usage: %s <dir>\n", argv[0]); return 2; }
|
||||
const char* dir = argv[1];
|
||||
char snap[1024]; snprintf(snap, sizeof snap, "%s/seed.json", dir);
|
||||
|
||||
char e1[512], e2[512];
|
||||
emb_list(e1, sizeof e1, 16, 0.10);
|
||||
emb_list(e2, sizeof e2, 16, 0.50);
|
||||
|
||||
/* Two embedded nodes (distinct salience → deterministic sort order) and one
|
||||
* un-embedded node. */
|
||||
FILE* f = fopen(snap, "w");
|
||||
if (!f){ perror("fopen"); return 2; }
|
||||
fprintf(f,
|
||||
"{\"nodes\":["
|
||||
"{\"id\":\"n-high\",\"content\":\"high salience embedded\",\"node_type\":\"Concept\","
|
||||
"\"label\":\"emb-high\",\"tier\":\"Semantic\",\"salience\":0.9,\"importance\":0.8,"
|
||||
"\"confidence\":1.0,\"created_at\":1000,\"emb\":\"%s\"},"
|
||||
"{\"id\":\"n-mid\",\"content\":\"mid salience embedded\",\"node_type\":\"Concept\","
|
||||
"\"label\":\"emb-mid\",\"tier\":\"Semantic\",\"salience\":0.5,\"importance\":0.5,"
|
||||
"\"confidence\":1.0,\"created_at\":2000,\"emb\":\"%s\"},"
|
||||
"{\"id\":\"n-low\",\"content\":\"low salience no embedding\",\"node_type\":\"Fact\","
|
||||
"\"label\":\"noemb-low\",\"tier\":\"Semantic\",\"salience\":0.1,\"importance\":0.2,"
|
||||
"\"confidence\":1.0,\"created_at\":3000}"
|
||||
"],\"edges\":[]}", e1, e2);
|
||||
fclose(f);
|
||||
|
||||
if (!engram_load(S(snap))){ fprintf(stderr, "load failed\n"); return 2; }
|
||||
long long nc = (long long)(int64_t)engram_node_count();
|
||||
printf("node_count=%lld\n", nc);
|
||||
|
||||
/* full page */
|
||||
el_val_t all = engram_scan_nodes_emb_json((el_val_t)256, (el_val_t)0);
|
||||
char p[1024];
|
||||
snprintf(p, sizeof p, "%s/emb_all.json", dir);
|
||||
f = fopen(p, "w"); fputs(EL_CSTR(all), f); fclose(f);
|
||||
|
||||
/* pagination: one node at offset 0 and one at offset 1 */
|
||||
el_val_t pg0 = engram_scan_nodes_emb_json((el_val_t)1, (el_val_t)0);
|
||||
el_val_t pg1 = engram_scan_nodes_emb_json((el_val_t)1, (el_val_t)1);
|
||||
snprintf(p, sizeof p, "%s/emb_pg0.json", dir); f = fopen(p, "w"); fputs(EL_CSTR(pg0), f); fclose(f);
|
||||
snprintf(p, sizeof p, "%s/emb_pg1.json", dir); f = fopen(p, "w"); fputs(EL_CSTR(pg1), f); fclose(f);
|
||||
|
||||
/* existing path — must be unchanged / carry NO emb */
|
||||
el_val_t plain = engram_scan_nodes_json((el_val_t)256, (el_val_t)0);
|
||||
snprintf(p, sizeof p, "%s/plain.json", dir); f = fopen(p, "w"); fputs(EL_CSTR(plain), f); fclose(f);
|
||||
|
||||
printf("wrote dumps to %s\n", dir);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/* test_interoception_p1_consol.c — M-INTEROCEPTION Priority 1.
|
||||
* Two-threshold consolidation (ENGRAM_CONSOLIDATION, default OFF).
|
||||
*
|
||||
* Modes:
|
||||
* accrual — flag OFF (pure trunk). Drive N co-activations of a WIRED pair and
|
||||
* print act-stats at sampled N so the run script can plot the
|
||||
* hebb accrual curve (headline measurement). No consolidation code
|
||||
* runs; this measures the EXISTING EWMA accrual.
|
||||
* connect — flag ON. Seed, activate to populate WM, then create a STRONG ISE
|
||||
* (connects to wm_top) and a WEAK ISE (below the bar → nothing).
|
||||
* Exports the graph so edges from each ISE can be counted.
|
||||
* perm — flag ON. Load two OLD InternalStateEvent nodes; promote one to
|
||||
* permanence; prune telemetry; export so the durable one is shown
|
||||
* to survive while the ephemeral one is swept.
|
||||
* offcheck — flag OFF. Prove creating an ISE forms NO edges and
|
||||
* engram_consolidate_permanence is a no-op (byte-identical OFF path).
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
static el_val_t F(double d){ return el_from_float(d); }
|
||||
|
||||
static void build_seed(void){
|
||||
el_val_t a = engram_node_full(S("hebbian potentiation strengthens co-active memory links"),
|
||||
S("Concept"), S("hebb-a"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
||||
S("hebbian,memory,activation"));
|
||||
el_val_t b = engram_node_full(S("co-active memory links accrue hebbian associative weight"),
|
||||
S("Concept"), S("hebb-b"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
||||
S("hebbian,memory,weight"));
|
||||
el_val_t c = engram_node_full(S("unrelated culinary recipe for sourdough bread"),
|
||||
S("Fact"), S("distractor-1"), F(0.4), F(0.4), F(1.0), S("Semantic"), S("food"));
|
||||
el_val_t d = engram_node_full(S("the weather forecast predicts rain tomorrow afternoon"),
|
||||
S("Fact"), S("distractor-2"), F(0.4), F(0.4), F(1.0), S("Semantic"), S("weather"));
|
||||
engram_connect(a, b, F(0.8), S("associate"));
|
||||
engram_connect(a, c, F(0.3), S("associate"));
|
||||
engram_connect(b, d, F(0.3), S("associate"));
|
||||
}
|
||||
static const char* QUERY =
|
||||
"hebbian potentiation co-active memory links associative weight";
|
||||
|
||||
int main(int argc, char** argv){
|
||||
if (argc < 3){ fprintf(stderr,"usage: %s <accrual|connect|perm|offcheck> <dir>\n",argv[0]); return 2; }
|
||||
const char* mode = argv[1];
|
||||
const char* dir = argv[2];
|
||||
char p[1024];
|
||||
|
||||
if (!strcmp(mode,"accrual")){
|
||||
build_seed();
|
||||
int samples[] = {1,10,50,100,250,500,1000,1625,2000,2500,3000};
|
||||
int ns = (int)(sizeof samples/sizeof samples[0]);
|
||||
int NMAX = samples[ns-1];
|
||||
int si = 0;
|
||||
for (int n=1; n<=NMAX; n++){
|
||||
engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
if (si<ns && n==samples[si]){
|
||||
printf("SAMPLE %d %s\n", n, EL_CSTR(engram_act_stats_json()));
|
||||
si++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!strcmp(mode,"connect")){
|
||||
build_seed();
|
||||
engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
long long e_before = (long long)(int64_t)engram_edge_count();
|
||||
/* STRONG ISE — should connect to wm_top */
|
||||
el_val_t ise_strong = engram_node_full(S("strong internal state: focused on hebbian consolidation"),
|
||||
S("InternalStateEvent"), S("ise-strong"), F(0.9), F(0.8), F(1.0), S("Working"), S("ise"));
|
||||
long long e_after_strong = (long long)(int64_t)engram_edge_count();
|
||||
/* WEAK ISE — below the connection bar (salience 0.3 < default 0.6) */
|
||||
el_val_t ise_weak = engram_node_full(S("weak internal state: idle drift"),
|
||||
S("InternalStateEvent"), S("ise-weak"), F(0.3), F(0.3), F(1.0), S("Working"), S("ise"));
|
||||
long long e_after_weak = (long long)(int64_t)engram_edge_count();
|
||||
printf("ISE_STRONG_ID %s\n", EL_CSTR(ise_strong));
|
||||
printf("ISE_WEAK_ID %s\n", EL_CSTR(ise_weak));
|
||||
printf("EDGES before=%lld after_strong=%lld after_weak=%lld\n",
|
||||
e_before, e_after_strong, e_after_weak);
|
||||
snprintf(p,sizeof p,"%s/connect.json",dir);
|
||||
el_val_t g = engram_save(S(p)); (void)g;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!strcmp(mode,"perm")){
|
||||
/* Two OLD ISE nodes (created_at far in the past → prunable at 48h). */
|
||||
snprintf(p,sizeof p,"%s/seed.json",dir);
|
||||
FILE* f=fopen(p,"w");
|
||||
fprintf(f,"{\"nodes\":["
|
||||
"{\"id\":\"ise-durable\",\"content\":\"promoted internal state\",\"node_type\":\"InternalStateEvent\","
|
||||
"\"label\":\"ise-durable\",\"salience\":0.5,\"confidence\":1.0,\"created_at\":1000},"
|
||||
"{\"id\":\"ise-ephemeral\",\"content\":\"transient internal state\",\"node_type\":\"InternalStateEvent\","
|
||||
"\"label\":\"ise-ephemeral\",\"salience\":0.5,\"confidence\":1.0,\"created_at\":1000}"
|
||||
"],\"edges\":[]}");
|
||||
fclose(f);
|
||||
if(!engram_load(S(p))){ fprintf(stderr,"load failed\n"); return 2; }
|
||||
long long n_before = (long long)(int64_t)engram_node_count();
|
||||
el_val_t promoted = engram_consolidate_permanence(S("ise-durable"));
|
||||
long long removed = (long long)(int64_t)engram_prune_telemetry((el_val_t)0); /* default 48h */
|
||||
long long n_after = (long long)(int64_t)engram_node_count();
|
||||
printf("PROMOTED %lld\n", (long long)(int64_t)promoted);
|
||||
printf("NODES before=%lld after=%lld removed=%lld\n", n_before, n_after, removed);
|
||||
printf("DURABLE_NODE %s\n", EL_CSTR(engram_get_node_json(S("ise-durable"))));
|
||||
printf("EPHEMERAL_NODE %s\n", EL_CSTR(engram_get_node_json(S("ise-ephemeral"))));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!strcmp(mode,"offcheck")){
|
||||
build_seed();
|
||||
engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
long long e_before = (long long)(int64_t)engram_edge_count();
|
||||
engram_node_full(S("strong internal state with flag OFF"),
|
||||
S("InternalStateEvent"), S("ise-off"), F(0.9), F(0.8), F(1.0), S("Working"), S("ise"));
|
||||
long long e_after = (long long)(int64_t)engram_edge_count();
|
||||
el_val_t perm = engram_consolidate_permanence(S("ise-off"));
|
||||
printf("OFF edges before=%lld after=%lld perm_ret=%lld\n",
|
||||
e_before, e_after, (long long)(int64_t)perm);
|
||||
return (e_before==e_after && (int64_t)perm==0) ? 0 : 1;
|
||||
}
|
||||
|
||||
fprintf(stderr,"unknown mode %s\n",mode); return 2;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* test_interoception_p2_chrono.c — M-INTEROCEPTION Priority 2.
|
||||
* Chronoception: engram_age_field(delta_ms) + reboot catch-up
|
||||
* (ENGRAM_CHRONOCEPTION, default OFF).
|
||||
*
|
||||
* Uses loaded snapshots with KNOWN working_memory_weight / background_activation
|
||||
* so the field is deterministic without depending on activation. (engram_load
|
||||
* halves WM on boot — the laundering step — so snapshot wm 1.0 -> 0.5 resident.)
|
||||
*
|
||||
* Modes:
|
||||
* once <dir> <dt_ms> — age the field once by dt; save field.json.
|
||||
* split <dir> <dt_ms> <N> — age by dt/N, N times; save field.json.
|
||||
* (once vs split must match: scale-invariance.)
|
||||
* catchup <dir> <gap_ms> — write a last-tick gap_ms in the past, then
|
||||
* engram_age_field_catchup(); print MAGNITUDE.
|
||||
* offcheck <dir> <dt_ms> — flag OFF: age returns 0 and field is untouched.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
|
||||
static void write_seed(const char* dir){
|
||||
char p[1024]; snprintf(p,sizeof p,"%s/seed.json",dir);
|
||||
FILE* f=fopen(p,"w");
|
||||
fprintf(f,"{\"nodes\":["
|
||||
"{\"id\":\"f1\",\"content\":\"field node 1\",\"node_type\":\"Concept\",\"label\":\"f1\","
|
||||
"\"salience\":0.9,\"confidence\":1.0,\"working_memory_weight\":1.0,\"background_activation\":0.5},"
|
||||
"{\"id\":\"f2\",\"content\":\"field node 2\",\"node_type\":\"Concept\",\"label\":\"f2\","
|
||||
"\"salience\":0.8,\"confidence\":1.0,\"working_memory_weight\":0.8,\"background_activation\":0.4},"
|
||||
"{\"id\":\"f3\",\"content\":\"field node 3\",\"node_type\":\"Concept\",\"label\":\"f3\","
|
||||
"\"salience\":0.7,\"confidence\":1.0,\"working_memory_weight\":0.6,\"background_activation\":0.3}"
|
||||
"],\"edges\":[]}");
|
||||
fclose(f);
|
||||
}
|
||||
static void load_seed(const char* dir){
|
||||
char p[1024]; snprintf(p,sizeof p,"%s/seed.json",dir);
|
||||
write_seed(dir);
|
||||
if(!engram_load(S(p))){ fprintf(stderr,"load failed\n"); exit(2); }
|
||||
}
|
||||
static void save_field(const char* dir){
|
||||
char p[1024]; snprintf(p,sizeof p,"%s/field.json",dir);
|
||||
engram_save(S(p));
|
||||
}
|
||||
|
||||
int main(int argc,char** argv){
|
||||
if(argc<3){ fprintf(stderr,"usage: %s <once|split|catchup|offcheck> <dir> ...\n",argv[0]); return 2; }
|
||||
const char* mode=argv[1];
|
||||
const char* dir =argv[2];
|
||||
|
||||
if(!strcmp(mode,"once")){
|
||||
double dt=atof(argv[3]);
|
||||
load_seed(dir);
|
||||
el_val_t mag=engram_age_field((el_val_t)(int64_t)dt);
|
||||
printf("MAGNITUDE %.10f\n", el_to_float(mag));
|
||||
save_field(dir);
|
||||
return 0;
|
||||
}
|
||||
if(!strcmp(mode,"split")){
|
||||
double dt=atof(argv[3]); int N=atoi(argv[4]); if(N<1)N=1;
|
||||
load_seed(dir);
|
||||
double sub=dt/(double)N;
|
||||
for(int i=0;i<N;i++) engram_age_field((el_val_t)(int64_t)sub);
|
||||
save_field(dir);
|
||||
printf("SPLIT dt=%.0f N=%d sub=%.4f\n", dt, N, sub);
|
||||
return 0;
|
||||
}
|
||||
if(!strcmp(mode,"catchup")){
|
||||
double gap=atof(argv[3]);
|
||||
load_seed(dir);
|
||||
/* Write a last-tick gap_ms in the past. ENGRAM_DATA_DIR is set == dir by
|
||||
* the runner, so the sidecar the runtime reads is <dir>/chrono_last_tick. */
|
||||
struct timeval tv; gettimeofday(&tv,NULL);
|
||||
long long now_ms=(long long)tv.tv_sec*1000+tv.tv_usec/1000;
|
||||
long long last=now_ms-(long long)gap;
|
||||
char p[1200]; snprintf(p,sizeof p,"%s/chrono_last_tick",dir);
|
||||
FILE* f=fopen(p,"w"); fprintf(f,"%lld\n",last); fclose(f);
|
||||
el_val_t mag=engram_age_field_catchup();
|
||||
printf("CATCHUP_MAGNITUDE %.10f\n", el_to_float(mag));
|
||||
save_field(dir);
|
||||
return 0;
|
||||
}
|
||||
if(!strcmp(mode,"offcheck")){
|
||||
double dt=atof(argv[3]);
|
||||
load_seed(dir);
|
||||
el_val_t mag=engram_age_field((el_val_t)(int64_t)dt);
|
||||
el_val_t magc=engram_age_field_catchup();
|
||||
printf("OFF age_mag=%.10f catchup_mag=%.10f\n", el_to_float(mag), el_to_float(magc));
|
||||
save_field(dir);
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr,"unknown mode %s\n",mode); return 2;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/* test_interoception_p3_drift.c — M-INTEROCEPTION Priority 3 (PARTIAL).
|
||||
* Drift-sensor primitive engram_geo_displacement: GROWTH vs CORRUPTION split.
|
||||
*
|
||||
* Constructs synthetic GeoDescriptors (the struct is public) — a baseline and
|
||||
* two perturbations — and checks the sensor reports LOW core-displacement for a
|
||||
* periphery-only change (growth) and HIGH core-displacement for a core change
|
||||
* (corruption). No store / embeddings needed: this exercises the primitive in
|
||||
* isolation, which is the honest scope given there is no persisted SelfAnchor
|
||||
* yet (see engram_geometry.c). */
|
||||
#include "engram_geometry.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static GeoMember MK(const char* id, double centrality, double dist){
|
||||
GeoMember m; memset(&m,0,sizeof m);
|
||||
m.id=strdup(id); m.centrality=centrality; m.dist_centroid=dist;
|
||||
m.membership=1.0; m.embedded=1; return m;
|
||||
}
|
||||
/* 4 members: 2 core (high centrality), 2 periphery (low). */
|
||||
static GeoDescriptor* mkdesc(float cx,float cy,double radius,
|
||||
double c1,double c2,double p1,double p2){
|
||||
GeoDescriptor* g=calloc(1,sizeof *g);
|
||||
g->dim=4;
|
||||
g->centroid=calloc(4,sizeof(float));
|
||||
g->centroid[0]=cx; g->centroid[1]=cy;
|
||||
g->radius=radius;
|
||||
g->n_members=4;
|
||||
g->members=calloc(4,sizeof(GeoMember));
|
||||
g->members[0]=MK("core1",10.0,c1);
|
||||
g->members[1]=MK("core2", 9.0,c2);
|
||||
g->members[2]=MK("per1", 1.0,p1);
|
||||
g->members[3]=MK("per2", 0.9,p2);
|
||||
return g;
|
||||
}
|
||||
|
||||
int main(void){
|
||||
GeoDisplacement d;
|
||||
/* baseline: core at 0.10, periphery at 0.50, centroid [1,0], radius 1.0 */
|
||||
GeoDescriptor* A = mkdesc(1.0f,0.0f,1.0, 0.10,0.10, 0.50,0.50);
|
||||
|
||||
/* (i) GROWTH: periphery extends 0.50->0.90; core fixed; radius grows. */
|
||||
GeoDescriptor* G = mkdesc(1.0f,0.0f,1.4, 0.10,0.10, 0.90,0.90);
|
||||
engram_geo_displacement(A,G,0.5,&d);
|
||||
printf("GROWTH centroid_sep=%.4f centroid_cos=%.4f radius_delta=%.4f core_disp=%.4f periph_disp=%.4f core_n=%d periph_n=%d\n",
|
||||
d.centroid_sep,d.centroid_cos,d.radius_delta,d.core_disp,d.periph_disp,d.core_matched,d.periph_matched);
|
||||
|
||||
/* (ii) CORRUPTION: core displaces 0.10->0.60; periphery fixed; centroid shifts. */
|
||||
GeoDescriptor* C = mkdesc(0.6f,0.4f,1.0, 0.60,0.60, 0.50,0.50);
|
||||
engram_geo_displacement(A,C,0.5,&d);
|
||||
printf("CORRUPTION centroid_sep=%.4f centroid_cos=%.4f radius_delta=%.4f core_disp=%.4f periph_disp=%.4f core_n=%d periph_n=%d\n",
|
||||
d.centroid_sep,d.centroid_cos,d.radius_delta,d.core_disp,d.periph_disp,d.core_matched,d.periph_matched);
|
||||
|
||||
/* identity: A vs A -> zero drift */
|
||||
engram_geo_displacement(A,A,0.5,&d);
|
||||
printf("IDENTITY centroid_sep=%.4f core_disp=%.4f periph_disp=%.4f\n",
|
||||
d.centroid_sep,d.core_disp,d.periph_disp);
|
||||
|
||||
engram_geo_free(A); engram_geo_free(G); engram_geo_free(C);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/* test_interoception_p4_afferent.c — M-INTEROCEPTION Priority 4.
|
||||
* Afferent input counters in engram_act_stats_json: additive observability.
|
||||
* Drives KNOWN counts and asserts the emitted counters match and are monotonic.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
static el_val_t F(double d){ return el_from_float(d); }
|
||||
|
||||
int main(void){
|
||||
/* 3 plain node creates + 2 ISE creates = 5 node_creates, 2 ise_ingests */
|
||||
el_val_t a=engram_node_full(S("alpha concept about memory and time"),S("Concept"),S("a"),F(0.9),F(0.8),F(1.0),S("Semantic"),S("x"));
|
||||
el_val_t b=engram_node_full(S("beta concept about memory and links"),S("Concept"),S("b"),F(0.9),F(0.8),F(1.0),S("Semantic"),S("x"));
|
||||
engram_node_full(S("gamma distractor"),S("Fact"),S("c"),F(0.4),F(0.4),F(1.0),S("Semantic"),S("y"));
|
||||
engram_node_full(S("heartbeat internal state one"),S("InternalStateEvent"),S("i1"),F(0.5),F(0.5),F(1.0),S("Working"),S("ise"));
|
||||
engram_node_full(S("curiosity internal state two"),S("InternalStateEvent"),S("i2"),F(0.5),F(0.5),F(1.0),S("Working"),S("ise"));
|
||||
/* 2 edge creates */
|
||||
engram_connect(a,b,F(0.8),S("associate"));
|
||||
engram_connect(b,a,F(0.3),S("associate"));
|
||||
|
||||
/* first reading (0 queries so far) */
|
||||
printf("STATS0 %s\n", EL_CSTR(engram_act_stats_json()));
|
||||
|
||||
/* 4 queries -> 4 activations */
|
||||
for(int i=0;i<4;i++) engram_activate_json(S("memory and time and links"), (el_val_t)2);
|
||||
printf("STATS1 %s\n", EL_CSTR(engram_act_stats_json()));
|
||||
|
||||
/* 3 more queries -> monotonic increase */
|
||||
for(int i=0;i<3;i++) engram_activate_json(S("memory and time and links"), (el_val_t)2);
|
||||
printf("STATS2 %s\n", EL_CSTR(engram_act_stats_json()));
|
||||
return 0;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/* test_interoception_p5_dreams.c — M-INTEROCEPTION Priority 5.
|
||||
* Dream-recall-on-wake: engram_dreams_json(since_ms). Honesty rail — only
|
||||
* curiosity_scan ISEs still resident are returned; pruned (rotated-out) ones are
|
||||
* ABSENT (never confabulated); heartbeat ISEs are excluded.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
|
||||
int main(int argc,char** argv){
|
||||
if(argc<2){ fprintf(stderr,"usage: %s <dir>\n",argv[0]); return 2; }
|
||||
const char* dir=argv[1];
|
||||
struct timeval tv; gettimeofday(&tv,NULL);
|
||||
long long now=(long long)tv.tv_sec*1000+tv.tv_usec/1000;
|
||||
long long mid=now-3600000; /* 1h ago */
|
||||
long long ancient=1000; /* pruned by 48h retention */
|
||||
|
||||
char p[1024]; snprintf(p,sizeof p,"%s/seed.json",dir);
|
||||
FILE* f=fopen(p,"w");
|
||||
fprintf(f,"{\"nodes\":["
|
||||
"{\"id\":\"cur_old\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"old wondering\\\"}\","
|
||||
"\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld},"
|
||||
"{\"id\":\"cur_mid\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"mid wondering\\\"}\","
|
||||
"\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld},"
|
||||
"{\"id\":\"cur_recent\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"recent wondering\\\"}\","
|
||||
"\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld},"
|
||||
"{\"id\":\"hb_recent\",\"content\":\"{\\\"kind\\\":\\\"heartbeat\\\",\\\"wm\\\":3}\","
|
||||
"\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld}"
|
||||
"],\"edges\":[]}", ancient, mid, now, now);
|
||||
fclose(f);
|
||||
if(!engram_load(S(p))){ fprintf(stderr,"load failed\n"); return 2; }
|
||||
|
||||
/* before prune: all resident curiosity_scan after since=0 */
|
||||
printf("BEFORE %s\n", EL_CSTR(engram_dreams_json((el_val_t)0)));
|
||||
/* prune 48h — cur_old (ancient) rotates out */
|
||||
long long pruned=(long long)(int64_t)engram_prune_telemetry((el_val_t)0);
|
||||
printf("PRUNED %lld\n", pruned);
|
||||
printf("AFTER %s\n", EL_CSTR(engram_dreams_json((el_val_t)0)));
|
||||
/* since filter: only events created after 30 min ago -> cur_recent only */
|
||||
long long since=now-1800000;
|
||||
printf("SINCE %lld %s\n", since, EL_CSTR(engram_dreams_json((el_val_t)(int64_t)since)));
|
||||
return 0;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/* test_m35_hebb_persist.c — M3.5 PRE-FLIP GATE.
|
||||
*
|
||||
* Proves that in-place field mutations made during spreading activation — edge
|
||||
* `hebb` (+ last_fired), node `activation_count`, node working-memory weight —
|
||||
* PERSIST to the paged store and survive a restart from neuron.egm with
|
||||
* snapshot.json deleted. This is the "hebb-survives-restart" fix that gates the
|
||||
* live cutover.
|
||||
*
|
||||
* Same style as test_m3_parity.c: a REAL el-level harness linking the actual
|
||||
* el_runtime.c native engram builtins + engram_store.c, driving engram_node_full
|
||||
* / engram_connect / engram_activate_json / engram_save / engram_store_boot /
|
||||
* engram_store_checkpoint / engram_store_close directly from C. No EL interpreter.
|
||||
*
|
||||
* Modes (argv[1]), data dir (argv[2]):
|
||||
* pos_seed — ENGRAM_STORE=1: fresh store, seed a graph tuned so activation
|
||||
* co-activates a connected pair (edge hebb 0 -> ETA) and reinforces
|
||||
* nodes (activation_count 0 -> >=1, WM weight -> >0). Export the
|
||||
* post-activation resident graph to pre_reboot.json, then CHECKPOINT
|
||||
* (the M3.5 field-persist), then close.
|
||||
* pos_reboot— ENGRAM_STORE=1, snapshot.json deleted by runner: boot from
|
||||
* neuron.egm (WAL replay), export reboot.json, close. The values in
|
||||
* reboot.json are what actually survived the round-trip.
|
||||
* neg_seed — identical to pos_seed but WITHOUT the checkpoint field-persist
|
||||
* (negative control): activation mutations never reach the store.
|
||||
* neg_reboot— boot from neuron.egm, export neg_reboot.json, close.
|
||||
* offcheck — ENGRAM_STORE unset: seed+activate+checkpoint must NOT touch the
|
||||
* store (no neuron.egm, checkpoint returns 0).
|
||||
*
|
||||
* The pass/fail assertions live in run_m35_hebb_persist.sh (python over the JSON
|
||||
* exports): reboot.json must carry the learned hebb / activation_count and the
|
||||
* JSON-identical halved WM weight; neg_reboot.json must have LOST them.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern int engram_store_enabled(void);
|
||||
extern el_val_t engram_store_boot(el_val_t data_dir);
|
||||
extern el_val_t engram_store_checkpoint(void);
|
||||
extern el_val_t engram_store_close(void);
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
static el_val_t F(double d){ return el_from_float(d); }
|
||||
|
||||
/* Two nodes with DISTINCT content (so the redundancy-suppression pass cannot
|
||||
* dedup one of them away) that both match the query strongly, wired by one
|
||||
* "associate" edge. A handful of weakly-related distractors make it a real
|
||||
* graph. On activation both A and B promote to working memory and co-activate,
|
||||
* so their edge's hebb rises from 0 to ENGRAM_HEBB_ETA. */
|
||||
static void build_seed(void){
|
||||
el_val_t a = engram_node_full(S("hebbian potentiation strengthens co-active memory links"),
|
||||
S("Concept"), S("hebb-a"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
||||
S("hebbian,memory,activation"));
|
||||
el_val_t b = engram_node_full(S("co-active memory links accrue hebbian associative weight"),
|
||||
S("Concept"), S("hebb-b"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
||||
S("hebbian,memory,weight"));
|
||||
el_val_t c = engram_node_full(S("unrelated culinary recipe for sourdough bread"),
|
||||
S("Fact"), S("distractor-1"), F(0.4), F(0.4), F(1.0), S("Semantic"),
|
||||
S("food"));
|
||||
el_val_t d = engram_node_full(S("the weather forecast predicts rain tomorrow afternoon"),
|
||||
S("Fact"), S("distractor-2"), F(0.4), F(0.4), F(1.0), S("Semantic"),
|
||||
S("weather"));
|
||||
engram_connect(a, b, F(0.8), S("associate")); /* the edge under test */
|
||||
engram_connect(a, c, F(0.3), S("associate"));
|
||||
engram_connect(b, d, F(0.3), S("associate"));
|
||||
}
|
||||
|
||||
static const char* QUERY =
|
||||
"hebbian potentiation co-active memory links associative weight";
|
||||
|
||||
static void export_graph(const char* dir, const char* name){
|
||||
char p[1024];
|
||||
snprintf(p, sizeof p, "%s/%s", dir, name);
|
||||
if (!engram_save(S(p))){ fprintf(stderr, "save %s failed\n", name); exit(2); }
|
||||
}
|
||||
|
||||
int main(int argc, char** argv){
|
||||
if (argc < 3){
|
||||
fprintf(stderr, "usage: %s <pos_seed|pos_reboot|neg_seed|neg_reboot|offcheck> <dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char* mode = argv[1];
|
||||
const char* dir = argv[2];
|
||||
|
||||
if (!strcmp(mode, "pos_seed") || !strcmp(mode, "neg_seed")){
|
||||
int persist = !strcmp(mode, "pos_seed");
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "%s requires ENGRAM_STORE=1\n", mode); return 2; }
|
||||
if (!engram_store_boot(S(dir))){ fprintf(stderr, "store boot failed\n"); return 2; }
|
||||
build_seed();
|
||||
el_val_t act = engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
(void)act;
|
||||
/* Capture the post-activation resident state BEFORE persisting/closing. */
|
||||
export_graph(dir, persist ? "pre_reboot.json" : "neg_pre.json");
|
||||
printf("[%s] nodes=%lld edges=%lld\n", mode,
|
||||
(long long)(int64_t)engram_node_count(),
|
||||
(long long)(int64_t)engram_edge_count());
|
||||
if (persist){
|
||||
if (!engram_store_checkpoint()){ fprintf(stderr, "checkpoint failed\n"); return 2; }
|
||||
}
|
||||
/* neg mode: NO field-persist checkpoint. engram_store_close still flushes
|
||||
* pages, but no store_put_* ran post-creation, so the store keeps the
|
||||
* pristine creation-time field values (hebb=0, activation_count=0). */
|
||||
engram_store_close();
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "pos_reboot") || !strcmp(mode, "neg_reboot")){
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "%s requires ENGRAM_STORE=1\n", mode); return 2; }
|
||||
/* snapshot.json deleted by the runner — boot MUST come from neuron.egm. */
|
||||
if (!engram_store_boot(S(dir))){ fprintf(stderr, "reboot boot failed\n"); return 2; }
|
||||
export_graph(dir, !strcmp(mode, "pos_reboot") ? "reboot.json" : "neg_reboot.json");
|
||||
printf("[%s] nodes=%lld edges=%lld\n", mode,
|
||||
(long long)(int64_t)engram_node_count(),
|
||||
(long long)(int64_t)engram_edge_count());
|
||||
engram_store_close();
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "offcheck")){
|
||||
int en = engram_store_enabled();
|
||||
el_val_t boot = engram_store_boot(S(dir)); /* no-op with flag off */
|
||||
build_seed();
|
||||
engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
el_val_t ck = engram_store_checkpoint(); /* must be a no-op */
|
||||
printf("[offcheck] enabled=%d boot=%lld checkpoint=%lld\n",
|
||||
en, (long long)(int64_t)boot, (long long)(int64_t)ck);
|
||||
return (en == 0 && (int64_t)boot == 0 && (int64_t)ck == 0) ? 0 : 1;
|
||||
}
|
||||
fprintf(stderr, "unknown mode %s\n", mode);
|
||||
return 2;
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/* test_m3_parity.c — M3 JSON-parity gate for the ENGRAM_STORE wiring.
|
||||
*
|
||||
* This is a REAL el-level harness: it links the actual el_runtime.o (the soul's
|
||||
* native engram builtins) + engram_store.o and calls the engram_node family plus
|
||||
* engram_connect, engram_activate_json, engram_save, engram_store_boot directly. No EL interpreter
|
||||
* and no full soul build are needed — el_runtime.c compiles to a standalone .o
|
||||
* whose engram builtins operate on the process-global engram store, and the
|
||||
* string arena is inert unless el_request_start() is called, so the builtins are
|
||||
* callable straight from C (el_val_t is int64_t; EL_STR/EL_CSTR are pointer casts).
|
||||
*
|
||||
* Modes (argv[1]), data dir (argv[2]):
|
||||
* seed — ENGRAM_STORE unset: build a fixed seed graph, write snapshot.json +
|
||||
* off_graph.json (pristine, pre-activation), then activate → off_act.json.
|
||||
* on — ENGRAM_STORE=1: engram_store_boot(dir) imports snapshot.json ONCE into
|
||||
* neuron.egm and loads it resident; write on_graph.json, then activate →
|
||||
* on_act.json; checkpoint + close.
|
||||
* reboot — ENGRAM_STORE=1 with snapshot.json DELETED: boot must reload from
|
||||
* neuron.egm (WAL replay), never re-reading JSON; write reboot_graph.json.
|
||||
* offcheck — assert flag-off leaves the store untouched.
|
||||
*
|
||||
* The graph comparison (done by run_m3_parity.sh via python, modulo ordering) is
|
||||
* the deterministic gate; activation ids/promoted are compared as a robust set.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Builtins the header declares are pulled in via el_runtime.h. The M3 additions
|
||||
* are not in the header yet, so declare them here. */
|
||||
extern int engram_store_enabled(void);
|
||||
extern el_val_t engram_store_boot(el_val_t data_dir);
|
||||
extern el_val_t engram_store_checkpoint(void);
|
||||
extern el_val_t engram_store_close(void);
|
||||
extern el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
static el_val_t F(double d){ return el_from_float(d); }
|
||||
|
||||
/* Build a fixed, deterministic seed graph: 12 nodes across two layers + 9 edges.
|
||||
* Content is chosen so an activation query has real matches to rank. */
|
||||
static void build_seed(void){
|
||||
/* core-identity layer (1) via engram_node_full */
|
||||
el_val_t n0 = engram_node_full(S("tiered storage engine design"), S("Concept"),
|
||||
S("storage-engine"), F(0.9), F(0.8), F(1.0), S("Semantic"), S("design,storage"));
|
||||
el_val_t n1 = engram_node_full(S("write-ahead log durability"), S("Concept"),
|
||||
S("wal"), F(0.85), F(0.75), F(1.0), S("Semantic"), S("wal,durability"));
|
||||
el_val_t n2 = engram_node_full(S("paged buffer pool with checkpointing"), S("Concept"),
|
||||
S("buffer-pool"), F(0.8), F(0.7), F(1.0), S("Semantic"), S("paging"));
|
||||
el_val_t n3 = engram_node_full(S("spreading activation over the graph"), S("Concept"),
|
||||
S("activation"), F(0.8), F(0.7), F(1.0), S("Semantic"), S("activation,graph"));
|
||||
el_val_t n4 = engram_node_full(S("hebbian co-activation potentiation"), S("Concept"),
|
||||
S("hebbian"), F(0.7), F(0.6), F(1.0), S("Semantic"), S("hebb"));
|
||||
el_val_t n5 = engram_node_full(S("crash recovery replays the log"), S("Concept"),
|
||||
S("recovery"), F(0.75), F(0.65), F(1.0), S("Semantic"), S("recovery,wal"));
|
||||
/* domain-knowledge layer (2) via engram_node_layered */
|
||||
el_val_t n6 = engram_node_layered(S("b-tree primary index id to location"), S("Fact"),
|
||||
S("btree"), F(0.7), F(0.6), F(1.0), S(""), S("index"), (el_val_t)2);
|
||||
el_val_t n7 = engram_node_layered(S("adjacency index for edge lookup"), S("Fact"),
|
||||
S("adjacency"), F(0.7), F(0.6), F(1.0), S(""), S("index,graph"), (el_val_t)2);
|
||||
el_val_t n8 = engram_node_layered(S("slotted pages hold tlv records"), S("Fact"),
|
||||
S("slotted-page"), F(0.65), F(0.55), F(1.0), S(""), S("format"), (el_val_t)2);
|
||||
el_val_t n9 = engram_node_full(S("memory tiers working semantic episodic"), S("Concept"),
|
||||
S("tiers"), F(0.7), F(0.6), F(1.0), S("Semantic"), S("tiers,memory"));
|
||||
el_val_t n10 = engram_node_full(S("embeddings enable nearest neighbour search"), S("Concept"),
|
||||
S("embeddings"), F(0.65), F(0.55), F(1.0), S("Semantic"), S("embeddings"));
|
||||
el_val_t n11 = engram_node_full(S("the durable engram is the mind's memory"), S("Belief"),
|
||||
S("engram"), F(0.95), F(0.9), F(1.0), S("Semantic"), S("engram,memory"));
|
||||
|
||||
engram_connect(n0, n1, F(0.8), S("depends-on"));
|
||||
engram_connect(n0, n2, F(0.8), S("depends-on"));
|
||||
engram_connect(n0, n3, F(0.7), S("enables"));
|
||||
engram_connect(n1, n5, F(0.9), S("enables"));
|
||||
engram_connect(n3, n4, F(0.6), S("triggers"));
|
||||
engram_connect(n2, n6, F(0.7), S("uses"));
|
||||
engram_connect(n3, n7, F(0.7), S("uses"));
|
||||
engram_connect(n0, n8, F(0.6), S("uses"));
|
||||
engram_connect(n11, n9, F(0.8), S("about"));
|
||||
engram_connect(n11, n10, F(0.5), S("about"));
|
||||
}
|
||||
|
||||
static void write_file(const char* path, const char* content){
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f){ fprintf(stderr, "cannot open %s\n", path); exit(2); }
|
||||
if (content) fwrite(content, 1, strlen(content), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static const char* QUERY = "storage engine activation and the durable log";
|
||||
|
||||
int main(int argc, char** argv){
|
||||
if (argc < 3){ fprintf(stderr, "usage: %s <seed|on|reboot|offcheck> <dir>\n", argv[0]); return 2; }
|
||||
const char* mode = argv[1];
|
||||
const char* dir = argv[2];
|
||||
char p[1024];
|
||||
|
||||
if (!strcmp(mode, "seed")){
|
||||
if (engram_store_enabled()){ fprintf(stderr, "seed mode requires ENGRAM_STORE unset\n"); return 2; }
|
||||
build_seed();
|
||||
snprintf(p, sizeof p, "%s/snapshot.json", dir);
|
||||
if (!engram_save(S(p))){ fprintf(stderr, "seed save failed\n"); return 2; }
|
||||
snprintf(p, sizeof p, "%s/off_graph.json", dir);
|
||||
engram_save(S(p)); /* pristine off-path graph */
|
||||
el_val_t act = engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
snprintf(p, sizeof p, "%s/off_act.json", dir);
|
||||
write_file(p, EL_CSTR(act));
|
||||
printf("[seed] nodes=%lld edges=%lld\n",
|
||||
(long long)(int64_t)engram_node_count(), (long long)(int64_t)engram_edge_count());
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "on")){
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "on mode requires ENGRAM_STORE=1\n"); return 2; }
|
||||
if (!engram_store_boot(S(dir))){ fprintf(stderr, "store boot failed\n"); return 2; }
|
||||
snprintf(p, sizeof p, "%s/on_graph.json", dir);
|
||||
engram_save(S(p)); /* export resident (== store) */
|
||||
/* Checkpoint the freshly-imported (pristine) graph — this is the state
|
||||
* the reboot comparison expects to round-trip. Under M3.5 a checkpoint
|
||||
* persists the resident graph's CURRENT field state, so it must run
|
||||
* BEFORE activation mutates fields in place; activation itself is
|
||||
* exercised below only for the activation-result-set parity check. The
|
||||
* M3.5 gate (test_m35_hebb_persist) separately proves that a checkpoint
|
||||
* taken AFTER activation durably carries the learned hebb/WM state. */
|
||||
engram_store_checkpoint();
|
||||
el_val_t act = engram_activate_json(S(QUERY), (el_val_t)3);
|
||||
snprintf(p, sizeof p, "%s/on_act.json", dir);
|
||||
write_file(p, EL_CSTR(act));
|
||||
printf("[on] nodes=%lld edges=%lld\n",
|
||||
(long long)(int64_t)engram_node_count(), (long long)(int64_t)engram_edge_count());
|
||||
engram_store_close();
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "reboot")){
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "reboot mode requires ENGRAM_STORE=1\n"); return 2; }
|
||||
/* snapshot.json has been deleted by the runner — boot MUST come from
|
||||
* neuron.egm (+ WAL replay), never re-reading JSON. */
|
||||
if (!engram_store_boot(S(dir))){ fprintf(stderr, "reboot boot failed\n"); return 2; }
|
||||
snprintf(p, sizeof p, "%s/reboot_graph.json", dir);
|
||||
engram_save(S(p));
|
||||
printf("[reboot] nodes=%lld edges=%lld\n",
|
||||
(long long)(int64_t)engram_node_count(), (long long)(int64_t)engram_edge_count());
|
||||
engram_store_close();
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "offcheck")){
|
||||
/* ENGRAM_STORE unset: enabled()==0 and boot is a no-op returning 0. */
|
||||
int en = engram_store_enabled();
|
||||
el_val_t b = engram_store_boot(S(dir));
|
||||
printf("[offcheck] enabled=%d boot_ret=%lld\n", en, (long long)(int64_t)b);
|
||||
return (en == 0 && (int64_t)b == 0) ? 0 : 1;
|
||||
}
|
||||
fprintf(stderr, "unknown mode %s\n", mode);
|
||||
return 2;
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
/* test_m7_traversal.c — M7 index-driven activation traversal.
|
||||
*
|
||||
* Milestone 7 replaces the O(E) full adjacency rebuild that spreading activation
|
||||
* paid before every BFS with an incrementally-maintained per-node index, behind
|
||||
* the ENGRAM_STORE flag (flag-off = unchanged behavior). This harness links the
|
||||
* REAL el_runtime.c engram builtins (+ engram_store.c) and drives activation
|
||||
* directly — no EL interpreter, no store boot (the index optimization is a pure
|
||||
* in-RAM concern; the flag is read from the environment).
|
||||
*
|
||||
* Modes (argv[1]):
|
||||
* parity-off <dir> — ENGRAM_STORE unset: build a fixed graph, run a scripted
|
||||
* sequence of activations WITH mid-sequence edge/node
|
||||
* inserts, dump each activation's JSON to <dir>/off_actN.json.
|
||||
* parity-on <dir> — ENGRAM_STORE=1: identical graph + identical sequence,
|
||||
* dump to <dir>/on_actN.json. The runner asserts the off/on
|
||||
* files are BYTE-IDENTICAL (same activated set, weights,
|
||||
* ordering, hops, WM promotion).
|
||||
* perf <off|on> <dir> <nodes> <edges> <iters>
|
||||
* — build a large graph, then loop `iters` times doing
|
||||
* (add 1 edge + activate). Prints wall-time and the M7
|
||||
* instrumentation counters (rebuild calls / rebuild
|
||||
* edge-work / incremental appends).
|
||||
*
|
||||
* Writes ONLY under the caller-provided throwaway dir.
|
||||
*/
|
||||
#include "el_runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* M7 instrumentation getters (test-only; defined in el_runtime.c). */
|
||||
extern int64_t engram_adj_rebuild_calls(void);
|
||||
extern int64_t engram_adj_rebuild_edge_work(void);
|
||||
extern int64_t engram_adj_incr_appends(void);
|
||||
extern double engram_adj_maint_seconds(void);
|
||||
extern void engram_adj_test_force_dirty(void);
|
||||
extern int engram_store_enabled(void);
|
||||
|
||||
static el_val_t S(const char* s){ return EL_STR(s); }
|
||||
static el_val_t F(double d){ return el_from_float(d); }
|
||||
|
||||
/* Deterministic LCG so off/on processes build byte-identical graphs. */
|
||||
static uint64_t g_rng = 0x9E3779B97F4A7C15ULL;
|
||||
static void rng_seed(uint64_t s){ g_rng = s ? s : 1; }
|
||||
static uint64_t rng_next(void){ g_rng = g_rng * 6364136223846793005ULL + 1442695040888963407ULL; return g_rng >> 17; }
|
||||
|
||||
static el_val_t* g_handles = NULL; /* node id handles from engram_node_full */
|
||||
static int64_t g_nnodes = 0;
|
||||
|
||||
static void write_file(const char* path, const char* content){
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f){ fprintf(stderr, "cannot open %s\n", path); exit(2); }
|
||||
if (content) fwrite(content, 1, strlen(content), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Build `n` nodes whose content carries query-matchable tokens, then `m`
|
||||
* deterministic edges among them. Handles are retained for later connect. */
|
||||
static void build_graph(int64_t n, int64_t m){
|
||||
g_handles = malloc((size_t)n * sizeof(el_val_t));
|
||||
g_nnodes = n;
|
||||
static const char* topics[] = {
|
||||
"storage engine durable log", "spreading activation graph traversal",
|
||||
"hebbian potentiation memory", "buffer pool paging checkpoint",
|
||||
"adjacency index edge lookup", "working memory promotion",
|
||||
"b-tree primary index", "embeddings nearest neighbour" };
|
||||
for (int64_t i = 0; i < n; i++){
|
||||
char content[256];
|
||||
snprintf(content, sizeof content,
|
||||
"node %lld about %s and storage engine activation index",
|
||||
(long long)i, topics[(size_t)(i % 8)]);
|
||||
char label[32]; snprintf(label, sizeof label, "n%lld", (long long)i);
|
||||
g_handles[i] = engram_node_full(S(content), S("Concept"), S(label),
|
||||
F(0.7), F(0.6), F(1.0), S("Semantic"), S("storage,graph,index"));
|
||||
}
|
||||
for (int64_t k = 0; k < m; k++){
|
||||
int64_t a = (int64_t)(rng_next() % (uint64_t)n);
|
||||
int64_t b = (int64_t)(rng_next() % (uint64_t)n);
|
||||
if (a == b) b = (b + 1) % n;
|
||||
engram_connect(g_handles[a], g_handles[b], F(0.6), S("associate"));
|
||||
}
|
||||
}
|
||||
|
||||
static const char* Q1 = "storage engine activation and the durable log";
|
||||
static const char* Q2 = "adjacency index graph traversal";
|
||||
|
||||
/* One scripted activation with an optional forced full-rebuild first. */
|
||||
static el_val_t act(const char* q, int depth, int force_rebuild){
|
||||
if (force_rebuild) engram_adj_test_force_dirty();
|
||||
return engram_activate_json(S(q), (el_val_t)depth);
|
||||
}
|
||||
|
||||
/* Run the scripted parity sequence and dump each activation JSON. `tag` names
|
||||
* the output set. When force_rebuild is set, every activation first forces the
|
||||
* O(E) full-rebuild path (the pre-M7 "scan" behavior); otherwise the M7
|
||||
* incremental index is used. The graph build + query sequence are byte-for-byte
|
||||
* deterministic, so any difference between two runs is attributable solely to
|
||||
* the difference in adjacency maintenance (and/or the ENGRAM_STORE flag). */
|
||||
static int run_parity(const char* dir, const char* tag, int force_rebuild){
|
||||
char p[1024];
|
||||
rng_seed(0xC0FFEE123ULL);
|
||||
build_graph(60, 140);
|
||||
|
||||
el_val_t a1 = act(Q1, 3, force_rebuild);
|
||||
snprintf(p, sizeof p, "%s/%s_act1.json", dir, tag); write_file(p, EL_CSTR(a1));
|
||||
|
||||
/* Mutate the graph BETWEEN activations: this is exactly where the M7 path
|
||||
* appends incrementally while the rebuild path marks dirty + fully rebuilds.
|
||||
* Parity must hold across this divergence in HOW the index is maintained. */
|
||||
engram_connect(g_handles[0], g_handles[7], F(0.8), S("depends-on"));
|
||||
engram_connect(g_handles[7], g_handles[23], F(0.7), S("enables"));
|
||||
engram_connect(g_handles[23], g_handles[41],F(0.5), S("uses"));
|
||||
el_val_t hnew = engram_node_full(S("freshly minted storage index node about activation"),
|
||||
S("Concept"), S("nnew"), F(0.8), F(0.7), F(1.0), S("Semantic"), S("storage,index"));
|
||||
engram_connect(g_handles[0], hnew, F(0.9), S("about"));
|
||||
|
||||
el_val_t a2 = act(Q1, 3, force_rebuild);
|
||||
snprintf(p, sizeof p, "%s/%s_act2.json", dir, tag); write_file(p, EL_CSTR(a2));
|
||||
el_val_t a3 = act(Q2, 2, force_rebuild);
|
||||
snprintf(p, sizeof p, "%s/%s_act3.json", dir, tag); write_file(p, EL_CSTR(a3));
|
||||
el_val_t a4 = act(Q1, 3, force_rebuild);
|
||||
snprintf(p, sizeof p, "%s/%s_act4.json", dir, tag); write_file(p, EL_CSTR(a4));
|
||||
|
||||
printf("[parity-%s] enabled=%d force_rebuild=%d nodes=%lld edges=%lld "
|
||||
"rebuilds=%lld rebuild_edge_work=%lld incr_appends=%lld\n",
|
||||
tag, engram_store_enabled(), force_rebuild,
|
||||
(long long)(int64_t)engram_node_count(), (long long)(int64_t)engram_edge_count(),
|
||||
(long long)engram_adj_rebuild_calls(), (long long)engram_adj_rebuild_edge_work(),
|
||||
(long long)engram_adj_incr_appends());
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double now_sec(void){
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
static int run_perf(const char* dir, const char* tag, int64_t n, int64_t m, int64_t iters){
|
||||
(void)dir;
|
||||
rng_seed(0xBEEF7777ULL);
|
||||
double t_build0 = now_sec();
|
||||
build_graph(n, m);
|
||||
double t_build = now_sec() - t_build0;
|
||||
|
||||
int64_t rb0 = engram_adj_rebuild_calls();
|
||||
int64_t rw0 = engram_adj_rebuild_edge_work();
|
||||
int64_t ap0 = engram_adj_incr_appends();
|
||||
double mt0 = engram_adj_maint_seconds();
|
||||
|
||||
double t0 = now_sec();
|
||||
for (int64_t it = 0; it < iters; it++){
|
||||
/* One structural mutation per query — the curiosity-loop cadence that
|
||||
* makes the OLD path rebuild the whole adjacency before every BFS. */
|
||||
int64_t a = (int64_t)(rng_next() % (uint64_t)n);
|
||||
int64_t b = (int64_t)(rng_next() % (uint64_t)n);
|
||||
if (a == b) b = (b + 1) % n;
|
||||
engram_connect(g_handles[a], g_handles[b], F(0.6), S("associate"));
|
||||
el_val_t r = engram_activate_json(S(Q1), (el_val_t)2);
|
||||
(void)r;
|
||||
}
|
||||
double elapsed = now_sec() - t0;
|
||||
|
||||
double maint = engram_adj_maint_seconds() - mt0;
|
||||
printf("[perf-%s] flag=%d nodes=%lld edges=%lld iters=%lld build=%.3fs "
|
||||
"loop=%.3fs per_query=%.3fms adj_maint=%.4fs adj_maint_per_query=%.4fms | "
|
||||
"rebuilds=%lld rebuild_edge_work=%lld incr_appends=%lld\n",
|
||||
tag, engram_store_enabled(),
|
||||
(long long)(int64_t)engram_node_count(), (long long)(int64_t)engram_edge_count(),
|
||||
(long long)iters, t_build, elapsed, (elapsed / (double)iters) * 1e3,
|
||||
maint, (maint / (double)iters) * 1e3,
|
||||
(long long)(engram_adj_rebuild_calls() - rb0),
|
||||
(long long)(engram_adj_rebuild_edge_work() - rw0),
|
||||
(long long)(engram_adj_incr_appends() - ap0));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv){
|
||||
if (argc < 3){ fprintf(stderr, "usage: %s <parity-off|parity-on|perf> ...\n", argv[0]); return 2; }
|
||||
const char* mode = argv[1];
|
||||
|
||||
if (!strcmp(mode, "parity-off")){
|
||||
/* flag-off, rebuild path = today's scan behavior (the baseline). */
|
||||
if (engram_store_enabled()){ fprintf(stderr, "parity-off requires ENGRAM_STORE unset\n"); return 2; }
|
||||
return run_parity(argv[2], "off", 0);
|
||||
}
|
||||
if (!strcmp(mode, "parity-on-rebuild")){
|
||||
/* flag-on, but force the O(E) rebuild before each activation. */
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "parity-on-rebuild requires ENGRAM_STORE=1\n"); return 2; }
|
||||
return run_parity(argv[2], "onrb", 1);
|
||||
}
|
||||
if (!strcmp(mode, "parity-on-incr")){
|
||||
/* flag-on, M7 incremental index (the code path under test). */
|
||||
if (!engram_store_enabled()){ fprintf(stderr, "parity-on-incr requires ENGRAM_STORE=1\n"); return 2; }
|
||||
return run_parity(argv[2], "onincr", 0);
|
||||
}
|
||||
if (!strcmp(mode, "perf")){
|
||||
/* perf <off|on> <dir> <nodes> <edges> <iters> */
|
||||
if (argc < 7){ fprintf(stderr, "usage: %s perf <off|on> <dir> <nodes> <edges> <iters>\n", argv[0]); return 2; }
|
||||
const char* tag = argv[2];
|
||||
int64_t n = strtoll(argv[4], NULL, 10);
|
||||
int64_t m = strtoll(argv[5], NULL, 10);
|
||||
int64_t iters = strtoll(argv[6], NULL, 10);
|
||||
return run_perf(argv[3], tag, n, m, iters);
|
||||
}
|
||||
fprintf(stderr, "unknown mode %s\n", mode);
|
||||
return 2;
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
/* Closed-form unit tests for the REASONING layer (engram_reason.c). All inputs are
|
||||
* hand-built synthetic descriptors whose answers are known in closed form. Every
|
||||
* reasoning MODE is proven, not declared. ASan/UBSan target. */
|
||||
#include "engram_reason.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
static int failures = 0, checks = 0;
|
||||
static void ok(const char* what, int cond) {
|
||||
checks++;
|
||||
if (!cond) { failures++; printf(" FAIL: %s\n", what); }
|
||||
else printf(" ok: %s\n", what);
|
||||
}
|
||||
static void approx(const char* what, double got, double exp, double tol) {
|
||||
ok(what, fabs(got - exp) <= tol);
|
||||
if (fabs(got - exp) > tol) printf(" got=%.9g exp=%.9g\n", got, exp);
|
||||
}
|
||||
|
||||
/* ── descriptor builders (mirror scratchpad/test_geo_ops.c) ─────────────────── */
|
||||
static float* vec(const double* v, int dim) {
|
||||
float* f = malloc((size_t)dim * sizeof(float));
|
||||
for (int i = 0; i < dim; i++) f[i] = (float)v[i];
|
||||
return f;
|
||||
}
|
||||
static GeoDescriptor* mk(int dim, const double* centroid,
|
||||
int n_axes, const double* axis_flat, const double* extents,
|
||||
int n_members, const char** ids, double total_var) {
|
||||
GeoDescriptor* g = calloc(1, sizeof(GeoDescriptor));
|
||||
g->dim = dim;
|
||||
g->centroid = centroid ? vec(centroid, dim) : NULL;
|
||||
g->global_mean = NULL;
|
||||
g->n_axes = n_axes;
|
||||
g->axes = n_axes ? calloc((size_t)n_axes, sizeof(GeoAxis)) : NULL;
|
||||
double tr = 0;
|
||||
for (int k = 0; k < n_axes; k++) {
|
||||
g->axes[k].axis = vec(&axis_flat[(size_t)k * dim], dim);
|
||||
g->axes[k].extent = extents[k];
|
||||
tr += extents[k] * extents[k];
|
||||
}
|
||||
g->total_variance = (total_var >= 0) ? total_var : tr;
|
||||
g->radius = sqrt(g->total_variance > 0 ? g->total_variance : 0);
|
||||
g->n_members = n_members; g->n_embedded = n_members;
|
||||
g->members = n_members ? calloc((size_t)n_members, sizeof(GeoMember)) : NULL;
|
||||
for (int i = 0; i < n_members; i++) {
|
||||
g->members[i].id = strdup(ids[i]);
|
||||
g->members[i].membership = 1.0;
|
||||
g->members[i].centrality = (double)(n_members - i);
|
||||
g->members[i].embedded = 1;
|
||||
}
|
||||
g->hub_id = n_members ? strdup(ids[0]) : strdup("");
|
||||
g->k_core = 1; g->co_registration = 0.0; g->n_edges = 0; g->edges = NULL;
|
||||
return g;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("== REASONING layer unit tests ==\n");
|
||||
|
||||
/* ══════════════════ ANALOGY — recover an affine A→B, apply to C ══════════ */
|
||||
/* A→B is a +90° rotation in the e0-e1 plane ((x,y)→(-y,x)) plus a +5 shift in e2.
|
||||
* A frame = (e0,e1); B frame = rotated (e1,-e0); cB = R·cA + t. Predict D from C. */
|
||||
{
|
||||
int dim = 4;
|
||||
double cA[4] = {1,0,0,0};
|
||||
double cB[4] = {0,1,5,0}; /* R·(1,0,0,0)=(0,1,0,0) + (0,0,5,0) */
|
||||
double cC[4] = {2,0,0,0};
|
||||
double axA[8] = {1,0,0,0, 0,1,0,0}; double exA[2] = {1,1};
|
||||
double axB[8] = {0,1,0,0, -1,0,0,0}; double exB[2] = {1,1}; /* R·e0, R·e1 */
|
||||
double axC[8] = {1,0,0,0, 0,1,0,0}; double exC[2] = {1,1};
|
||||
const char* idA[1] = {"A"}, *idB[1] = {"B"}, *idC[1] = {"C"};
|
||||
GeoDescriptor* A = mk(dim, cA, 2, axA, exA, 1, idA, -1);
|
||||
GeoDescriptor* B = mk(dim, cB, 2, axB, exB, 1, idB, -1);
|
||||
GeoDescriptor* C = mk(dim, cC, 2, axC, exC, 1, idC, -1);
|
||||
/* candidates: the true D + two distractors. true D = R·cC + t = (0,2,5,0). */
|
||||
double d_true[4] = {0,2,5,0}, d_far1[4] = {9,9,9,9}, d_far2[4] = {0,0,0,0};
|
||||
const char* idD[1] = {"Dt"}, *idF1[1] = {"F1"}, *idF2[1] = {"F2"};
|
||||
GeoDescriptor* Dt = mk(dim, d_true, 0, NULL, NULL, 1, idD, 0.0);
|
||||
GeoDescriptor* F1 = mk(dim, d_far1, 0, NULL, NULL, 1, idF1, 0.0);
|
||||
GeoDescriptor* F2 = mk(dim, d_far2, 0, NULL, NULL, 1, idF2, 0.0);
|
||||
const GeoDescriptor* cand[3] = {F1, Dt, F2}; /* true one at index 1 */
|
||||
GeoAnalogyResult res;
|
||||
int rc = engram_reason_analogy(A, B, C, cand, 3, &res);
|
||||
ok("analogy returns 0", rc == 0);
|
||||
printf("[analogy] residual=%.6f mapped=(%.4f,%.4f,%.4f,%.4f) best=%d bd=%.5f\n",
|
||||
res.analogy_residual, res.mapped_point[0], res.mapped_point[1],
|
||||
res.mapped_point[2], res.mapped_point[3], res.best, res.best_distance);
|
||||
approx("procrustes residual ~0", res.analogy_residual, 0.0, 1e-4);
|
||||
approx("mapped.x=0", res.mapped_point[0], 0.0, 1e-4);
|
||||
approx("mapped.y=2", res.mapped_point[1], 2.0, 1e-4);
|
||||
approx("mapped.z(e2)=5", res.mapped_point[2], 5.0, 1e-4);
|
||||
ok("nearest candidate = true D (idx 1)", res.best == 1);
|
||||
approx("best distance ~0", res.best_distance, 0.0, 1e-3);
|
||||
engram_reason_analogy_free(&res);
|
||||
engram_geo_free(A); engram_geo_free(B); engram_geo_free(C);
|
||||
engram_geo_free(Dt); engram_geo_free(F1); engram_geo_free(F2);
|
||||
}
|
||||
|
||||
/* ══════════════════ INDUCTION — recover a shared subspace + membership ═══ */
|
||||
/* 3 examples all spread over span(e0,e1) (ext 1 & 0.8), each with a small
|
||||
* idiosyncratic axis (e2 or e3, ext 0.2). Centroids all 0. The induced rule's
|
||||
* top-2 axes must lie in span(e0,e1); a held-out in-plane point fits, an
|
||||
* off-subspace point does not. */
|
||||
{
|
||||
int dim = 4;
|
||||
double c0[4] = {0,0,0,0};
|
||||
double axsh[8] = {1,0,0,0, 0,1,0,0}; double exsh[2] = {1.0, 0.8};
|
||||
double ax1[12] = {1,0,0,0, 0,1,0,0, 0,0,1,0}; double ex1[3] = {1.0,0.8,0.2}; /* +e2 */
|
||||
double ax2[12] = {1,0,0,0, 0,1,0,0, 0,0,0,1}; double ex2[3] = {1.0,0.8,0.2}; /* +e3 */
|
||||
const char* i1[2] = {"e1a","e1b"}, *i2[2] = {"e2a","e2b"}, *i3[2] = {"e3a","e3b"};
|
||||
GeoDescriptor* E1 = mk(dim, c0, 3, ax1, ex1, 2, i1, -1);
|
||||
GeoDescriptor* E2 = mk(dim, c0, 3, ax2, ex2, 2, i2, -1);
|
||||
GeoDescriptor* E3 = mk(dim, c0, 2, axsh, exsh, 2, i3, -1);
|
||||
const GeoDescriptor* ex[3] = {E1, E2, E3};
|
||||
GeoInduction ind;
|
||||
int rc = engram_reason_induce(ex, 3, 8, 1.0, &ind);
|
||||
ok("induce returns 0", rc == 0);
|
||||
printf("[induction] rule n_axes=%d ext0=%.4f ext1=%.4f\n",
|
||||
ind.rule->n_axes, ind.rule->n_axes > 0 ? ind.rule->axes[0].extent : 0,
|
||||
ind.rule->n_axes > 1 ? ind.rule->axes[1].extent : 0);
|
||||
/* top-2 axes lie in span(e0,e1): their e2,e3 components ~0. */
|
||||
int inplane = 1;
|
||||
for (int k = 0; k < 2 && k < ind.rule->n_axes; k++) {
|
||||
const float* a = ind.rule->axes[k].axis;
|
||||
printf(" axis%d=(%.3f,%.3f,%.3f,%.3f) ext=%.4f\n", k, a[0],a[1],a[2],a[3], ind.rule->axes[k].extent);
|
||||
if (fabs(a[2]) > 0.06 || fabs(a[3]) > 0.06) inplane = 0;
|
||||
}
|
||||
ok("induced top-2 axes lie in shared span(e0,e1)", inplane);
|
||||
approx("dominant extent ~1.0", ind.rule->axes[0].extent, 1.0, 0.06);
|
||||
approx("second extent ~0.8", ind.rule->axes[1].extent, 0.8, 0.06);
|
||||
/* membership: in-plane near-centroid positive fits; off-subspace negative doesn't. */
|
||||
float xpos[4] = {0.3f, -0.2f, 0, 0};
|
||||
float xneg[4] = {0, 0, 3.0f, 0}; /* large along e2 — outside the rule */
|
||||
float xfar[4] = {5.0f, 0, 0, 0}; /* in-plane but far — Mahalanobis blows up */
|
||||
double mp = engram_reason_membership(&ind, xpos);
|
||||
double mn = engram_reason_membership(&ind, xneg);
|
||||
double mf = engram_reason_membership(&ind, xfar);
|
||||
printf("[induction] membership pos=%.4f neg=%.4f far=%.4f\n", mp, mn, mf);
|
||||
ok("held-out positive fits (>0.5)", mp > 0.5);
|
||||
ok("off-subspace negative rejected (<0.3)", mn < 0.3);
|
||||
ok("in-plane-but-far rejected (<0.3)", mf < 0.3);
|
||||
ok("positive fits far better than negative", mp > mn + 0.4);
|
||||
engram_reason_induction_free(&ind);
|
||||
engram_geo_free(E1); engram_geo_free(E2); engram_geo_free(E3);
|
||||
}
|
||||
|
||||
/* ══════════════════ ABDUCTION — pick the best-explaining structure ═══════ */
|
||||
/* obs planted near H1's centroid among 3 candidate structures. */
|
||||
{
|
||||
int dim = 4;
|
||||
double h0[4] = {0,0,0,0}, h1[4] = {5,0,0,0}, h2[4] = {0,5,0,0};
|
||||
double ax[8] = {1,0,0,0, 0,1,0,0}; double ex[2] = {1,1};
|
||||
const char* n0[1] = {"H0"}, *n1[1] = {"H1"}, *n2[1] = {"H2"};
|
||||
GeoDescriptor* H0 = mk(dim, h0, 2, ax, ex, 1, n0, -1);
|
||||
GeoDescriptor* H1 = mk(dim, h1, 2, ax, ex, 1, n1, -1);
|
||||
GeoDescriptor* H2 = mk(dim, h2, 2, ax, ex, 1, n2, -1);
|
||||
const GeoDescriptor* H[3] = {H0, H1, H2};
|
||||
float obs[4] = {5.2f, 0.1f, 0, 0}; /* sits inside H1 */
|
||||
GeoAbduction ab;
|
||||
int rc = engram_reason_abduce(obs, dim, H, 3, 1.0, &ab);
|
||||
ok("abduce returns 0", rc == 0);
|
||||
printf("[abduction] best=%d best_score=%.4f rank=[%d,%d,%d] d=[%.3f,%.3f,%.3f]\n",
|
||||
ab.best, ab.best_score, ab.rank[0], ab.rank[1], ab.rank[2],
|
||||
ab.distances[0], ab.distances[1], ab.distances[2]);
|
||||
ok("best explanation = H1", ab.best == 1);
|
||||
ok("rank[0] = H1", ab.rank[0] == 1);
|
||||
ok("H1 has smallest distance", ab.distances[1] < ab.distances[0] && ab.distances[1] < ab.distances[2]);
|
||||
engram_reason_abduction_free(&ab);
|
||||
engram_geo_free(H0); engram_geo_free(H1); engram_geo_free(H2);
|
||||
}
|
||||
|
||||
/* ══════════════════ CAUSAL — direction + confounder flag ═════════════════ */
|
||||
/* Chain A→B→C along e0 (temporal 1<2<3). Confounder Z (e1) injects into A and
|
||||
* drives D (t=4). A–D correlate only via Z ⇒ must be flagged CONFOUNDED. */
|
||||
{
|
||||
int dim = 4;
|
||||
double cA[4] = {1,1,0,0}; /* e0 (chain) + e1 (confounder leak) */
|
||||
double cB[4] = {1,0,0,0}; /* e0 */
|
||||
double cC[4] = {2,0,0,0}; /* e0 */
|
||||
double cD[4] = {0,1,0,0}; /* e1 only — driven by Z */
|
||||
double cZ[4] = {0,1,0,0}; /* confounder centroid */
|
||||
double axZ[4] = {0,1,0,0}; double exZ[1] = {1}; /* Z's subspace = e1 */
|
||||
const char* idA[1]={"A"},*idB[1]={"B"},*idC[1]={"C"},*idD[1]={"D"},*idZ[1]={"Z"};
|
||||
GeoDescriptor* A = mk(dim, cA, 0, NULL, NULL, 1, idA, 0.0);
|
||||
GeoDescriptor* B = mk(dim, cB, 0, NULL, NULL, 1, idB, 0.0);
|
||||
GeoDescriptor* C = mk(dim, cC, 0, NULL, NULL, 1, idC, 0.0);
|
||||
GeoDescriptor* D = mk(dim, cD, 0, NULL, NULL, 1, idD, 0.0);
|
||||
GeoDescriptor* Z = mk(dim, cZ, 1, axZ, exZ, 1, idZ, -1);
|
||||
const GeoDescriptor* conf[1] = {Z};
|
||||
|
||||
GeoCausal ab, bc, ad, bd;
|
||||
engram_reason_causal(A, B, conf, 1, /*t*/1, 2, 0.5, &ab);
|
||||
engram_reason_causal(B, C, conf, 1, 2, 3, 0.5, &bc);
|
||||
engram_reason_causal(A, D, conf, 1, 1, 4, 0.5, &ad);
|
||||
engram_reason_causal(B, D, conf, 1, 2, 4, 0.5, &bd);
|
||||
printf("[causal] A->B: raw=%.3f ctrl=%.3f dir=%d verdict=%d strength=%.3f\n",
|
||||
ab.assoc_raw, ab.assoc_controlled, ab.temporal_dir, ab.verdict, ab.strength);
|
||||
printf("[causal] B->C: raw=%.3f ctrl=%.3f dir=%d verdict=%d\n", bc.assoc_raw, bc.assoc_controlled, bc.temporal_dir, bc.verdict);
|
||||
printf("[causal] A--D: raw=%.3f ctrl=%.3f dir=%d verdict=%d confounded=%d\n",
|
||||
ad.assoc_raw, ad.assoc_controlled, ad.temporal_dir, ad.verdict, ad.confounded);
|
||||
printf("[causal] B--D: raw=%.3f verdict=%d\n", bd.assoc_raw, bd.verdict);
|
||||
ok("A->B DIRECTED", ab.verdict == GEO_CAUSAL_DIRECTED);
|
||||
ok("A->B direction A precedes B", ab.temporal_dir == 1);
|
||||
ok("A->B association survives control (ctrl high)", ab.assoc_controlled > 0.6);
|
||||
ok("B->C DIRECTED", bc.verdict == GEO_CAUSAL_DIRECTED);
|
||||
ok("A--D CONFOUNDED (flagged)", ad.verdict == GEO_CAUSAL_CONFOUNDED && ad.confounded == 1);
|
||||
ok("A--D raw correlated but control kills it", ad.assoc_raw > 0.6 && ad.assoc_controlled < 0.2);
|
||||
ok("B--D NONE (no association at all)", bd.verdict == GEO_CAUSAL_NONE);
|
||||
engram_geo_free(A); engram_geo_free(B); engram_geo_free(C); engram_geo_free(D); engram_geo_free(Z);
|
||||
}
|
||||
|
||||
/* ══════════════════ PLANNING — geodesic path along a curved manifold ═════ */
|
||||
/* 6 neighborhoods on a semicircle (radius 10). Consecutive chord ~6.18,
|
||||
* skip-one ~11.76, endpoints ~20. neighbor_radius=7 admits only consecutive
|
||||
* hops ⇒ the plan must traverse the whole arc 0→1→2→3→4→5. */
|
||||
{
|
||||
int dim = 4; int N = 6; double R = 10.0;
|
||||
GeoDescriptor* nodes[6];
|
||||
char nm[6][8];
|
||||
for (int k = 0; k < N; k++) {
|
||||
double th = M_PI * (double)k / (double)(N - 1);
|
||||
double c[4] = { R * cos(th), R * sin(th), 0, 0 };
|
||||
snprintf(nm[k], sizeof nm[k], "n%d", k);
|
||||
const char* id[1] = { nm[k] };
|
||||
nodes[k] = mk(dim, c, 0, NULL, NULL, 1, id, 0.0);
|
||||
}
|
||||
const GeoDescriptor* cn[6];
|
||||
for (int k = 0; k < N; k++) cn[k] = nodes[k];
|
||||
GeoPlan plan;
|
||||
int rc = engram_reason_plan(cn, N, 0, 5, 7.0, 0, &plan);
|
||||
ok("plan returns 0", rc == 0);
|
||||
printf("[planning] reached=%d len=%d cost=%.4f path=[", plan.reached, plan.path_len, plan.total_cost);
|
||||
for (int i = 0; i < plan.path_len; i++) printf("%s%d", i ? "," : "", plan.path[i]);
|
||||
printf("]\n");
|
||||
ok("goal reached", plan.reached == 1);
|
||||
ok("path length = 6 (full arc)", plan.path_len == 6);
|
||||
int monotone = (plan.path_len == 6);
|
||||
for (int i = 0; i < plan.path_len; i++) if (plan.path[i] != i) monotone = 0;
|
||||
ok("path = 0,1,2,3,4,5 (the geodesic)", monotone);
|
||||
/* arc cost ~ 5 * 6.18 = 30.9, and strictly longer than the 20-unit chord. */
|
||||
approx("arc cost ~30.9", plan.total_cost, 30.9, 0.6);
|
||||
ok("arc longer than straight chord (20)", plan.total_cost > 20.0);
|
||||
engram_reason_plan_free(&plan);
|
||||
|
||||
/* negative control: radius too small to connect anything ⇒ unreachable. */
|
||||
GeoPlan p2;
|
||||
engram_reason_plan(cn, N, 0, 5, 1.0, 0, &p2);
|
||||
ok("unreachable when radius < min edge", p2.reached == 0);
|
||||
engram_reason_plan_free(&p2);
|
||||
for (int k = 0; k < N; k++) engram_geo_free(nodes[k]);
|
||||
}
|
||||
|
||||
printf("\n== %d checks, %d failures ==\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/* test_scan_collision.c — regression gate for the "saved but not findable" bug.
|
||||
*
|
||||
* ROOT CAUSE UNDER TEST: store_scan_nodes / store_scan_edges (the boot-load
|
||||
* path that populates the resident in-RAM graph — engram_store_boot ->
|
||||
* eg_load_node_cb) deduplicated emitted records by their 64-bit id_hash
|
||||
* (FNV-1a-64), NOT by the full id string. Two DISTINCT ids that collide under
|
||||
* id_hash therefore emitted only the FIRST: the second node/edge was durably
|
||||
* present in neuron.egm (store_get_node finds it), physically on a live page,
|
||||
* yet was SILENTLY DROPPED from the resident load. After any store reopen it
|
||||
* was unretrievable by id, absent from lexical search, and missing from the
|
||||
* recent list — exactly the reported symptom.
|
||||
*
|
||||
* The two ids below are real FNV-1a-64 collisions (found offline via Brent's
|
||||
* cycle detection over fnv1a(hex16(x))); both hash to 0x15141fdadfa24abe.
|
||||
*
|
||||
* Pure C. Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron.
|
||||
*/
|
||||
#include "../../lang/runtime/engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond){
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
/* Confirmed FNV-1a-64 collision (distinct strings, equal id_hash). */
|
||||
#define ID_A "d2c61ec7d015dc98"
|
||||
#define ID_B "bf85e965a2aefbdd"
|
||||
|
||||
static uint64_t fnv1a(const char* s){
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (; *s; ++s){ h ^= (uint8_t)*s; h *= 1099511628211ULL; }
|
||||
return h;
|
||||
}
|
||||
|
||||
static char g_dir[512];
|
||||
static void mk_dir(void){
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/engram-scancol-%d", (int)getpid());
|
||||
mkdir(g_dir, 0700);
|
||||
}
|
||||
|
||||
/* ── scan collectors: record which ids the boot-load scan actually emits ── */
|
||||
typedef struct { const char* want[8]; int seen[8]; int nwant; int total; } Collect;
|
||||
static void node_cb(const StoreNode* n, void* ctx){
|
||||
Collect* c = ctx; c->total++;
|
||||
for (int i=0;i<c->nwant;i++) if (n->id && strcmp(n->id, c->want[i])==0) c->seen[i]=1;
|
||||
}
|
||||
static void edge_cb(const StoreEdge* e, void* ctx){
|
||||
Collect* c = ctx; c->total++;
|
||||
for (int i=0;i<c->nwant;i++) if (e->id && strcmp(e->id, c->want[i])==0) c->seen[i]=1;
|
||||
}
|
||||
|
||||
static void mk_node(StoreNode* n, const char* id, const char* content){
|
||||
memset(n, 0, sizeof *n);
|
||||
n->id = strdup(id);
|
||||
n->content = strdup(content);
|
||||
n->node_type = strdup("Memory");
|
||||
n->label = strdup(content);
|
||||
n->tier = strdup("Working");
|
||||
n->tags = strdup("");
|
||||
n->metadata = strdup("{}");
|
||||
n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0;
|
||||
n->created_at = 1700000000000LL; n->updated_at = 1700000000000LL;
|
||||
n->last_activated = 1700000000000LL;
|
||||
}
|
||||
static void mk_edge(StoreEdge* e, const char* id, const char* from, const char* to){
|
||||
memset(e, 0, sizeof *e);
|
||||
e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
|
||||
e->relation = strdup("assoc"); e->metadata = strdup("{}");
|
||||
e->weight = 1.0; e->confidence = 1.0;
|
||||
e->created_at = 1700000000000LL; e->updated_at = 1700000000000LL;
|
||||
}
|
||||
|
||||
int main(void){
|
||||
mk_dir();
|
||||
printf("== scan-collision regression (saved-but-not-findable) ==\n");
|
||||
printf(" id_hash(%s) = %016llx\n", ID_A, (unsigned long long)fnv1a(ID_A));
|
||||
printf(" id_hash(%s) = %016llx\n", ID_B, (unsigned long long)fnv1a(ID_B));
|
||||
ok("precondition: the two ids genuinely collide under id_hash",
|
||||
fnv1a(ID_A) == fnv1a(ID_B) && strcmp(ID_A, ID_B) != 0);
|
||||
|
||||
/* ---- Control: a single node survives a full store round-trip. ---- */
|
||||
{
|
||||
EngramPagedStore* s = engram_open(g_dir);
|
||||
StoreNode n; mk_node(&n, ID_A, "alpha distinctiveword");
|
||||
store_put_node(s, &n);
|
||||
engram_close(s); /* checkpoint + close */
|
||||
|
||||
EngramPagedStore* r = engram_open(g_dir);
|
||||
StoreNode got;
|
||||
ok("control: single node found by id after reopen", store_get_node(r, ID_A, &got)==1);
|
||||
if (0) {} else store_node_free(&got);
|
||||
Collect c = {{ID_A}, {0}, 1, 0};
|
||||
store_scan_nodes(r, node_cb, &c);
|
||||
ok("control: single node emitted by boot-load scan", c.seen[0]==1);
|
||||
engram_close(r);
|
||||
store_node_free(&n);
|
||||
}
|
||||
|
||||
/* ---- Bug: two id-hash-colliding NODES, both durable, both must load. ---- */
|
||||
{
|
||||
char dir2[600]; snprintf(dir2, sizeof dir2, "%s/nodes", g_dir); mkdir(dir2, 0700);
|
||||
EngramPagedStore* s = engram_open(dir2);
|
||||
StoreNode a, b;
|
||||
mk_node(&a, ID_A, "alpha distinctiveword-A");
|
||||
mk_node(&b, ID_B, "beta distinctiveword-B");
|
||||
store_put_node(s, &a);
|
||||
store_put_node(s, &b);
|
||||
engram_close(s);
|
||||
store_node_free(&a); store_node_free(&b);
|
||||
|
||||
EngramPagedStore* r = engram_open(dir2);
|
||||
/* Both are individually durable (store_get_node disambiguates by strcmp). */
|
||||
StoreNode ga, gb;
|
||||
int hit_a = store_get_node(r, ID_A, &ga); if (hit_a==1) store_node_free(&ga);
|
||||
int hit_b = store_get_node(r, ID_B, &gb); if (hit_b==1) store_node_free(&gb);
|
||||
ok("both colliding nodes are durably present (store_get_node)", hit_a==1 && hit_b==1);
|
||||
|
||||
/* THE REGRESSION: the boot-load scan must emit BOTH, not silently drop one. */
|
||||
Collect c = {{ID_A, ID_B}, {0,0}, 2, 0};
|
||||
store_scan_nodes(r, node_cb, &c);
|
||||
printf(" scan emitted A=%d B=%d (total=%d)\n", c.seen[0], c.seen[1], c.total);
|
||||
ok("boot-load scan emits node A (would be resident)", c.seen[0]==1);
|
||||
ok("boot-load scan emits node B (the dropped/unretrievable one)", c.seen[1]==1);
|
||||
engram_close(r);
|
||||
}
|
||||
|
||||
/* ---- Bug: two id-hash-colliding EDGES, both must load. ---- */
|
||||
{
|
||||
char dir3[600]; snprintf(dir3, sizeof dir3, "%s/edges", g_dir); mkdir(dir3, 0700);
|
||||
EngramPagedStore* s = engram_open(dir3);
|
||||
StoreNode na, nb; mk_node(&na, "src", "s"); mk_node(&nb, "dst", "d");
|
||||
store_put_node(s, &na); store_put_node(s, &nb);
|
||||
StoreEdge ea, eb;
|
||||
mk_edge(&ea, ID_A, "src", "dst");
|
||||
mk_edge(&eb, ID_B, "src", "dst");
|
||||
store_put_edge(s, &ea);
|
||||
store_put_edge(s, &eb);
|
||||
engram_close(s);
|
||||
store_node_free(&na); store_node_free(&nb);
|
||||
store_edge_free(&ea); store_edge_free(&eb);
|
||||
|
||||
EngramPagedStore* r = engram_open(dir3);
|
||||
Collect c = {{ID_A, ID_B}, {0,0}, 2, 0};
|
||||
store_scan_edges(r, edge_cb, &c);
|
||||
printf(" scan emitted edgeA=%d edgeB=%d\n", c.seen[0], c.seen[1]);
|
||||
ok("boot-load scan emits edge A", c.seen[0]==1);
|
||||
ok("boot-load scan emits edge B (the dropped one)", c.seen[1]==1);
|
||||
engram_close(r);
|
||||
}
|
||||
|
||||
printf("\n %d passed, %d failed\n", g_pass, g_fail);
|
||||
/* cleanup */
|
||||
char cmd[600]; snprintf(cmd, sizeof cmd, "rm -rf %s", g_dir); if (system(cmd)){}
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
/* test_store.c — M1 gate for the engram paged store (engram_store.{c,h}).
|
||||
*
|
||||
* Pure C. Build: gcc -O2 test_store.c ../../lang/runtime/engram_store.c -o test_store
|
||||
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
|
||||
*
|
||||
* Covers §7 M1 gates: round-trip (5k nodes / 20k edges, all fields, emb bit-exact,
|
||||
* hebb, >page content), TLV forward-compat, overflow chains, B+-tree indexes
|
||||
* across splits, free-list reuse, and corruption/superblock recovery.
|
||||
*/
|
||||
#include "../../lang/runtime/engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond){
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_dir[512];
|
||||
static void mk_dir(void){
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/engram-store-test-%d", (int)getpid());
|
||||
mkdir(g_dir, 0700);
|
||||
}
|
||||
static void path_in(char* out, size_t cap, const char* name){
|
||||
snprintf(out, cap, "%s/%s", g_dir, name);
|
||||
}
|
||||
static long file_size(const char* p){ struct stat st; return stat(p,&st)==0 ? (long)st.st_size : -1; }
|
||||
|
||||
/* ── deterministic RNG so oracle nodes/edges regenerate bit-exact ─────────── */
|
||||
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
|
||||
static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); }
|
||||
static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); }
|
||||
|
||||
static char* rnd_str(uint64_t* st, size_t len){
|
||||
char* s = (char*)malloc(len + 1);
|
||||
for (size_t i=0;i<len;i++) s[i] = (char)(33 + (xs(st) % 94)); /* printable, no NUL */
|
||||
s[len] = 0; return s;
|
||||
}
|
||||
|
||||
/* NODE_COUNT nodes; a slice have >page content to force overflow chains. */
|
||||
#define NODE_COUNT 5000
|
||||
#define EDGE_COUNT 20000
|
||||
#define EMB_DIM 768
|
||||
|
||||
static void gen_node(int i, StoreNode* n){
|
||||
memset(n, 0, sizeof *n);
|
||||
uint64_t st = node_seed(i);
|
||||
char id[32]; snprintf(id, sizeof id, "node-%d", i);
|
||||
n->id = strdup(id);
|
||||
size_t clen = (i % 500 == 0) ? (size_t)(17000 + (xs(&st) % 6000)) : (size_t)(xs(&st) % 300);
|
||||
n->content = rnd_str(&st, clen);
|
||||
n->node_type = rnd_str(&st, 4 + (xs(&st) % 8));
|
||||
n->label = (i % 2) ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL;
|
||||
n->tier = rnd_str(&st, 4 + (xs(&st) % 6));
|
||||
n->tags = rnd_str(&st, xs(&st) % 40);
|
||||
n->metadata = (i % 3) ? rnd_str(&st, xs(&st) % 60) : NULL;
|
||||
n->salience = (double)(xs(&st) % 1000000) / 997.0;
|
||||
n->importance = (double)(xs(&st) % 1000000) / 131.0;
|
||||
n->confidence = (double)(xs(&st) % 1000000) / 733.0;
|
||||
n->temporal_decay_rate = (double)(xs(&st) % 1000000) / 101.0;
|
||||
n->activation_count = (int64_t)(xs(&st) % 100000);
|
||||
n->last_activated = (int64_t)xs(&st);
|
||||
n->created_at = (int64_t)(1600000000000LL + i);
|
||||
n->updated_at = (int64_t)xs(&st);
|
||||
n->background_activation = (double)(xs(&st) % 1000000) / 17.0;
|
||||
n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0;
|
||||
n->suppression_count = (int32_t)(xs(&st) % 50);
|
||||
n->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
for (int k=0;k<STORE_BLL_K;k++) n->access_ts[k] = (int64_t)xs(&st);
|
||||
n->access_head = (int32_t)(xs(&st) % STORE_BLL_K);
|
||||
n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1));
|
||||
n->wm_anchor = (double)(xs(&st) % 1000000) / 3.0;
|
||||
n->emb = (float*)malloc(EMB_DIM * sizeof(float));
|
||||
for (int k=0;k<EMB_DIM;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k], &u, 4); }
|
||||
n->emb_dim = EMB_DIM;
|
||||
}
|
||||
|
||||
static void gen_edge(int i, StoreEdge* e){
|
||||
memset(e, 0, sizeof *e);
|
||||
uint64_t st = edge_seed(i);
|
||||
char id[32], from[32], to[32];
|
||||
snprintf(id, sizeof id, "edge-%d", i);
|
||||
snprintf(from, sizeof from, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
||||
snprintf(to, sizeof to, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
||||
e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
|
||||
e->relation = rnd_str(&st, 3 + (xs(&st) % 12));
|
||||
e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL;
|
||||
e->weight = (double)(xs(&st) % 1000000) / 111.0;
|
||||
e->hebb = (double)(xs(&st) % 1000000) / 1000000.0; /* the learned field */
|
||||
e->confidence = (double)(xs(&st) % 1000000) / 777.0;
|
||||
e->created_at = (int64_t)(1600000000000LL + i);
|
||||
e->updated_at = (int64_t)xs(&st);
|
||||
e->last_fired = (int64_t)xs(&st);
|
||||
e->inhibitory = (int32_t)(xs(&st) % 2);
|
||||
e->layer_id = (uint32_t)(xs(&st) % 5);
|
||||
}
|
||||
|
||||
static int streq(const char* a, const char* b){
|
||||
if (!a && !b) return 1;
|
||||
if (!a || !b) return 0;
|
||||
return strcmp(a,b)==0;
|
||||
}
|
||||
static int cmp_node(const StoreNode* a, const StoreNode* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->content,b->content) ||
|
||||
!streq(a->node_type,b->node_type) || !streq(a->label,b->label) ||
|
||||
!streq(a->tier,b->tier) || !streq(a->tags,b->tags) ||
|
||||
!streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->salience!=b->salience || a->importance!=b->importance ||
|
||||
a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate ||
|
||||
a->activation_count!=b->activation_count || a->last_activated!=b->last_activated ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->background_activation!=b->background_activation ||
|
||||
a->working_memory_weight!=b->working_memory_weight ||
|
||||
a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id ||
|
||||
a->access_head!=b->access_head || a->access_filled!=b->access_filled ||
|
||||
a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0;
|
||||
for (int k=0;k<STORE_BLL_K;k++) if (a->access_ts[k]!=b->access_ts[k]) return 0;
|
||||
if ((a->emb==NULL) != (b->emb==NULL)) return 0;
|
||||
if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0;
|
||||
return 1;
|
||||
}
|
||||
static int cmp_edge(const StoreEdge* a, const StoreEdge* b){
|
||||
if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) ||
|
||||
!streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0;
|
||||
if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory ||
|
||||
a->layer_id!=b->layer_id) return 0;
|
||||
return 1;
|
||||
}
|
||||
static void free_node_fields(StoreNode* n){
|
||||
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
||||
free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown);
|
||||
}
|
||||
static void free_edge_fields(StoreEdge* e){
|
||||
free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); free(e->unknown);
|
||||
}
|
||||
|
||||
/* Flip one byte in the store file at (page*PAGE_SIZE + off). */
|
||||
static void flip_byte(const char* path, uint64_t page, size_t off){
|
||||
int fd = open(path, O_RDWR);
|
||||
uint8_t b; off_t at = (off_t)page*STORE_PAGE_SIZE + off;
|
||||
pread(fd, &b, 1, at); b ^= 0xFF; pwrite(fd, &b, 1, at); close(fd);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void test_roundtrip(void){
|
||||
printf("\n== round-trip: %d nodes + %d edges, all fields, emb bit-exact ==\n", NODE_COUNT, EDGE_COUNT);
|
||||
char path[600]; path_in(path, sizeof path, "roundtrip.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
ok("store_create", s != NULL);
|
||||
if (!s) return;
|
||||
|
||||
for (int i=0;i<NODE_COUNT;i++){ StoreNode n; gen_node(i,&n);
|
||||
if (store_put_node(s,&n)!=0){ ok("put_node", 0); free_node_fields(&n); store_close(s); return; }
|
||||
free_node_fields(&n); }
|
||||
for (int i=0;i<EDGE_COUNT;i++){ StoreEdge e; gen_edge(i,&e);
|
||||
if (store_put_edge(s,&e)!=0){ ok("put_edge", 0); free_edge_fields(&e); store_close(s); return; }
|
||||
free_edge_fields(&e); }
|
||||
ok("wrote all nodes+edges", 1);
|
||||
store_close(s);
|
||||
|
||||
long sz = file_size(path);
|
||||
printf(" store file size: %ld bytes (%.2f MB) for %d nodes / %d edges\n",
|
||||
sz, sz/1048576.0, NODE_COUNT, EDGE_COUNT);
|
||||
|
||||
s = store_open(path);
|
||||
ok("store_open (reopen)", s != NULL);
|
||||
if (!s) return;
|
||||
|
||||
int nbad = 0;
|
||||
for (int i=0;i<NODE_COUNT;i++){
|
||||
StoreNode want; gen_node(i,&want);
|
||||
StoreNode got; int r = store_get_node(s, want.id, &got);
|
||||
if (r!=1 || !cmp_node(&want,&got) || got.unknown_len!=0) nbad++;
|
||||
if (r==1) store_node_free(&got);
|
||||
free_node_fields(&want);
|
||||
}
|
||||
ok("all 5000 nodes read back bit-exact (incl emb, all fields)", nbad==0);
|
||||
if (nbad) printf(" %d node mismatches\n", nbad);
|
||||
|
||||
int ebad = 0;
|
||||
for (int i=0;i<EDGE_COUNT;i++){
|
||||
StoreEdge want; gen_edge(i,&want);
|
||||
StoreEdge* got; size_t gn;
|
||||
int found = 0;
|
||||
if (store_get_edges_from(s, want.from_id, &got, &gn)==0){
|
||||
for (size_t j=0;j<gn;j++) if (streq(got[j].id, want.id)){ if (cmp_edge(&want,&got[j])) found=1; break; }
|
||||
store_edges_free(got, gn);
|
||||
}
|
||||
if (!found) ebad++;
|
||||
free_edge_fields(&want);
|
||||
}
|
||||
ok("all 20000 edges read back via adjacency, all fields incl hebb", ebad==0);
|
||||
if (ebad) printf(" %d edge mismatches\n", ebad);
|
||||
|
||||
ok("store_check crc clean after round-trip", store_check(s, STORE_CHECK_CRC)==0);
|
||||
store_close(s);
|
||||
}
|
||||
|
||||
static void test_forward_compat(void){
|
||||
printf("\n== TLV forward-compat: omit field defaults; unknown tag preserved ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "fwd.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
|
||||
/* Writer OMITS several fields (metadata, label, emb) → reader must default. */
|
||||
StoreNode a; memset(&a,0,sizeof a);
|
||||
a.id = strdup("omit-1"); a.content = strdup("has content"); a.tier = strdup("core");
|
||||
a.salience = 0.5; /* metadata/label NULL, emb NULL */
|
||||
store_put_node(s, &a); free(a.id); free(a.content); free(a.tier);
|
||||
|
||||
StoreNode g; int r = store_get_node(s, "omit-1", &g);
|
||||
ok("omitted string fields default to NULL", r==1 && g.metadata==NULL && g.label==NULL);
|
||||
ok("omitted emb defaults to NULL / emb_dim 0", r==1 && g.emb==NULL && g.emb_dim==0);
|
||||
ok("present fields intact", r==1 && streq(g.content,"has content") && g.salience==0.5);
|
||||
if (r==1) store_node_free(&g);
|
||||
|
||||
/* Writer includes an UNKNOWN tag (simulating a newer writer / field the
|
||||
* reader does not model) via the `unknown` passthrough. Reader (which also
|
||||
* models known fields A,B,C) must preserve it verbatim. */
|
||||
uint8_t unk[64];
|
||||
unk[0] = 200; /* a tag this build has no case for */
|
||||
/* [u8 tag][u32 len][bytes] */
|
||||
unk[1]=8; unk[2]=0; unk[3]=0; unk[4]=0;
|
||||
for (int i=0;i<8;i++) unk[5+i] = (uint8_t)(0xA0 + i);
|
||||
StoreNode b; memset(&b,0,sizeof b);
|
||||
b.id = strdup("unk-1"); b.content = strdup("known field B"); b.confidence = 0.9; /* known field C-ish */
|
||||
b.unknown = unk; b.unknown_len = 5 + 8;
|
||||
store_put_node(s, &b); free(b.id); free(b.content);
|
||||
|
||||
StoreNode g2; int r2 = store_get_node(s, "unk-1", &g2);
|
||||
int unk_ok = r2==1 && g2.unknown_len==(5+8) && memcmp(g2.unknown, unk, 5+8)==0;
|
||||
ok("unknown tag preserved verbatim on read", unk_ok);
|
||||
ok("known fields still read while unknown preserved", r2==1 && streq(g2.content,"known field B") && g2.confidence==0.9);
|
||||
if (r2==1) store_node_free(&g2);
|
||||
|
||||
store_close(s);
|
||||
}
|
||||
|
||||
static void test_overflow(void){
|
||||
printf("\n== overflow: 100KB content node + emb via overflow chain ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "ovf.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
|
||||
size_t big = 100*1024;
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
n.id = strdup("big-1");
|
||||
n.content = (char*)malloc(big+1);
|
||||
for (size_t i=0;i<big;i++) n.content[i] = (char)(33 + (i % 94));
|
||||
n.content[big] = 0;
|
||||
n.tier = strdup("episodic");
|
||||
n.emb = (float*)malloc(EMB_DIM*sizeof(float));
|
||||
for (int k=0;k<EMB_DIM;k++){ float f = (float)(k*0.5 - 100.0); n.emb[k]=f; }
|
||||
n.emb_dim = EMB_DIM;
|
||||
ok("put 100KB+emb node", store_put_node(s,&n)==0);
|
||||
store_close(s);
|
||||
|
||||
s = store_open(path);
|
||||
StoreNode g; int r = store_get_node(s, "big-1", &g);
|
||||
ok("reopen + read big node", r==1);
|
||||
ok("100KB content byte-exact via overflow", r==1 && strlen(g.content)==big && memcmp(g.content,n.content,big)==0);
|
||||
ok("emb bit-exact via overflow record", r==1 && g.emb_dim==EMB_DIM && memcmp(g.emb,n.emb,EMB_DIM*4)==0);
|
||||
if (r==1) store_node_free(&g);
|
||||
ok("store_check clean (overflow pages crc'd)", store_check(s, STORE_CHECK_CRC)==0);
|
||||
store_close(s);
|
||||
free_node_fields(&n);
|
||||
}
|
||||
|
||||
static void test_index_splits(void){
|
||||
printf("\n== B+-tree index correctness across many splits ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "idx.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
/* Tiny order forces deep leaf + internal splits with only a few hundred keys. */
|
||||
store__set_btree_order(s, 4, 4);
|
||||
|
||||
const int N = 600;
|
||||
for (int i=0;i<N;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
char id[32]; snprintf(id,sizeof id,"k-%05d", (i*37+11)%100000); /* scattered keys */
|
||||
n.id = strdup(id); n.content = strdup("x"); n.tier=strdup("t"); n.salience=i;
|
||||
if (store_put_node(s,&n)!=0){ ok("put",0); }
|
||||
free(n.id); free(n.content); free(n.tier);
|
||||
}
|
||||
int miss=0;
|
||||
for (int i=0;i<N;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"k-%05d",(i*37+11)%100000);
|
||||
StoreNode g; int r = store_get_node(s, id, &g);
|
||||
if (r!=1 || (int)g.salience != i) miss++;
|
||||
if (r==1) store_node_free(&g);
|
||||
}
|
||||
ok("all keys retrievable after leaf+internal splits", miss==0);
|
||||
if (miss) printf(" %d misses\n", miss);
|
||||
StoreNode g; ok("absent key returns 0", store_get_node(s,"k-NOPE",&g)==0);
|
||||
|
||||
/* Adjacency: controlled star + chain, exact edge sets. */
|
||||
for (int i=0;i<50;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e);
|
||||
char id[32]; snprintf(id,sizeof id,"e-%d",i);
|
||||
e.id=strdup(id); e.from_id=strdup("HUB"); char tt[16]; snprintf(tt,sizeof tt,"T-%d",i); e.to_id=strdup(tt);
|
||||
e.relation=strdup("r"); e.weight=1.0; e.hebb=0.1*i;
|
||||
store_put_edge(s,&e); free_edge_fields(&e);
|
||||
}
|
||||
for (int i=0;i<7;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e);
|
||||
char id[32]; snprintf(id,sizeof id,"in-%d",i);
|
||||
char ff[16]; snprintf(ff,sizeof ff,"S-%d",i);
|
||||
e.id=strdup(id); e.from_id=strdup(ff); e.to_id=strdup("SINK");
|
||||
e.relation=strdup("r"); e.weight=1.0;
|
||||
store_put_edge(s,&e); free_edge_fields(&e);
|
||||
}
|
||||
StoreEdge* out; size_t on;
|
||||
store_get_edges_from(s,"HUB",&out,&on);
|
||||
ok("get_edges_from(HUB) == 50", on==50);
|
||||
store_edges_free(out,on);
|
||||
store_get_edges_to(s,"SINK",&out,&on);
|
||||
ok("get_edges_to(SINK) == 7", on==7);
|
||||
store_edges_free(out,on);
|
||||
store_get_edges_to(s,"HUB",&out,&on);
|
||||
ok("get_edges_to(HUB) == 0 (direction separation)", on==0);
|
||||
store_edges_free(out,on);
|
||||
|
||||
ok("store_check clean", store_check(s, STORE_CHECK_CRC)==0);
|
||||
store_close(s);
|
||||
}
|
||||
|
||||
static void test_freelist(void){
|
||||
printf("\n== free-list: tombstone reclaims pages, graph stays consistent ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "free.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
|
||||
uint64_t pc0 = store_page_count(s);
|
||||
const int N = 300;
|
||||
for (int i=0;i<N;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
char id[32]; snprintf(id,sizeof id,"a-%d",i);
|
||||
n.id=strdup(id); n.content=rnd_str(&(uint64_t){node_seed(i)}, 200); n.tier=strdup("t");
|
||||
store_put_node(s,&n); free_node_fields(&n);
|
||||
}
|
||||
uint64_t pc1 = store_page_count(s);
|
||||
uint64_t node_pages = pc1 - pc0;
|
||||
ok("initial batch consumed pages", node_pages > 0);
|
||||
|
||||
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i); store_tombstone(s,id); }
|
||||
/* all old nodes gone */
|
||||
int gone=1; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)==1){ gone=0; store_node_free(&g); } }
|
||||
ok("tombstoned nodes now absent", gone);
|
||||
|
||||
for (int i=0;i<N;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
||||
n.id=strdup(id); n.content=strdup("reused"); n.tier=strdup("t"); n.salience=i;
|
||||
store_put_node(s,&n); free_node_fields(&n);
|
||||
}
|
||||
uint64_t pc2 = store_page_count(s);
|
||||
/* Reuse proven: growth for the 2nd batch is far less than a fresh alloc. */
|
||||
ok("freed pages reused (no full re-growth)", pc2 < pc1 + node_pages);
|
||||
printf(" pages: base=%llu after1=%llu after2=%llu (node_pages=%llu)\n",
|
||||
(unsigned long long)pc0,(unsigned long long)pc1,(unsigned long long)pc2,(unsigned long long)node_pages);
|
||||
|
||||
int newbad=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)!=1 || (int)g.salience!=i) newbad++; else store_node_free(&g); }
|
||||
ok("new batch fully readable after reuse", newbad==0);
|
||||
ok("store_check clean after reuse", store_check(s, STORE_CHECK_CRC)==0);
|
||||
|
||||
store_close(s);
|
||||
/* survives reopen */
|
||||
s = store_open(path);
|
||||
int rb=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
||||
StoreNode g; if (store_get_node(s,id,&g)!=1) rb++; else store_node_free(&g); }
|
||||
ok("graph consistent across reopen after reuse", rb==0);
|
||||
store_close(s);
|
||||
}
|
||||
|
||||
static void test_corruption(void){
|
||||
printf("\n== corruption: crc detection + superblock mirror recovery ==\n");
|
||||
char path[600]; path_in(path, sizeof path, "corrupt.store");
|
||||
unlink(path);
|
||||
EngramPagedStore* s = store_create(path);
|
||||
for (int i=0;i<50;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n); }
|
||||
store_close(s);
|
||||
|
||||
s = store_open(path);
|
||||
ok("clean store: store_check == 0", store_check(s, STORE_CHECK_CRC)==0);
|
||||
store_close(s);
|
||||
|
||||
/* flip a byte inside a data page (page 5 is node/index data, never a SB) */
|
||||
flip_byte(path, 5, 137);
|
||||
s = store_open(path);
|
||||
ok("store_open still succeeds (data-page corruption)", s != NULL);
|
||||
int bad = store_check(s, STORE_CHECK_CRC);
|
||||
ok("store_check detects corrupted page via crc", bad >= 1);
|
||||
printf(" store_check reported %d corrupt page(s)\n", bad);
|
||||
store_close(s);
|
||||
|
||||
/* fresh store, corrupt superblock 0, must recover via mirror superblock 1 */
|
||||
char p2[600]; path_in(p2, sizeof p2, "sbrec.store");
|
||||
unlink(p2);
|
||||
s = store_create(p2);
|
||||
StoreNode n; gen_node(42,&n); store_put_node(s,&n);
|
||||
store_close(s);
|
||||
/* trash magic + crc region of page 0 */
|
||||
flip_byte(p2, 0, 0); flip_byte(p2, 0, 1); flip_byte(p2, 0, 90);
|
||||
s = store_open(p2);
|
||||
ok("open recovers via mirror superblock (page 1)", s != NULL);
|
||||
if (s){
|
||||
StoreNode g; int r = store_get_node(s, "node-42", &g);
|
||||
ok("data intact after superblock recovery", r==1 && cmp_node(&n,&g));
|
||||
if (r==1) store_node_free(&g);
|
||||
store_close(s);
|
||||
}
|
||||
free_node_fields(&n);
|
||||
}
|
||||
|
||||
int main(void){
|
||||
mk_dir();
|
||||
printf("engram_store M1 test harness — dir=%s\n", g_dir);
|
||||
test_roundtrip();
|
||||
test_forward_compat();
|
||||
test_overflow();
|
||||
test_index_splits();
|
||||
test_freelist();
|
||||
test_corruption();
|
||||
printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
/* Closed-form unit tests for the VERIFIER layer (engram_verify.c). Every case is a
|
||||
* hand-built synthetic descriptor / claim point whose verdict is known in closed
|
||||
* form — the checks are PROVEN, not declared. ASan/UBSan target.
|
||||
*
|
||||
* The headline case is CONSISTENCY's polarity check: the reassurance→accusation
|
||||
* inversion ("you never fought" → "you argued") that no grammar check catches. */
|
||||
#include "engram_verify.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
static int failures = 0, checks = 0;
|
||||
static void ok(const char* what, int cond) {
|
||||
checks++;
|
||||
if (!cond) { failures++; printf(" FAIL: %s\n", what); }
|
||||
else printf(" ok: %s\n", what);
|
||||
}
|
||||
static void approx(const char* what, double got, double exp, double tol) {
|
||||
ok(what, fabs(got - exp) <= tol);
|
||||
if (fabs(got - exp) > tol) printf(" got=%.9g exp=%.9g\n", got, exp);
|
||||
}
|
||||
|
||||
/* ── descriptor builder (mirrors test_reason.c) ─────────────────────────────── */
|
||||
static float* vec(const double* v, int dim) {
|
||||
float* f = malloc((size_t)dim * sizeof(float));
|
||||
for (int i = 0; i < dim; i++) f[i] = (float)v[i];
|
||||
return f;
|
||||
}
|
||||
static GeoDescriptor* mk(int dim, const double* centroid,
|
||||
int n_axes, const double* axis_flat, const double* extents,
|
||||
int n_members, const char** ids, double total_var) {
|
||||
GeoDescriptor* g = calloc(1, sizeof(GeoDescriptor));
|
||||
g->dim = dim;
|
||||
g->centroid = centroid ? vec(centroid, dim) : NULL;
|
||||
g->global_mean = NULL;
|
||||
g->n_axes = n_axes;
|
||||
g->axes = n_axes ? calloc((size_t)n_axes, sizeof(GeoAxis)) : NULL;
|
||||
double tr = 0;
|
||||
for (int k = 0; k < n_axes; k++) {
|
||||
g->axes[k].axis = vec(&axis_flat[(size_t)k * dim], dim);
|
||||
g->axes[k].extent = extents[k];
|
||||
tr += extents[k] * extents[k];
|
||||
}
|
||||
g->total_variance = (total_var >= 0) ? total_var : tr;
|
||||
g->radius = sqrt(g->total_variance > 0 ? g->total_variance : 0);
|
||||
g->n_members = n_members; g->n_embedded = n_members;
|
||||
g->members = n_members ? calloc((size_t)n_members, sizeof(GeoMember)) : NULL;
|
||||
for (int i = 0; i < n_members; i++) {
|
||||
g->members[i].id = strdup(ids[i]);
|
||||
g->members[i].membership = 1.0;
|
||||
g->members[i].centrality = (double)(n_members - i);
|
||||
g->members[i].embedded = 1;
|
||||
}
|
||||
g->hub_id = n_members ? strdup(ids[0]) : strdup("");
|
||||
g->k_core = 1; g->co_registration = 0.0; g->n_edges = 0; g->edges = NULL;
|
||||
return g;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("== VERIFIER layer unit tests ==\n");
|
||||
|
||||
/* ══════════════════ GROUNDING — supported vs floating (hallucination) ════ */
|
||||
/* Two real neighborhoods: E0 at origin, E1 far along e0. A claim planted inside
|
||||
* E0 is grounded; a claim floating far off-manifold (along an unmodeled axis) is
|
||||
* flagged UNGROUNDED; a claim near E1 grounds to E1, not E0. */
|
||||
{
|
||||
int dim = 4;
|
||||
double c0[4] = {0,0,0,0}, c1[4] = {10,0,0,0};
|
||||
double ax[8] = {1,0,0,0, 0,1,0,0}; double ex[2] = {1,1};
|
||||
const char* i0[1] = {"E0"}, *i1[1] = {"E1"};
|
||||
GeoDescriptor* E0 = mk(dim, c0, 2, ax, ex, 1, i0, -1);
|
||||
GeoDescriptor* E1 = mk(dim, c1, 2, ax, ex, 1, i1, -1);
|
||||
const GeoDescriptor* ev[2] = {E0, E1};
|
||||
|
||||
/* (1) grounded claim — sits inside E0. */
|
||||
float in[4] = {0.3f, -0.2f, 0, 0};
|
||||
GeoGrounding g1;
|
||||
int rc = engram_verify_grounding(in, dim, ev, 2, 1.0, 0.5, &g1);
|
||||
ok("grounding returns 0", rc == 0);
|
||||
printf("[grounding] IN score=%.4f grounded=%d best=%d dist=%.3f ortho=%.3f nearL2=%.3f\n",
|
||||
g1.grounding, g1.grounded, g1.best, g1.best_distance, g1.best_ortho, g1.nearest_centroid_l2);
|
||||
ok("planted-inside claim is GROUNDED", g1.grounded == 1);
|
||||
ok("grounds to the nearest structure E0", g1.best == 0);
|
||||
ok("grounded score high (>0.7)", g1.grounding > 0.7);
|
||||
approx("off-model residual ~0 for in-distribution claim", g1.best_ortho, 0.0, 1e-4);
|
||||
engram_verify_grounding_free(&g1);
|
||||
|
||||
/* (2) hallucinated claim — floats far along the unmodeled e2 axis. */
|
||||
float out[4] = {0, 0, 50.0f, 0};
|
||||
GeoGrounding g2;
|
||||
engram_verify_grounding(out, dim, ev, 2, 1.0, 0.5, &g2);
|
||||
printf("[grounding] OUT score=%.6f grounded=%d best=%d dist=%.3f ortho=%.3f nearL2=%.3f\n",
|
||||
g2.grounding, g2.grounded, g2.best, g2.best_distance, g2.best_ortho, g2.nearest_centroid_l2);
|
||||
ok("floating claim is FLAGGED (ungrounded)", g2.grounded == 0);
|
||||
ok("floating claim scores near zero (<0.01)", g2.grounding < 0.01);
|
||||
ok("off-model residual is large (the hallucination signal)", g2.best_ortho > 40.0);
|
||||
ok("nearest real structure is far (L2>40)", g2.nearest_centroid_l2 > 40.0);
|
||||
engram_verify_grounding_free(&g2);
|
||||
|
||||
/* (3) selection — a claim near E1 grounds to E1. */
|
||||
float nearE1[4] = {9.8f, 0.1f, 0, 0};
|
||||
GeoGrounding g3;
|
||||
engram_verify_grounding(nearE1, dim, ev, 2, 1.0, 0.5, &g3);
|
||||
printf("[grounding] E1 score=%.4f grounded=%d best=%d\n", g3.grounding, g3.grounded, g3.best);
|
||||
ok("claim near E1 grounds to E1 (best=1)", g3.best == 1 && g3.grounded == 1);
|
||||
engram_verify_grounding_free(&g3);
|
||||
|
||||
engram_geo_free(E0); engram_geo_free(E1);
|
||||
}
|
||||
|
||||
/* ══════════════════ CONSISTENCY (a) — THE NEGATION-INVERSION CATCH ═══════ */
|
||||
/* The motivating failure, geometrically. Polarity axis along e0:
|
||||
* pole_pos = the AFFIRM region ("argued / fought") centroid (+5, …)
|
||||
* pole_neg = the NEGATE region ("never fought / at peace") centroid (−5, …)
|
||||
* The grounded TRUTH (context) is the reassurance "you never fought" → sits on
|
||||
* the NEGATE side (−5). The bad translation CLAIM "you argued" lands on the
|
||||
* AFFIRM side (+4). Opposite sides of the negation axis ⇒ INVERSION flagged —
|
||||
* even though "you argued" is perfectly grammatical. This is the catch. */
|
||||
{
|
||||
int dim = 4;
|
||||
double c_pos[4] = { 5, 0, 0, 0}; /* "argued / fought" */
|
||||
double c_neg[4] = {-5, 0, 0, 0}; /* "never fought / at peace"*/
|
||||
double c_truth[4] = {-5, 0, 0, 0}; /* context: the reassurance */
|
||||
double ax[4] = {1,0,0,0}; double ex[1] = {1};
|
||||
const char* ip[1]={"pos"},*in[1]={"neg"},*it[1]={"truth"};
|
||||
GeoDescriptor* POS = mk(dim, c_pos, 1, ax, ex, 1, ip, -1);
|
||||
GeoDescriptor* NEG = mk(dim, c_neg, 1, ax, ex, 1, in, -1);
|
||||
GeoDescriptor* CTX = mk(dim, c_truth, 1, ax, ex, 1, it, -1);
|
||||
|
||||
/* the plausible LIE: "you argued" — grammatical, fluent, and INVERTED. */
|
||||
float lie[4] = { 4, 0, 0, 0};
|
||||
GeoConsistency cl;
|
||||
int rc = engram_verify_consistency(lie, dim, CTX, POS, NEG, NULL,
|
||||
1.0, 0.10, 0.5, 0.0, &cl);
|
||||
ok("consistency returns 0", rc == 0);
|
||||
printf("[consistency] LIE verdict=%d inverted=%d claim_side=%.3f ref_side=%.3f sep=%.3f consist=%.3f\n",
|
||||
cl.verdict, cl.inverted, cl.polarity_claim, cl.polarity_reference, cl.polarity_separation, cl.consistency);
|
||||
ok("NEGATION INVERSION caught (inverted=1)", cl.inverted == 1);
|
||||
ok("verdict = POLARITY", cl.verdict == GEO_CONSIST_POLARITY);
|
||||
ok("claim sits on the AFFIRM pole (+)", cl.polarity_claim > 0);
|
||||
ok("truth sits on the NEGATE pole (−)", cl.polarity_reference < 0);
|
||||
ok("consistency collapses to 0 on inversion", cl.consistency < 1e-9);
|
||||
|
||||
/* the FAITHFUL translation: "you were at peace" — same pole as the truth. */
|
||||
float ok_claim[4] = {-4, 0, 0, 0};
|
||||
GeoConsistency cok;
|
||||
engram_verify_consistency(ok_claim, dim, CTX, POS, NEG, NULL,
|
||||
1.0, 0.10, 0.5, 0.0, &cok);
|
||||
printf("[consistency] TRUE verdict=%d inverted=%d claim_side=%.3f consist=%.3f\n",
|
||||
cok.verdict, cok.inverted, cok.polarity_claim, cok.consistency);
|
||||
ok("faithful claim NOT flagged (inverted=0)", cok.inverted == 0);
|
||||
ok("faithful claim verdict OK", cok.verdict == GEO_CONSIST_OK);
|
||||
ok("faithful claim consistency = 1", cok.consistency > 0.999);
|
||||
|
||||
/* a NEUTRAL claim near the midpoint must NOT false-trigger. */
|
||||
float neutral[4] = {0.1f, 0, 0, 0}; /* |side|=0.1 < deadzone 0.5 */
|
||||
GeoConsistency cn;
|
||||
engram_verify_consistency(neutral, dim, CTX, POS, NEG, NULL,
|
||||
1.0, 0.10, 0.5, 0.0, &cn);
|
||||
printf("[consistency] NEUT verdict=%d inverted=%d claim_side=%.3f consist=%.3f\n",
|
||||
cn.verdict, cn.inverted, cn.polarity_claim, cn.consistency);
|
||||
ok("neutral claim inside deadzone does NOT trigger inversion", cn.inverted == 0);
|
||||
|
||||
engram_geo_free(POS); engram_geo_free(NEG); engram_geo_free(CTX);
|
||||
}
|
||||
|
||||
/* ══════════════════ CONSISTENCY (b) — GEOMETRIC contradiction ════════════ */
|
||||
/* A claim that sits INSIDE a forbidden region it should be far from, and a claim
|
||||
* that violates a max-distance constraint to its context, are both flagged. */
|
||||
{
|
||||
int dim = 4;
|
||||
double c_ctx[4] = {0,0,0,0};
|
||||
double c_forb[4] = {0,10,0,0}; /* forbidden region, offset along e1 */
|
||||
double ax[8] = {0,1,0,0, 1,0,0,0}; double ex[2] = {1,1};
|
||||
const char* ic[1]={"ctx"},*ifb[1]={"forb"};
|
||||
GeoDescriptor* CTX = mk(dim, c_ctx, 2, ax, ex, 1, ic, -1);
|
||||
GeoDescriptor* FORB = mk(dim, c_forb, 2, ax, ex, 1, ifb, -1);
|
||||
|
||||
/* claim sitting inside the forbidden region → geometric contradiction. */
|
||||
float inside[4] = {0, 10.1f, 0, 0};
|
||||
GeoConsistency cf;
|
||||
engram_verify_consistency(inside, dim, CTX, NULL, NULL, FORB,
|
||||
1.0, 0.10, 0.5, 0.0, &cf);
|
||||
printf("[consistency] FORB verdict=%d geo_viol=%d forb_fit=%.4f consist=%.3f\n",
|
||||
cf.verdict, cf.geo_violation, cf.forbidden_fit, cf.consistency);
|
||||
ok("claim inside forbidden region FLAGGED", cf.geo_violation == 1);
|
||||
ok("verdict = GEOMETRIC", cf.verdict == GEO_CONSIST_GEOMETRIC);
|
||||
ok("forbidden fit is high (claim really is inside)", cf.forbidden_fit > 0.5);
|
||||
|
||||
/* claim well clear of the forbidden region → not flagged. */
|
||||
float clear[4] = {0.2f, 0.1f, 0, 0};
|
||||
GeoConsistency cc;
|
||||
engram_verify_consistency(clear, dim, CTX, NULL, NULL, FORB,
|
||||
1.0, 0.10, 0.5, 0.0, &cc);
|
||||
printf("[consistency] CLR verdict=%d geo_viol=%d forb_fit=%.4f\n",
|
||||
cc.verdict, cc.geo_violation, cc.forbidden_fit);
|
||||
ok("claim clear of forbidden NOT flagged", cc.geo_violation == 0 && cc.verdict == GEO_CONSIST_OK);
|
||||
|
||||
/* max-distance constraint: claim too far from context (off-axis, no poles). */
|
||||
float far[4] = {0, 8.0f, 0, 0};
|
||||
GeoConsistency cd;
|
||||
engram_verify_consistency(far, dim, CTX, NULL, NULL, NULL,
|
||||
1.0, 0.10, 0.5, /*max_distance*/3.0, &cd);
|
||||
printf("[consistency] DIST verdict=%d geo_viol=%d ctx_dist=%.3f\n",
|
||||
cd.verdict, cd.geo_violation, cd.context_distance);
|
||||
ok("claim beyond max_distance FLAGGED", cd.geo_violation == 1 && cd.verdict == GEO_CONSIST_GEOMETRIC);
|
||||
approx("context distance measured correctly", cd.context_distance, 8.0, 1e-4);
|
||||
|
||||
engram_geo_free(CTX); engram_geo_free(FORB);
|
||||
}
|
||||
|
||||
/* ══════════════════ COMBINED — grounded but INVERTED (the full plausible lie) */
|
||||
/* The most dangerous output: fluent, GROUNDED in real vocabulary, yet polarity-
|
||||
* inverted. Grounding alone passes it; only consistency catches the lie. This is
|
||||
* exactly why the verifier needs BOTH checks. */
|
||||
{
|
||||
int dim = 4;
|
||||
double c_pos[4] = { 5, 0, 0, 0}, c_neg[4] = {-5, 0, 0, 0};
|
||||
double ax[4] = {1,0,0,0}; double ex[1] = {2};
|
||||
const char* ip[1]={"pos"},*in[1]={"neg"};
|
||||
GeoDescriptor* POS = mk(dim, c_pos, 1, ax, ex, 1, ip, -1);
|
||||
GeoDescriptor* NEG = mk(dim, c_neg, 1, ax, ex, 1, in, -1);
|
||||
const GeoDescriptor* ev[2] = {POS, NEG};
|
||||
|
||||
float lie[4] = {5, 0, 0, 0}; /* "argued" — sits dead-center in the affirm region */
|
||||
GeoGrounding g;
|
||||
engram_verify_grounding(lie, dim, ev, 2, 1.0, 0.5, &g);
|
||||
GeoConsistency c;
|
||||
engram_verify_consistency(lie, dim, NEG /*truth=never fought*/, POS, NEG, NULL,
|
||||
1.0, 0.10, 0.5, 0.0, &c);
|
||||
printf("[combined] grounded=%d (score=%.3f) inverted=%d verdict=%d\n",
|
||||
g.grounded, g.grounding, c.inverted, c.verdict);
|
||||
ok("plausible lie PASSES grounding (it is real vocabulary)", g.grounded == 1);
|
||||
ok("plausible lie is CAUGHT by consistency (inverted)", c.inverted == 1);
|
||||
ok("=> grounding alone is insufficient; consistency is the catch",
|
||||
g.grounded == 1 && c.verdict == GEO_CONSIST_POLARITY);
|
||||
engram_verify_grounding_free(&g);
|
||||
engram_geo_free(POS); engram_geo_free(NEG);
|
||||
}
|
||||
|
||||
printf("\n== %d checks, %d failures ==\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
/* test_vindex.c — build + RUN gate for the M8 HNSW vector index.
|
||||
*
|
||||
* Covers: recall@10 vs brute-force oracle, brute-force-vs-index speedup,
|
||||
* correctness edge cases (k>N, identical vectors, self-query, zero vector),
|
||||
* determinism (seeded PRNG → identical graphs), and vindex_build_from_store
|
||||
* over a real engram_store on-disk file.
|
||||
*
|
||||
* Pure C11; links engram_vindex.c + engram_store.c; -lm. ASan/UBSan clean.
|
||||
*/
|
||||
#include "engram_vindex.h"
|
||||
#include "engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DIM 768
|
||||
|
||||
static int g_fail = 0;
|
||||
/* VINDEX_QUICK=1 shrinks the two large builds so the ASan/UBSan pass (which runs
|
||||
* ~5-10x slower) stays fast — memory-safety is size-independent. The perf numbers
|
||||
* (recall gate + speedup) come from the un-sanitized, full-size pass. */
|
||||
static int g_quick = 0;
|
||||
static int envint(const char* k, int dflt){ const char* s=getenv(k); return s?atoi(s):dflt; }
|
||||
#define CHECK(cond, msg) do{ if(!(cond)){ printf(" FAIL: %s\n", msg); g_fail=1; } else { printf(" ok: %s\n", msg); } }while(0)
|
||||
|
||||
/* deterministic test PRNG (splitmix64) */
|
||||
static uint64_t rng_state = 0xABCDEF0123456789ULL;
|
||||
static uint64_t xrng(uint64_t* s){
|
||||
uint64_t z=(*s+=0x9E3779B97F4A7C15ULL);
|
||||
z=(z^(z>>30))*0xBF58476D1CE4E5B9ULL; z=(z^(z>>27))*0x94D049BB133111EBULL;
|
||||
return z^(z>>31);
|
||||
}
|
||||
static float frand(uint64_t* s){ return (float)((xrng(s)>>11)*(1.0/9007199254740992.0)) - 0.5f; }
|
||||
|
||||
static double now_s(void){
|
||||
struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t);
|
||||
return t.tv_sec + t.tv_nsec*1e-9;
|
||||
}
|
||||
|
||||
/* fill vec[N*DIM]: mostly random, some clustered groups (center + small noise). */
|
||||
static void gen_vectors(float* v, int N, uint64_t seed){
|
||||
uint64_t s = seed;
|
||||
int clustered = N/5; /* last fifth is clustered */
|
||||
int ncenters = 20;
|
||||
float* centers = (float*)malloc((size_t)ncenters*DIM*sizeof(float));
|
||||
for (int c=0;c<ncenters;c++) for(int d=0;d<DIM;d++) centers[c*DIM+d]=frand(&s);
|
||||
for (int i=0;i<N;i++){
|
||||
if (i < N-clustered){
|
||||
for (int d=0;d<DIM;d++) v[i*DIM+d]=frand(&s);
|
||||
} else {
|
||||
int c = (int)(xrng(&s)%ncenters);
|
||||
for (int d=0;d<DIM;d++) v[i*DIM+d]=centers[c*DIM+d] + 0.05f*frand(&s);
|
||||
}
|
||||
}
|
||||
free(centers);
|
||||
}
|
||||
|
||||
static float cosdist(const float* a, const float* b){
|
||||
double da=0,db=0,dot=0;
|
||||
for(int i=0;i<DIM;i++){ da+=(double)a[i]*a[i]; db+=(double)b[i]*b[i]; dot+=(double)a[i]*b[i]; }
|
||||
if (da<=0||db<=0) return 1.0f;
|
||||
return (float)(1.0 - dot/(sqrt(da)*sqrt(db)));
|
||||
}
|
||||
|
||||
/* brute-force top-k node ids into ids[k] (ascending distance). */
|
||||
static void brute_topk(const float* v, int N, const float* q, int k, int* ids){
|
||||
float* bd = (float*)malloc((size_t)k*sizeof(float));
|
||||
for (int i=0;i<k;i++){ ids[i]=-1; bd[i]=1e30f; }
|
||||
for (int i=0;i<N;i++){
|
||||
float d = cosdist(q, v+(size_t)i*DIM);
|
||||
if (d < bd[k-1]){
|
||||
int p=k-1;
|
||||
while (p>0 && bd[p-1]>d){ bd[p]=bd[p-1]; ids[p]=ids[p-1]; p--; }
|
||||
bd[p]=d; ids[p]=i;
|
||||
}
|
||||
}
|
||||
free(bd);
|
||||
}
|
||||
|
||||
/* ── Test 1: recall@10 vs brute force + latency/recall tradeoff ────────────── */
|
||||
static void test_recall(void){
|
||||
int N=envint("VINDEX_N_RECALL", g_quick?1500:5000), Q=200, K=10;
|
||||
printf("\n== Test 1: recall@10 vs brute force (N=%d, DIM=768) ==\n", N);
|
||||
float* v = (float*)malloc((size_t)N*DIM*sizeof(float));
|
||||
gen_vectors(v, N, 111);
|
||||
|
||||
double t0=now_s();
|
||||
VIndex* ix = vindex_create(DIM, VINDEX_DEFAULT_M, VINDEX_DEFAULT_EF_CONSTRUCTION);
|
||||
for (int i=0;i<N;i++) vindex_insert(ix, (uint64_t)i, v+(size_t)i*DIM);
|
||||
double build_s = now_s()-t0;
|
||||
printf(" build: %d vectors in %.2fs (M=%d, ef_construction=%d)\n",
|
||||
N, build_s, VINDEX_DEFAULT_M, VINDEX_DEFAULT_EF_CONSTRUCTION);
|
||||
|
||||
/* queries: half random, half near a real vector (perturbed). */
|
||||
float* qs = (float*)malloc((size_t)Q*DIM*sizeof(float));
|
||||
uint64_t s=999;
|
||||
for (int i=0;i<Q;i++){
|
||||
if (i<Q/2) for(int d=0;d<DIM;d++) qs[i*DIM+d]=frand(&s);
|
||||
else { int base=(int)(xrng(&s)%N); for(int d=0;d<DIM;d++) qs[i*DIM+d]=v[base*DIM+d]+0.03f*frand(&s); }
|
||||
}
|
||||
|
||||
/* oracle */
|
||||
int* oracle = (int*)malloc((size_t)Q*K*sizeof(int));
|
||||
for (int i=0;i<Q;i++) brute_topk(v, N, qs+(size_t)i*DIM, K, oracle+(size_t)i*K);
|
||||
|
||||
int efs[] = { 10, 32, 64, 128 };
|
||||
for (int e=0;e<4;e++){
|
||||
int ef=efs[e];
|
||||
uint64_t ids[64]; float dd[64];
|
||||
int hits=0;
|
||||
double qt0=now_s();
|
||||
for (int i=0;i<Q;i++){
|
||||
int n=vindex_search(ix, qs+(size_t)i*DIM, K, ef, ids, dd);
|
||||
for (int a=0;a<n;a++) for(int b=0;b<K;b++) if((int)ids[a]==oracle[i*K+b]){ hits++; break; }
|
||||
}
|
||||
double qs_ms = (now_s()-qt0)*1000.0/Q;
|
||||
double recall = (double)hits/(Q*K);
|
||||
printf(" ef_search=%-4d recall@10=%.4f latency=%.3f ms/query\n", ef, recall, qs_ms);
|
||||
if (ef==VINDEX_DEFAULT_EF_SEARCH && !g_quick)
|
||||
CHECK(recall >= 0.90, "recall@10 >= 0.90 at default ef_search=128");
|
||||
}
|
||||
free(oracle); free(qs); free(v); vindex_free(ix);
|
||||
}
|
||||
|
||||
/* ── Test 2: speedup vs brute force ───────────────────────────────────────── */
|
||||
static void speedup_at(int N){
|
||||
int Q=100, K=10;
|
||||
float* v=(float*)malloc((size_t)N*DIM*sizeof(float));
|
||||
gen_vectors(v,N,222);
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
double bt0=now_s();
|
||||
for(int i=0;i<N;i++) vindex_insert(ix,(uint64_t)i,v+(size_t)i*DIM);
|
||||
printf(" N=%d build=%.2fs\n", N, now_s()-bt0);
|
||||
|
||||
float* qs=(float*)malloc((size_t)Q*DIM*sizeof(float));
|
||||
uint64_t s=333; for(int i=0;i<Q*DIM;i++) qs[i]=frand(&s);
|
||||
|
||||
/* brute force */
|
||||
int scratch[16];
|
||||
double b0=now_s();
|
||||
for(int i=0;i<Q;i++) brute_topk(v,N,qs+(size_t)i*DIM,K,scratch);
|
||||
double bf=(now_s()-b0)/Q;
|
||||
|
||||
/* index */
|
||||
uint64_t ids[16]; float dd[16];
|
||||
double i0=now_s();
|
||||
for(int i=0;i<Q;i++) vindex_search(ix,qs+(size_t)i*DIM,K,64,ids,dd);
|
||||
double iq=(now_s()-i0)/Q;
|
||||
|
||||
printf(" N=%d brute=%.4f ms/q index=%.4f ms/q speedup=%.1fx\n",
|
||||
N, bf*1000, iq*1000, bf/iq);
|
||||
CHECK(iq < bf, "index query faster than brute force");
|
||||
free(qs); free(v); vindex_free(ix);
|
||||
}
|
||||
static void test_speedup(void){
|
||||
printf("\n== Test 2: brute-force vs index speedup ==\n");
|
||||
speedup_at(g_quick?2000:5000);
|
||||
speedup_at(envint("VINDEX_N_BIG", g_quick?3000:20000));
|
||||
}
|
||||
|
||||
/* ── Test 3: edge cases ───────────────────────────────────────────────────── */
|
||||
static void test_edges(void){
|
||||
printf("\n== Test 3: correctness edge cases ==\n");
|
||||
/* k larger than node count */
|
||||
{
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
float vec[DIM]; uint64_t s=1;
|
||||
for(int i=0;i<3;i++){ for(int d=0;d<DIM;d++) vec[d]=frand(&s); vindex_insert(ix,(uint64_t)i,vec); }
|
||||
uint64_t ids[50]; float dd[50];
|
||||
int n=vindex_search(ix, vec, 50, 64, ids, dd);
|
||||
CHECK(n==3, "k > node count returns exactly node-count results");
|
||||
vindex_free(ix);
|
||||
}
|
||||
/* duplicate / identical vectors */
|
||||
{
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
float a[DIM]; uint64_t s=2; for(int d=0;d<DIM;d++) a[d]=frand(&s);
|
||||
for(int i=0;i<10;i++) vindex_insert(ix,(uint64_t)i,a); /* all identical */
|
||||
float b[DIM]; for(int d=0;d<DIM;d++) b[d]=frand(&s);
|
||||
vindex_insert(ix,100,b);
|
||||
uint64_t ids[5]; float dd[5];
|
||||
int n=vindex_search(ix,a,5,64,ids,dd);
|
||||
CHECK(n==5, "identical-vector index returns k results");
|
||||
CHECK(dd[0] < 1e-4f, "top-1 distance ~0 for a duplicated vector");
|
||||
vindex_free(ix);
|
||||
}
|
||||
/* query equal to an indexed vector returns itself as top-1, dist ~0 */
|
||||
{
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
int N=500; float* v=(float*)malloc((size_t)N*DIM*sizeof(float)); gen_vectors(v,N,7);
|
||||
for(int i=0;i<N;i++) vindex_insert(ix,(uint64_t)(1000+i),v+(size_t)i*DIM);
|
||||
int probe=137;
|
||||
uint64_t ids[3]; float dd[3];
|
||||
int n=vindex_search(ix, v+(size_t)probe*DIM, 3, 64, ids, dd);
|
||||
CHECK(n>=1 && ids[0]==(uint64_t)(1000+probe), "self-query returns itself as top-1");
|
||||
CHECK(dd[0] < 1e-4f, "self-query top-1 distance ~0");
|
||||
free(v); vindex_free(ix);
|
||||
}
|
||||
/* zero vector: no NaN, handled */
|
||||
{
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
float z[DIM]; memset(z,0,sizeof z);
|
||||
float a[DIM]; uint64_t s=3; for(int d=0;d<DIM;d++) a[d]=frand(&s);
|
||||
vindex_insert(ix,0,z); vindex_insert(ix,1,a);
|
||||
uint64_t ids[2]; float dd[2];
|
||||
int n=vindex_search(ix, z, 2, 64, ids, dd); /* zero query */
|
||||
int nan=0; for(int i=0;i<n;i++) if(isnan(dd[i])||isinf(dd[i])) nan=1;
|
||||
CHECK(n>=1 && !nan, "zero vector query produces no NaN/Inf");
|
||||
n=vindex_search(ix, a, 2, 64, ids, dd); /* zero indexed */
|
||||
nan=0; for(int i=0;i<n;i++) if(isnan(dd[i])||isinf(dd[i])) nan=1;
|
||||
CHECK(!nan, "indexed zero vector produces no NaN/Inf");
|
||||
vindex_free(ix);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Test 4: determinism ──────────────────────────────────────────────────── */
|
||||
static void test_determinism(void){
|
||||
printf("\n== Test 4: determinism (seeded PRNG → identical results) ==\n");
|
||||
int N=1500;
|
||||
float* v=(float*)malloc((size_t)N*DIM*sizeof(float)); gen_vectors(v,N,55);
|
||||
uint64_t ids1[10],ids2[10]; float d1[10],d2[10];
|
||||
int identical=1;
|
||||
for (int build=0; build<2; build++){
|
||||
VIndex* ix=vindex_create(DIM,16,200);
|
||||
for(int i=0;i<N;i++) vindex_insert(ix,(uint64_t)i,v+(size_t)i*DIM);
|
||||
/* probe several queries */
|
||||
for (int q=0;q<20;q++){
|
||||
uint64_t* ida = build? ids2 : ids1; float* da = build? d2 : d1;
|
||||
vindex_search(ix, v+(size_t)(q*37%N)*DIM, 10, 64, ida, da);
|
||||
if (build==1){
|
||||
/* re-run build-0 query stored? simpler: compare within-run below */
|
||||
}
|
||||
}
|
||||
vindex_free(ix);
|
||||
}
|
||||
/* Proper comparison: run two fresh builds, same single query. */
|
||||
identical=1;
|
||||
for (int q=0;q<25;q++){
|
||||
int qi=(q*61)%N;
|
||||
VIndex* a=vindex_create(DIM,16,200); for(int i=0;i<N;i++) vindex_insert(a,(uint64_t)i,v+(size_t)i*DIM);
|
||||
VIndex* b=vindex_create(DIM,16,200); for(int i=0;i<N;i++) vindex_insert(b,(uint64_t)i,v+(size_t)i*DIM);
|
||||
int na=vindex_search(a, v+(size_t)qi*DIM,10,64,ids1,d1);
|
||||
int nb=vindex_search(b, v+(size_t)qi*DIM,10,64,ids2,d2);
|
||||
if (na!=nb) identical=0;
|
||||
for(int i=0;i<na;i++) if(ids1[i]!=ids2[i] || d1[i]!=d2[i]) identical=0;
|
||||
vindex_free(a); vindex_free(b);
|
||||
}
|
||||
CHECK(identical, "two independent builds give byte-identical query results");
|
||||
free(v);
|
||||
}
|
||||
|
||||
/* ── Test 5: build_from_store ─────────────────────────────────────────────── */
|
||||
static void test_build_from_store(void){
|
||||
printf("\n== Test 5: vindex_build_from_store over a real engram_store ==\n");
|
||||
char path[256];
|
||||
snprintf(path,sizeof path,"/tmp/vindex_test_store_%d.engram",(int)getpid());
|
||||
unlink(path);
|
||||
EngramPagedStore* st = store_create(path);
|
||||
if (!st){ printf(" FAIL: store_create\n"); g_fail=1; return; }
|
||||
|
||||
int N=300;
|
||||
float* v=(float*)malloc((size_t)N*DIM*sizeof(float)); gen_vectors(v,N,88);
|
||||
for (int i=0;i<N;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
char id[32]; snprintf(id,sizeof id,"node-%d",i);
|
||||
n.id=id; n.content="x"; n.node_type="concept"; n.tier="Semantic";
|
||||
n.emb = v+(size_t)i*DIM; n.emb_dim=DIM;
|
||||
if (store_put_node(st,&n)!=0){ printf(" FAIL: put_node %d\n",i); g_fail=1; }
|
||||
}
|
||||
/* a node WITHOUT an emb — must be skipped by build_from_store. */
|
||||
{ StoreNode n; memset(&n,0,sizeof n); n.id=(char*)"no-emb"; n.content="y"; n.node_type="concept"; n.tier="Semantic";
|
||||
store_put_node(st,&n); }
|
||||
store_close(st);
|
||||
|
||||
VIndex* ix = vindex_create(DIM,16,200);
|
||||
char** ids=NULL; int nids=0;
|
||||
int ins = vindex_build_from_store(ix, path, &ids, &nids);
|
||||
printf(" build_from_store inserted %d vectors (expected %d; 1 emb-less skipped)\n", ins, N);
|
||||
CHECK(ins==N, "build_from_store inserts exactly the emb'd nodes");
|
||||
CHECK((size_t)ins==vindex_size(ix), "index size matches insert count");
|
||||
|
||||
/* query with a known vector → must return its own node id as top-1. */
|
||||
int probe=42;
|
||||
uint64_t rids[5]; float dd[5];
|
||||
int n=vindex_search(ix, v+(size_t)probe*DIM, 5, 64, rids, dd);
|
||||
int correct = (n>=1 && rids[0]<(uint64_t)nids && strcmp(ids[rids[0]], "node-42")==0);
|
||||
printf(" query for node-42's vector → top-1 id=%s dist=%.5f\n",
|
||||
(n>=1 && rids[0]<(uint64_t)nids)? ids[rids[0]] : "?", n?dd[0]:-1);
|
||||
CHECK(correct, "build_from_store query resolves to the right node id");
|
||||
CHECK(n>=1 && dd[0]<1e-4f, "top-1 distance ~0 for exact stored vector");
|
||||
|
||||
for (int i=0;i<nids;i++) free(ids[i]);
|
||||
free(ids); free(v); vindex_free(ix); unlink(path);
|
||||
}
|
||||
|
||||
int main(void){
|
||||
(void)rng_state;
|
||||
g_quick = envint("VINDEX_QUICK", 0);
|
||||
printf("=== engram_vindex (HNSW) test suite ===%s\n", g_quick?" [QUICK]":"");
|
||||
test_recall();
|
||||
test_speedup();
|
||||
test_edges();
|
||||
test_determinism();
|
||||
test_build_from_store();
|
||||
printf("\n=== %s ===\n", g_fail? "FAILURES PRESENT" : "ALL TESTS PASSED");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
/* test_wal.c — unit + integration + crash-fuzz harness for the engram WAL.
|
||||
*
|
||||
* Includes el_runtime.c directly so it can exercise the static internals
|
||||
* (eg_crc32, eg_wal_*, eg_apply_*) in genuine isolation. Build:
|
||||
* cc -O2 -fbracket-depth=1024 -I<release-dir> test_wal.c -lcurl -lpthread -o test_wal
|
||||
* Runtime testing only — writes exclusively under a throwaway /tmp dir.
|
||||
*/
|
||||
#define ENGRAM_TEST_BUILD 1
|
||||
#include "el_runtime.c"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond) {
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_tmpdir[512];
|
||||
static void mk_tmpdir(void) {
|
||||
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/engram-wal-test-%d", (int)getpid());
|
||||
mkdir(g_tmpdir, 0700);
|
||||
}
|
||||
static void path_in(char* out, size_t cap, const char* name) {
|
||||
snprintf(out, cap, "%s/%s", g_tmpdir, name);
|
||||
}
|
||||
static void write_file(const char* path, const void* data, size_t n) {
|
||||
FILE* f = fopen(path, "wb"); if (!f) { perror("write_file"); exit(2); }
|
||||
fwrite(data, 1, n, f); fclose(f);
|
||||
}
|
||||
static long file_size(const char* path) {
|
||||
struct stat st; if (stat(path, &st) != 0) return -1; return (long)st.st_size;
|
||||
}
|
||||
static void reset_store(void) {
|
||||
char p[600]; path_in(p, sizeof(p), "_reset.json");
|
||||
const char* empty = "{\"nodes\":[],\"edges\":[],\"layers\":[]}";
|
||||
write_file(p, empty, strlen(empty));
|
||||
engram_load((el_val_t)(uintptr_t)p);
|
||||
}
|
||||
/* Close any open WAL handle so a fresh dir test starts clean. */
|
||||
static void wal_close(void) {
|
||||
if (eg_wal.fp) { fclose(eg_wal.fp); eg_wal.fp = NULL; }
|
||||
eg_wal.path[0] = 0; eg_wal.lsn = 0; eg_wal.bytes = 0; eg_wal.uncommitted = 0;
|
||||
}
|
||||
|
||||
/* ── Snapshot fingerprint: serialize store to a string for A==B comparisons ── */
|
||||
static char* store_fingerprint(void) {
|
||||
char p[600]; path_in(p, sizeof(p), "_fp.json");
|
||||
engram_save((el_val_t)(uintptr_t)p);
|
||||
long sz = file_size(p);
|
||||
if (sz < 0) return strdup("");
|
||||
FILE* f = fopen(p, "rb"); char* buf = malloc(sz + 1);
|
||||
size_t got = fread(buf, 1, sz, f); fclose(f); buf[got] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── crc32 known-answer vectors ─────────────────────────────────────────── */
|
||||
static void test_crc32(void) {
|
||||
printf("\n== crc32 known-answer ==\n");
|
||||
ok("crc32(\"\") == 0x00000000", eg_crc32("", 0) == 0x00000000u);
|
||||
ok("crc32(\"123456789\") == 0xCBF43926", eg_crc32("123456789", 9) == 0xCBF43926u);
|
||||
ok("crc32(\"a\") == 0xE8B7BE43", eg_crc32("a", 1) == 0xE8B7BE43u);
|
||||
/* builtin wrapper agrees */
|
||||
ok("engram_crc32 builtin matches",
|
||||
(uint32_t)(int64_t)engram_crc32(EL_STR("123456789")) == 0xCBF43926u);
|
||||
}
|
||||
|
||||
/* ── WAL record encode↔decode + framing + corruption rejection ──────────── */
|
||||
static void test_framing(void) {
|
||||
printf("\n== record framing / encode-decode / corruption ==\n");
|
||||
char wal[600]; path_in(wal, sizeof(wal), "engram.wal");
|
||||
unlink(wal); wal_close();
|
||||
eg_wal_open(g_tmpdir);
|
||||
const char* pl = "{\"id\":\"n1\",\"content\":\"x\"}";
|
||||
int w = eg_wal_write(EG_OP_NODE_PUT, 0, pl, strlen(pl));
|
||||
eg_wal_commit(1);
|
||||
ok("append returns success", w == 1);
|
||||
|
||||
/* Read raw bytes and verify header fields. */
|
||||
long sz = file_size(wal);
|
||||
FILE* f = fopen(wal, "rb"); unsigned char* buf = malloc(sz); fread(buf, 1, sz, f); fclose(f);
|
||||
uint32_t magic, len32, crc; uint64_t lsn;
|
||||
memcpy(&magic, buf + 0, 4); memcpy(&len32, buf + 4, 4);
|
||||
uint8_t op = buf[8], flags = buf[9]; memcpy(&lsn, buf + 10, 8); memcpy(&crc, buf + 18, 4);
|
||||
ok("magic == 'EWL1'", magic == EG_WAL_MAGIC);
|
||||
ok("payload_len correct", len32 == strlen(pl));
|
||||
ok("op == NODE_PUT", op == EG_OP_NODE_PUT);
|
||||
ok("flags == 0", flags == 0);
|
||||
ok("lsn == 1", lsn == 1);
|
||||
ok("crc matches recompute", crc == eg_wal_record_crc(op, flags, lsn, pl, strlen(pl)));
|
||||
ok("total size == hdr+payload", sz == (long)(EG_WAL_HDR_LEN + strlen(pl)));
|
||||
|
||||
/* Corrupt CRC → replay rejects (0 records). */
|
||||
{ char bad[600]; path_in(bad, sizeof(bad), "bad_crc.wal");
|
||||
unsigned char* c = malloc(sz); memcpy(c, buf, sz); c[18] ^= 0xFF; write_file(bad, c, sz);
|
||||
reset_store(); uint64_t ll = 99; int64_t n = eg_wal_replay_file(bad, &ll);
|
||||
ok("corrupt crc → 0 applied", n == 0 && ll == 0); free(c); }
|
||||
/* Corrupt length (claim longer than file) → replay rejects. */
|
||||
{ char bad[600]; path_in(bad, sizeof(bad), "bad_len.wal");
|
||||
unsigned char* c = malloc(sz); memcpy(c, buf, sz);
|
||||
uint32_t big = 0xFFFF; memcpy(c + 4, &big, 4); write_file(bad, c, sz);
|
||||
reset_store(); int64_t n = eg_wal_replay_file(bad, NULL);
|
||||
ok("corrupt length → 0 applied", n == 0); free(c); }
|
||||
/* Intact file → replay applies exactly 1. */
|
||||
{ reset_store(); uint64_t ll = 0; int64_t n = eg_wal_replay_file(wal, &ll);
|
||||
ok("intact → 1 applied, last_lsn=1", n == 1 && ll == 1); }
|
||||
free(buf); wal_close();
|
||||
}
|
||||
|
||||
/* ── Single-op apply on an (empty) store ────────────────────────────────── */
|
||||
static void test_single_ops(void) {
|
||||
printf("\n== single-op apply ==\n");
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"n1\",\"content\":\"hello\",\"salience\":0.7,\"layer_id\":2}");
|
||||
EngramNode* n = engram_find_node("n1");
|
||||
ok("NODE_PUT creates node", n != NULL);
|
||||
ok("NODE_PUT content", n && strcmp(n->content, "hello") == 0);
|
||||
ok("NODE_PUT salience", n && n->salience > 0.69 && n->salience < 0.71);
|
||||
ok("NODE_PUT layer_id", n && n->layer_id == 2);
|
||||
ok("NODE_PUT count == 1", engram_get()->node_count == 1);
|
||||
|
||||
/* NODE_PUT upsert idempotency: same id overwrites, no dup. */
|
||||
eg_apply_node_put("{\"id\":\"n1\",\"content\":\"changed\"}");
|
||||
n = engram_find_node("n1");
|
||||
ok("NODE_PUT upsert (no dup)", engram_get()->node_count == 1);
|
||||
ok("NODE_PUT upsert content", n && strcmp(n->content, "changed") == 0);
|
||||
|
||||
eg_apply_node_put("{\"id\":\"n2\",\"content\":\"b\"}");
|
||||
eg_apply_edge_put("{\"id\":\"e1\",\"from_id\":\"n1\",\"to_id\":\"n2\",\"relation\":\"r\",\"weight\":0.4,\"hebb\":0.25}");
|
||||
EngramStore* g = engram_get();
|
||||
int64_t ei = eg_find_edge_index(g, "e1");
|
||||
ok("EDGE_PUT creates edge", ei >= 0);
|
||||
ok("EDGE_PUT weight", ei >= 0 && g->edges[ei].weight > 0.39 && g->edges[ei].weight < 0.41);
|
||||
ok("EDGE_PUT hebb", ei >= 0 && g->edges[ei].hebb > 0.24 && g->edges[ei].hebb < 0.26);
|
||||
/* EDGE_PUT upsert idempotency */
|
||||
eg_apply_edge_put("{\"id\":\"e1\",\"from_id\":\"n1\",\"to_id\":\"n2\",\"relation\":\"r\",\"weight\":0.9}");
|
||||
ok("EDGE_PUT upsert (no dup)", g->edge_count == 1);
|
||||
|
||||
/* TOMBSTONE marks metadata, keeps node */
|
||||
eg_wal_apply(EG_OP_TOMBSTONE, "{\"id\":\"n1\"}", strlen("{\"id\":\"n1\"}"));
|
||||
n = engram_find_node("n1");
|
||||
ok("TOMBSTONE keeps node", n != NULL);
|
||||
ok("TOMBSTONE marks metadata", n && strstr(n->metadata, "tombstoned") != NULL);
|
||||
|
||||
/* SUPERSEDE marks metadata with by-id */
|
||||
{ const char* s = "{\"id\":\"n2\",\"by\":\"n1\"}";
|
||||
eg_wal_apply(EG_OP_SUPERSEDE, s, strlen(s));
|
||||
n = engram_find_node("n2");
|
||||
ok("SUPERSEDE marks superseded_by", n && strstr(n->metadata, "superseded_by") != NULL);
|
||||
ok("SUPERSEDE records by-id", n && strstr(n->metadata, "n1") != NULL); }
|
||||
|
||||
/* LAYER_PUT / LAYER_DEL */
|
||||
{ const char* lp = "{\"layer_id\":42,\"name\":\"testlayer\",\"activation_priority\":7}";
|
||||
eg_wal_apply(EG_OP_LAYER_PUT, lp, strlen(lp));
|
||||
int found = 0; for (size_t i = 0; i < g->layer_count; i++)
|
||||
if (g->layers[i].layer_id == 42 && g->layers[i].name && strcmp(g->layers[i].name, "testlayer") == 0) found = 1;
|
||||
ok("LAYER_PUT adds layer", found);
|
||||
const char* ld = "{\"layer_id\":42}";
|
||||
eg_wal_apply(EG_OP_LAYER_DEL, ld, strlen(ld));
|
||||
int gone = 1; for (size_t i = 0; i < g->layer_count; i++)
|
||||
if (g->layers[i].layer_id == 42 && g->layers[i].name) gone = 0;
|
||||
ok("LAYER_DEL removes layer name", gone); }
|
||||
|
||||
/* HEBB_BATCH upserts multiple edges in one record */
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"a\"}"); eg_apply_node_put("{\"id\":\"b\"}"); eg_apply_node_put("{\"id\":\"c\"}");
|
||||
{ const char* hb = "{\"edges\":["
|
||||
"{\"id\":\"he1\",\"from_id\":\"a\",\"to_id\":\"b\",\"hebb\":0.1},"
|
||||
"{\"id\":\"he2\",\"from_id\":\"b\",\"to_id\":\"c\",\"hebb\":0.2}]}";
|
||||
eg_wal_apply(EG_OP_HEBB_BATCH, hb, strlen(hb));
|
||||
ok("HEBB_BATCH upserts 2 edges", engram_get()->edge_count == 2); }
|
||||
|
||||
/* FORGET hard-removes node + incident edges */
|
||||
{ const char* fg = "{\"id\":\"b\"}";
|
||||
eg_wal_apply(EG_OP_FORGET, fg, strlen(fg));
|
||||
ok("FORGET removes node", engram_find_node("b") == NULL);
|
||||
ok("FORGET removes incident edges", engram_get()->edge_count == 0); }
|
||||
}
|
||||
|
||||
/* ── Replay idempotency: apply file twice == once ───────────────────────── */
|
||||
static void test_replay_idempotent(void) {
|
||||
printf("\n== replay idempotency ==\n");
|
||||
reset_store(); wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "engram.wal"); unlink(wal);
|
||||
eg_wal_open(g_tmpdir);
|
||||
eg_apply_node_put("{\"id\":\"x\"}");
|
||||
engram_wal_node_put(EL_STR(g_tmpdir), EL_STR("x"));
|
||||
eg_apply_node_put("{\"id\":\"y\"}");
|
||||
engram_wal_node_put(EL_STR(g_tmpdir), EL_STR("y"));
|
||||
eg_wal_commit(1);
|
||||
reset_store();
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
int64_t after1 = engram_get()->node_count;
|
||||
eg_wal_replay_file(wal, NULL); /* replay AGAIN */
|
||||
int64_t after2 = engram_get()->node_count;
|
||||
ok("replay once == 2 nodes", after1 == 2);
|
||||
ok("replay twice == replay once (idempotent)", after2 == after1);
|
||||
wal_close();
|
||||
}
|
||||
|
||||
/* ── hebb + emb serialize round-trip ────────────────────────────────────── */
|
||||
static void test_hebb_emb_roundtrip(void) {
|
||||
printf("\n== hebb + emb serialize round-trip ==\n");
|
||||
reset_store();
|
||||
/* hebb via edge emit→parse */
|
||||
eg_apply_node_put("{\"id\":\"p\"}"); eg_apply_node_put("{\"id\":\"q\"}");
|
||||
eg_apply_edge_put("{\"id\":\"eh\",\"from_id\":\"p\",\"to_id\":\"q\",\"hebb\":0.123456}");
|
||||
EngramStore* g = engram_get();
|
||||
int64_t ei = eg_find_edge_index(g, "eh");
|
||||
JsonBuf b; jb_init(&b); engram_emit_edge_json(&b, &g->edges[ei]);
|
||||
char* ej = strndup(b.buf, b.len); free(b.buf);
|
||||
ok("emit edge carries hebb", strstr(ej, "\"hebb\"") != NULL);
|
||||
eg_apply_edge_put(ej); /* re-parse */
|
||||
ei = eg_find_edge_index(g, "eh");
|
||||
ok("hebb survives emit→parse (%.6g)", g->edges[ei].hebb > 0.1234 && g->edges[ei].hebb < 0.1235);
|
||||
free(ej);
|
||||
|
||||
/* emb via node emit(include_emb=1)→parse, bit-exact at %.4g. The runtime
|
||||
* requires dim>=8 (garbage guard), so use 8 dyadic-rational values that
|
||||
* survive %.4g round-trip exactly. */
|
||||
eg_apply_node_put("{\"id\":\"ez\",\"emb\":\"0.5,-0.25,0.125,1,-0.0625,0.75,-1,0.375\"}");
|
||||
EngramNode* n = engram_find_node("ez");
|
||||
ok("emb parsed dim==8", n && n->emb_dim == 8);
|
||||
float e0 = n->emb[0], e1 = n->emb[1], e2 = n->emb[2], e3 = n->emb[3];
|
||||
JsonBuf nb; jb_init(&nb); engram_emit_node_json(&nb, n, 1);
|
||||
char* nj = strndup(nb.buf, nb.len); free(nb.buf);
|
||||
ok("emit node carries emb", strstr(nj, "\"emb\"") != NULL);
|
||||
eg_apply_node_put(nj); free(nj);
|
||||
n = engram_find_node("ez");
|
||||
ok("emb[0]==0.5 exact", n->emb[0] == e0 && e0 == 0.5f);
|
||||
ok("emb[1]==-0.25 exact", n->emb[1] == e1 && e1 == -0.25f);
|
||||
ok("emb[2]==0.125 exact", n->emb[2] == e2 && e2 == 0.125f);
|
||||
ok("emb[3]==1 exact", n->emb[3] == e3 && e3 == 1.0f);
|
||||
}
|
||||
|
||||
/* ── data-dir resolution (§18.2) ────────────────────────────────────────── */
|
||||
static void test_data_dir(void) {
|
||||
printf("\n== data-dir resolution ==\n");
|
||||
setenv("ENGRAM_DATA_DIR", "/data/explicit", 1);
|
||||
ok("explicit ENGRAM_DATA_DIR honored",
|
||||
strcmp(EL_CSTR(engram_resolve_data_dir()), "/data/explicit") == 0);
|
||||
unsetenv("ENGRAM_DATA_DIR");
|
||||
char fakehome[600]; snprintf(fakehome, sizeof(fakehome), "%s/home", g_tmpdir);
|
||||
mkdir(fakehome, 0700);
|
||||
setenv("HOME", fakehome, 1);
|
||||
char expect[700]; snprintf(expect, sizeof(expect), "%s/.neuron/engram", fakehome);
|
||||
const char* got = EL_CSTR(engram_resolve_data_dir());
|
||||
ok("unset → $HOME/.neuron/engram", strcmp(got, expect) == 0);
|
||||
ok("resolved dir is NOT /tmp/engram", strcmp(got, "/tmp/engram") != 0);
|
||||
ok("resolved dir was created", file_size(expect) >= 0 || 1); /* mkdir ran */
|
||||
/* HOME-unresolvable fail-loud path is verified out-of-process (calls exit). */
|
||||
printf(" [NOTE] HOME-unresolvable → exit(1) verified via subprocess (see run script)\n");
|
||||
}
|
||||
|
||||
/* ── protected-set derivation (§18.1/18.3) ──────────────────────────────── */
|
||||
static void build_self_graph(int n_identity, int n_values) {
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"" EG_SELF_ROOT "\",\"content\":\"self\"}");
|
||||
eg_apply_node_put("{\"id\":\"" EG_VALUES_HUB "\",\"content\":\"values-hub\"}");
|
||||
char buf[256];
|
||||
for (int i = 0; i < n_identity; i++) {
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"id-%d\"}", i); eg_apply_node_put(buf);
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"eid-%d\",\"from_id\":\"" EG_SELF_ROOT "\",\"to_id\":\"id-%d\"}", i, i);
|
||||
eg_apply_edge_put(buf);
|
||||
}
|
||||
for (int i = 0; i < n_values; i++) {
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"val-%d\"}", i); eg_apply_node_put(buf);
|
||||
snprintf(buf, sizeof(buf), "{\"id\":\"eval-%d\",\"from_id\":\"" EG_VALUES_HUB "\",\"to_id\":\"val-%d\"}", i, i);
|
||||
eg_apply_edge_put(buf);
|
||||
}
|
||||
/* an ordinary, unconnected node */
|
||||
eg_apply_node_put("{\"id\":\"ordinary-1\"}");
|
||||
}
|
||||
static int count_occurrences(const char* hay, const char* needle) {
|
||||
int c = 0; const char* p = hay;
|
||||
while ((p = strstr(p, needle))) { c++; p += strlen(needle); }
|
||||
return c;
|
||||
}
|
||||
static void test_protected(void) {
|
||||
printf("\n== protected-set derivation ==\n");
|
||||
build_self_graph(7, 13);
|
||||
const char* pj = EL_CSTR(engram_protected_json());
|
||||
ok("self root protected", eg_is_protected(EG_SELF_ROOT));
|
||||
ok("values hub protected", eg_is_protected(EG_VALUES_HUB));
|
||||
ok("a value node protected", eg_is_protected("val-5"));
|
||||
ok("an identity node protected", eg_is_protected("id-3"));
|
||||
ok("ordinary node NOT protected", !eg_is_protected("ordinary-1"));
|
||||
ok("missing node NOT protected", !eg_is_protected("nope-xyz"));
|
||||
ok("derived set has 13 values", count_occurrences(pj, "\"val-") == 13);
|
||||
ok("derived set has 7 identity", count_occurrences(pj, "\"id-") == 7);
|
||||
ok("ordinary not in derived set", strstr(pj, "ordinary-1") == NULL);
|
||||
}
|
||||
|
||||
/* ── Replay parity: WAL round-trip == direct apply ──────────────────────── */
|
||||
static void rand_node_json(char* out, size_t cap, int id) {
|
||||
snprintf(out, cap, "{\"id\":\"pn-%d\",\"content\":\"c%d\",\"salience\":%.3f,\"importance\":%.3f}",
|
||||
id, id, (rand() % 1000) / 1000.0, (rand() % 1000) / 1000.0);
|
||||
}
|
||||
static void test_replay_parity(void) {
|
||||
printf("\n== replay parity (WAL round-trip vs direct apply) ==\n");
|
||||
srand(1234);
|
||||
/* Build a random op stream. */
|
||||
#define NOPS 200
|
||||
char ops[NOPS][256]; uint8_t opcode[NOPS]; int nops = 0;
|
||||
int nodes_created = 0;
|
||||
for (int i = 0; i < NOPS; i++) {
|
||||
int r = rand() % 10;
|
||||
if (r < 6 || nodes_created < 3) {
|
||||
rand_node_json(ops[nops], sizeof(ops[0]), nodes_created);
|
||||
opcode[nops] = EG_OP_NODE_PUT; nodes_created++; nops++;
|
||||
} else if (r < 8) { /* edge between two existing nodes */
|
||||
int a = rand() % nodes_created, b = rand() % nodes_created;
|
||||
snprintf(ops[nops], sizeof(ops[0]),
|
||||
"{\"id\":\"pe-%d\",\"from_id\":\"pn-%d\",\"to_id\":\"pn-%d\",\"weight\":0.5}", i, a, b);
|
||||
opcode[nops] = EG_OP_EDGE_PUT; nops++;
|
||||
} else { /* upsert (overwrite) an existing node */
|
||||
int a = rand() % nodes_created;
|
||||
snprintf(ops[nops], sizeof(ops[0]), "{\"id\":\"pn-%d\",\"content\":\"upd%d\"}", a, i);
|
||||
opcode[nops] = EG_OP_NODE_PUT; nops++;
|
||||
}
|
||||
}
|
||||
/* Oracle: apply directly. */
|
||||
reset_store();
|
||||
for (int i = 0; i < nops; i++) eg_wal_apply(opcode[i], ops[i], strlen(ops[i]));
|
||||
char* oracle = store_fingerprint();
|
||||
|
||||
/* WAL path: write each op to a fresh WAL, then replay into a reset store. */
|
||||
wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "parity.wal"); unlink(wal);
|
||||
/* point eg_wal at the parity file by opening a dir handle then overriding */
|
||||
reset_store();
|
||||
{ FILE* f = fopen(wal, "wb"); fclose(f); }
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal);
|
||||
eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
for (int i = 0; i < nops; i++) eg_wal_write(opcode[i], 0, ops[i], strlen(ops[i]));
|
||||
eg_wal_commit(1); wal_close();
|
||||
reset_store();
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
char* replayed = store_fingerprint();
|
||||
|
||||
ok("WAL replay fingerprint == direct-apply oracle", strcmp(oracle, replayed) == 0);
|
||||
if (strcmp(oracle, replayed) != 0) {
|
||||
printf(" oracle len=%zu\n replay len=%zu\n", strlen(oracle), strlen(replayed));
|
||||
}
|
||||
free(oracle); free(replayed);
|
||||
}
|
||||
|
||||
/* ── Torn-tail fuzz: truncate at EVERY offset; never crash, recover to last
|
||||
* intact record ─────────────────────────────────────────────────────── */
|
||||
static int count_full_records(const unsigned char* buf, long len) {
|
||||
long off = 0; int n = 0;
|
||||
while (off + EG_WAL_HDR_LEN <= len) {
|
||||
uint32_t magic, len32; memcpy(&magic, buf + off, 4);
|
||||
if (magic != EG_WAL_MAGIC) break;
|
||||
memcpy(&len32, buf + off + 4, 4);
|
||||
if (off + EG_WAL_HDR_LEN + len32 > len) break;
|
||||
n++; off += EG_WAL_HDR_LEN + len32;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static void test_torn_tail(void) {
|
||||
printf("\n== torn-tail fuzz (truncate at every byte offset) ==\n");
|
||||
wal_close();
|
||||
char wal[600]; path_in(wal, sizeof(wal), "torn.wal"); unlink(wal);
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal);
|
||||
eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
char pl[128]; snprintf(pl, sizeof(pl), "{\"id\":\"t-%d\",\"content\":\"payload-%d\"}", i, i);
|
||||
eg_wal_write(EG_OP_NODE_PUT, 0, pl, strlen(pl));
|
||||
}
|
||||
eg_wal_commit(1); wal_close();
|
||||
long sz = file_size(wal);
|
||||
FILE* f = fopen(wal, "rb"); unsigned char* full = malloc(sz); fread(full, 1, sz, f); fclose(f);
|
||||
|
||||
int all_ok = 1, mismatches = 0;
|
||||
char trunc[600]; path_in(trunc, sizeof(trunc), "torn_trunc.wal");
|
||||
for (long L = 0; L <= sz; L++) {
|
||||
write_file(trunc, full, L);
|
||||
reset_store();
|
||||
uint64_t last = 12345;
|
||||
int64_t applied = eg_wal_replay_file(trunc, &last); /* must not crash */
|
||||
int expect = count_full_records(full, L);
|
||||
if (applied != expect) { all_ok = 0; if (mismatches++ < 3)
|
||||
printf(" L=%ld applied=%lld expect=%d\n", L, (long long)applied, expect); }
|
||||
}
|
||||
ok("no crash across all truncation offsets", 1); /* reached here => survived */
|
||||
ok("recovered record count == #intact records at every offset", all_ok);
|
||||
free(full);
|
||||
}
|
||||
|
||||
/* ── Compaction crash-window convergence (§7) ───────────────────────────── */
|
||||
static void test_compaction_crash(void) {
|
||||
printf("\n== compaction crash-window convergence ==\n");
|
||||
/* Build state: base snapshot has n1; WAL adds n2,n3. */
|
||||
char dir[600]; snprintf(dir, sizeof(dir), "%s/comp", g_tmpdir); mkdir(dir, 0700);
|
||||
char base[700], wal[700], waltmp[700];
|
||||
snprintf(base, sizeof(base), "%s/snapshot.json", dir);
|
||||
snprintf(wal, sizeof(wal), "%s/engram.wal", dir);
|
||||
snprintf(waltmp, sizeof(waltmp), "%s/engram.wal.tmp", dir);
|
||||
|
||||
/* Reference full state = n1,n2,n3. */
|
||||
reset_store();
|
||||
eg_apply_node_put("{\"id\":\"n1\"}");
|
||||
eg_apply_node_put("{\"id\":\"n2\"}");
|
||||
eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
char* full = store_fingerprint();
|
||||
|
||||
/* Prepare OLD base (n1 only) + OLD wal (n2,n3). */
|
||||
reset_store(); eg_apply_node_put("{\"id\":\"n1\"}");
|
||||
engram_save((el_val_t)(uintptr_t)base);
|
||||
wal_close(); unlink(wal);
|
||||
eg_wal.fp = fopen(wal, "ab"); snprintf(eg_wal.path, sizeof(eg_wal.path), "%s", wal); eg_wal.lsn = 0; eg_wal.bytes = 0;
|
||||
reset_store(); eg_apply_node_put("{\"id\":\"n1\"}"); eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_wal_node_put(EL_STR(dir), EL_STR("n2"));
|
||||
engram_wal_node_put(EL_STR(dir), EL_STR("n3"));
|
||||
eg_wal_commit(1); wal_close();
|
||||
|
||||
/* Boot helper: load base then replay wal (mirrors server boot order). */
|
||||
#define BOOT_FP(fp) do { \
|
||||
engram_load((el_val_t)(uintptr_t)base); \
|
||||
eg_wal_replay_file(wal, NULL); \
|
||||
fp = store_fingerprint(); } while (0)
|
||||
|
||||
/* Crash BEFORE compaction (steady state). */
|
||||
char* c0; BOOT_FP(c0);
|
||||
ok("pre-compaction boot converges to full", strcmp(c0, full) == 0); free(c0);
|
||||
|
||||
/* Crash AFTER step 1 (new base written) but BEFORE wal swap:
|
||||
* base now = full (n1,n2,n3), wal still = old (n2,n3). Idempotent replay. */
|
||||
engram_load((el_val_t)(uintptr_t)base); /* reload old base into store */
|
||||
eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_save((el_val_t)(uintptr_t)base); /* == compaction step 1: new base */
|
||||
char* c1; BOOT_FP(c1);
|
||||
ok("crash after new-base, before wal-swap → converges", strcmp(c1, full) == 0); free(c1);
|
||||
|
||||
/* Crash AFTER wal.tmp written but BEFORE rename: stray tmp ignored,
|
||||
* old wal still authoritative over (new) base. */
|
||||
{ FILE* tf = fopen(waltmp, "wb"); const char* junk = "PARTIAL"; fwrite(junk,1,7,tf); fclose(tf); }
|
||||
char* c2; BOOT_FP(c2);
|
||||
ok("crash after wal.tmp, before rename → converges", strcmp(c2, full) == 0);
|
||||
unlink(waltmp); free(c2);
|
||||
|
||||
/* Crash AFTER rename (compaction complete): base=full, wal=only COMPACT_MARK. */
|
||||
reset_store();
|
||||
engram_load((el_val_t)(uintptr_t)base);
|
||||
eg_apply_node_put("{\"id\":\"n2\"}"); eg_apply_node_put("{\"id\":\"n3\"}");
|
||||
engram_wal_compact(EL_STR(dir)); /* full compaction */
|
||||
wal_close();
|
||||
char* c3;
|
||||
engram_load((el_val_t)(uintptr_t)base);
|
||||
eg_wal_replay_file(wal, NULL);
|
||||
c3 = store_fingerprint();
|
||||
ok("post-compaction boot converges to full", strcmp(c3, full) == 0);
|
||||
long wsz = file_size(wal);
|
||||
ok("post-compaction WAL truncated (only COMPACT_MARK)",
|
||||
wsz > 0 && wsz < 64); /* just the marker record */
|
||||
free(c3); free(full);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mk_tmpdir();
|
||||
printf("engram WAL test harness — tmpdir=%s\n", g_tmpdir);
|
||||
test_crc32();
|
||||
test_framing();
|
||||
test_single_ops();
|
||||
test_replay_idempotent();
|
||||
test_hebb_emb_roundtrip();
|
||||
test_data_dir();
|
||||
test_protected();
|
||||
test_replay_parity();
|
||||
test_torn_tail();
|
||||
test_compaction_crash();
|
||||
printf("\n================= %d passed, %d failed =================\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -1,466 +0,0 @@
|
||||
/* test_wal_store.c — M2 gate for the WAL + checkpoint + crash recovery + legacy
|
||||
* import layered on the M1 paged store (engram_store.{c,h}).
|
||||
*
|
||||
* Pure C. Build: gcc -O2 test_wal_store.c ../../lang/runtime/engram_store.c -o t
|
||||
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
|
||||
*
|
||||
* Covers §7/M2 gates:
|
||||
* 1 replay parity — random op stream: normal-durable path == crash-recover path
|
||||
* 2 torn-tail fuzz — truncate neuron.wal at EVERY byte offset → never crash,
|
||||
* recover to the last intact record (contiguous prefix)
|
||||
* 3 checkpoint-crash — kill at each checkpoint phase → converge, no loss past fsync
|
||||
* 4 torn-page + WAL — corrupt a store page under WAL coverage → redo re-derives
|
||||
* 5 legacy import — synth snapshot.json (emb+hebb, edges, layers) → import once,
|
||||
* bit-exact readback; JSON never re-read as the store
|
||||
* 6 hebb survives crash— hebb via WAL, crash before checkpoint → hebb recovered
|
||||
*/
|
||||
#include "../../lang/runtime/engram_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void ok(const char* name, int cond){
|
||||
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (cond) g_pass++; else g_fail++;
|
||||
}
|
||||
|
||||
static char g_base[512];
|
||||
static void mk_base(void){
|
||||
snprintf(g_base, sizeof g_base, "/tmp/engram-wal-test-%d", (int)getpid());
|
||||
mkdir(g_base, 0700);
|
||||
}
|
||||
static void mk_dir(const char* name, char* out, size_t cap){
|
||||
snprintf(out, cap, "%s/%s", g_base, name);
|
||||
mkdir(out, 0700);
|
||||
}
|
||||
|
||||
/* deterministic RNG */
|
||||
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
|
||||
|
||||
/* ── small node/edge generators (kept compact so WAL frames stay small) ─────── */
|
||||
static void gen_node(int i, int with_emb, StoreNode* n){
|
||||
memset(n, 0, sizeof *n);
|
||||
uint64_t st = 0x1234ULL ^ ((uint64_t)(i+1)*0x9E3779B97F4A7C15ULL);
|
||||
char id[32]; snprintf(id, sizeof id, "n%d", i); n->id = strdup(id);
|
||||
char c[64]; snprintf(c, sizeof c, "content-of-node-%d-%llu", i, (unsigned long long)(xs(&st)%9999));
|
||||
n->content = strdup(c);
|
||||
n->node_type = strdup("concept");
|
||||
n->tier = strdup("Working");
|
||||
n->salience = (double)(xs(&st)%100000)/7.0;
|
||||
n->importance = (double)(xs(&st)%100000)/11.0;
|
||||
n->confidence = (double)(xs(&st)%100000)/13.0;
|
||||
n->activation_count = (int64_t)(xs(&st)%1000);
|
||||
n->created_at = 1600000000000LL + i;
|
||||
n->updated_at = 1600000000000LL + i*2;
|
||||
n->layer_id = (uint32_t)(i % 4);
|
||||
n->wm_anchor = (double)(xs(&st)%1000)/3.0;
|
||||
if (with_emb){
|
||||
n->emb_dim = 32;
|
||||
n->emb = (float*)malloc(sizeof(float)*n->emb_dim);
|
||||
for (int k=0;k<n->emb_dim;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k],&u,4); }
|
||||
}
|
||||
}
|
||||
static void gen_edge(int i, const char* from, const char* to, StoreEdge* e){
|
||||
memset(e, 0, sizeof *e);
|
||||
uint64_t st = 0xABCDULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL);
|
||||
char id[32]; snprintf(id, sizeof id, "e%d", i); e->id = strdup(id);
|
||||
e->from_id = strdup(from); e->to_id = strdup(to);
|
||||
e->relation = strdup("relates_to");
|
||||
e->weight = (double)(xs(&st)%100000)/17.0;
|
||||
e->hebb = (double)(xs(&st)%100000)/100000.0;
|
||||
e->confidence = (double)(xs(&st)%100000)/19.0;
|
||||
e->created_at = 1600000000000LL + i;
|
||||
e->last_fired = 1600000000000LL + i*3;
|
||||
e->layer_id = (uint32_t)(i % 4);
|
||||
}
|
||||
|
||||
static int dcmp(double a, double b){ return a==b; }
|
||||
static int scmp(const char* a, const char* b){
|
||||
if (!a && !b) return 1; if (!a || !b) return 0; return strcmp(a,b)==0;
|
||||
}
|
||||
static int node_eq(const StoreNode* a, const StoreNode* b){
|
||||
if (!scmp(a->id,b->id) || !scmp(a->content,b->content) || !scmp(a->node_type,b->node_type) ||
|
||||
!scmp(a->tier,b->tier)) return 0;
|
||||
if (!dcmp(a->salience,b->salience) || !dcmp(a->importance,b->importance) ||
|
||||
!dcmp(a->confidence,b->confidence) || a->activation_count!=b->activation_count ||
|
||||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
||||
a->layer_id!=b->layer_id || !dcmp(a->wm_anchor,b->wm_anchor)) return 0;
|
||||
if (a->emb_dim != b->emb_dim) return 0;
|
||||
if (a->emb_dim>0){
|
||||
if (!a->emb || !b->emb) return 0;
|
||||
if (memcmp(a->emb, b->emb, sizeof(float)*a->emb_dim)!=0) return 0; /* bit-exact */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static int edge_eq(const StoreEdge* a, const StoreEdge* b){
|
||||
return scmp(a->id,b->id) && scmp(a->from_id,b->from_id) && scmp(a->to_id,b->to_id) &&
|
||||
scmp(a->relation,b->relation) && dcmp(a->weight,b->weight) && dcmp(a->hebb,b->hebb) &&
|
||||
dcmp(a->confidence,b->confidence) && a->created_at==b->created_at &&
|
||||
a->last_fired==b->last_fired && a->layer_id==b->layer_id;
|
||||
}
|
||||
|
||||
/* whole-file read / write helpers (for torn-tail + torn-page fuzzing) */
|
||||
static uint8_t* read_file(const char* p, long* len){
|
||||
FILE* f=fopen(p,"rb"); if(!f) return NULL;
|
||||
fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
|
||||
uint8_t* b=malloc(n?n:1); if(fread(b,1,n,f)!=(size_t)n){ fclose(f); free(b); return NULL; }
|
||||
fclose(f); *len=n; return b;
|
||||
}
|
||||
static void write_file(const char* p, const uint8_t* b, long len){
|
||||
FILE* f=fopen(p,"wb"); fwrite(b,1,len,f); fclose(f);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 1 — replay parity ═══════════════════════ */
|
||||
#define UNIV_NODES 60
|
||||
#define UNIV_EDGES 40
|
||||
static void test_replay_parity(void){
|
||||
printf("\n== replay parity: normal-durable path == crash-then-recover path ==\n");
|
||||
char da[600], db[600]; mk_dir("parityA", da, sizeof da); mk_dir("parityB", db, sizeof db);
|
||||
EngramPagedStore* A = engram_open(da);
|
||||
EngramPagedStore* B = engram_open(db);
|
||||
ok("opened both stores", A && B);
|
||||
if (!A || !B) return;
|
||||
|
||||
uint64_t rng = 0xF00DFACEULL;
|
||||
int OPS = 800;
|
||||
for (int step=0; step<OPS; step++){
|
||||
uint64_t r = xs(&rng);
|
||||
int kind = r % 100;
|
||||
if (kind < 45){ /* node put / re-put */
|
||||
int i = (int)(xs(&rng) % UNIV_NODES);
|
||||
StoreNode n; gen_node(i, (i%3)==0, &n);
|
||||
n.activation_count += step; /* vary re-puts */
|
||||
store_put_node(A,&n); store_put_node(B,&n);
|
||||
store_node_free(&n);
|
||||
} else if (kind < 80){ /* edge put */
|
||||
int i = (int)(xs(&rng) % UNIV_EDGES);
|
||||
char from[32], to[32];
|
||||
snprintf(from,sizeof from,"n%d",(int)(xs(&rng)%UNIV_NODES));
|
||||
snprintf(to,sizeof to,"n%d",(int)(xs(&rng)%UNIV_NODES));
|
||||
StoreEdge e; gen_edge(i, from, to, &e);
|
||||
store_put_edge(A,&e); store_put_edge(B,&e);
|
||||
store_edge_free(&e);
|
||||
} else if (kind < 88){ /* tombstone a node */
|
||||
int i = (int)(xs(&rng) % UNIV_NODES);
|
||||
char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
store_tombstone(A,id); store_tombstone(B,id);
|
||||
} else if (kind < 94){ /* hebb batch on a couple edges */
|
||||
StoreHebbDelta d[3]; char ids[3][32];
|
||||
int m = 1 + (int)(xs(&rng)%3);
|
||||
for (int j=0;j<m;j++){ snprintf(ids[j],sizeof ids[j],"e%d",(int)(xs(&rng)%UNIV_EDGES));
|
||||
d[j].edge_id=ids[j]; d[j].hebb=(double)(xs(&rng)%100000)/100000.0; d[j].last_fired=1700000000000LL+step; }
|
||||
store_hebb_batch(A,d,m); store_hebb_batch(B,d,m);
|
||||
} else { /* layer put */
|
||||
StoreLayer L; memset(&L,0,sizeof L);
|
||||
L.layer_id=(uint32_t)(xs(&rng)%4); char nm[32]; snprintf(nm,sizeof nm,"layer-%u-%d",L.layer_id,step);
|
||||
L.name=nm; L.activation_priority=(uint32_t)(xs(&rng)%10); L.suppressible=(int)(xs(&rng)%2);
|
||||
store_put_layer(A,&L); store_put_layer(B,&L);
|
||||
}
|
||||
}
|
||||
|
||||
/* A: the normal durable path (checkpoint + clean close), then reopen. */
|
||||
engram_close(A);
|
||||
A = engram_open(da);
|
||||
/* B: power loss with NO checkpoint since open → recover purely from the WAL. */
|
||||
store__crash(B);
|
||||
B = engram_open(db);
|
||||
ok("A reopened, B recovered from WAL", A && B);
|
||||
if (!A || !B) return;
|
||||
|
||||
int node_mismatch=0, edge_mismatch=0, presence_mismatch=0;
|
||||
for (int i=0;i<UNIV_NODES;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
StoreNode na, nb; int ra=store_get_node(A,id,&na), rb=store_get_node(B,id,&nb);
|
||||
if (ra!=rb){ presence_mismatch++; }
|
||||
else if (ra==1){ if (!node_eq(&na,&nb)) node_mismatch++; }
|
||||
if (ra==1) store_node_free(&na); if (rb==1) store_node_free(&nb);
|
||||
}
|
||||
for (int i=0;i<UNIV_EDGES;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"e%d",i);
|
||||
StoreEdge ea, eb; int ra=store_get_edge(A,id,&ea), rb=store_get_edge(B,id,&eb);
|
||||
if (ra!=rb){ presence_mismatch++; }
|
||||
else if (ra==1){ if (!edge_eq(&ea,&eb)) edge_mismatch++; }
|
||||
if (ra==1) store_edge_free(&ea); if (rb==1) store_edge_free(&eb);
|
||||
}
|
||||
/* adjacency parity (no duplicate edges after re-put/hebb supersede) */
|
||||
int adj_mismatch=0;
|
||||
for (int i=0;i<UNIV_NODES;i++){
|
||||
char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
StoreEdge *fa,*fb; size_t na2, nb2;
|
||||
store_get_edges_from(A,id,&fa,&na2); store_get_edges_from(B,id,&fb,&nb2);
|
||||
if (na2!=nb2) adj_mismatch++;
|
||||
store_edges_free(fa,na2); store_edges_free(fb,nb2);
|
||||
}
|
||||
/* layer parity */
|
||||
StoreLayer *la,*lb; size_t nla,nlb;
|
||||
store_list_layers(A,&la,&nla); store_list_layers(B,&lb,&nlb);
|
||||
|
||||
ok("node presence identical (oracle vs recovered)", presence_mismatch==0);
|
||||
ok("all live nodes bit-exact (incl emb)", node_mismatch==0);
|
||||
ok("all live edges exact (incl hebb)", edge_mismatch==0);
|
||||
ok("adjacency counts identical (no dup edges)", adj_mismatch==0);
|
||||
ok("layer set identical", nla==nlb);
|
||||
ok("recovered store_check clean", store_check(B, STORE_CHECK_CRC)==0);
|
||||
printf(" ops=%d nodes=%d edges=%d layersA=%zu layersB=%zu\n", OPS, UNIV_NODES, UNIV_EDGES, nla, nlb);
|
||||
store_layers_free(la,nla); store_layers_free(lb,nlb);
|
||||
engram_close(A); engram_close(B);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 2 — torn-tail fuzz ═══════════════════════ */
|
||||
#define TT_NODES 14
|
||||
static void test_torn_tail(void){
|
||||
printf("\n== torn-tail fuzz: truncate neuron.wal at every byte offset ==\n");
|
||||
char base[600]; mk_dir("tornbase", base, sizeof base);
|
||||
EngramPagedStore* s = engram_open(base);
|
||||
for (int i=0;i<TT_NODES;i++){ StoreNode n; gen_node(i,0,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
store__crash(s); /* leave store(at ckpt) + full WAL on disk */
|
||||
|
||||
char sp[700], wp[700]; snprintf(sp,sizeof sp,"%s/neuron.egm",base); snprintf(wp,sizeof wp,"%s/neuron.wal",base);
|
||||
long slen, wlen; uint8_t* sb=read_file(sp,&slen); uint8_t* wb=read_file(wp,&wlen);
|
||||
ok("captured store + WAL images", sb && wb);
|
||||
if (!sb || !wb) return;
|
||||
|
||||
char work[600]; mk_dir("tornwork", work, sizeof work);
|
||||
char wsp[700], wwp[700]; snprintf(wsp,sizeof wsp,"%s/neuron.egm",work); snprintf(wwp,sizeof wwp,"%s/neuron.wal",work);
|
||||
|
||||
int crashes=0, dirty_check=0, non_prefix=0, full_recovered=0;
|
||||
for (long t=0; t<=wlen; t++){
|
||||
write_file(wsp, sb, slen);
|
||||
write_file(wwp, wb, t); /* WAL truncated to t bytes */
|
||||
EngramPagedStore* r = engram_open(work);
|
||||
if (!r){ crashes++; continue; }
|
||||
if (store_check(r, STORE_CHECK_CRC)!=0) dirty_check++;
|
||||
/* recovered set must be a contiguous prefix n0..n{c-1} */
|
||||
int c=0; while (c<TT_NODES){ char id[32]; snprintf(id,sizeof id,"n%d",c);
|
||||
StoreNode n; int hit=store_get_node(r,id,&n); if(hit==1) store_node_free(&n); if(!hit) break; c++; }
|
||||
for (int k=c;k<TT_NODES;k++){ char id[32]; snprintf(id,sizeof id,"n%d",k);
|
||||
StoreNode n; int hit=store_get_node(r,id,&n); if(hit==1){ store_node_free(&n); non_prefix++; break; } }
|
||||
if (c==TT_NODES) full_recovered++;
|
||||
engram_close(r);
|
||||
}
|
||||
ok("recovery never crashed at any truncation offset", crashes==0);
|
||||
ok("recovered store_check clean at every offset", dirty_check==0);
|
||||
ok("recovered set always a contiguous prefix (last intact record)", non_prefix==0);
|
||||
ok("full WAL length recovers all records", full_recovered>0);
|
||||
printf(" WAL bytes fuzzed=%ld full-recover offsets=%d\n", wlen, full_recovered);
|
||||
free(sb); free(wb);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 3 — checkpoint-crash ═══════════════════════ */
|
||||
#define CK_NODES 30
|
||||
#define CK_EDGES 20
|
||||
static int build_and_crash_at_phase(const char* dir, int phase){
|
||||
EngramPagedStore* s = engram_open(dir);
|
||||
if (!s) return -1;
|
||||
for (int i=0;i<CK_NODES;i++){ StoreNode n; gen_node(i,(i%2)==0,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
for (int i=0;i<CK_EDGES;i++){ char f[32],t[32]; snprintf(f,sizeof f,"n%d",i%CK_NODES); snprintf(t,sizeof t,"n%d",(i+1)%CK_NODES);
|
||||
StoreEdge e; gen_edge(i,f,t,&e); store_put_edge(s,&e); store_edge_free(&e); }
|
||||
store__checkpoint_crashat(s, phase); /* crashes (frees s) after `phase` */
|
||||
return 0;
|
||||
}
|
||||
static int verify_full(const char* dir){
|
||||
EngramPagedStore* s = engram_open(dir);
|
||||
if (!s) return -1;
|
||||
int miss=0;
|
||||
for (int i=0;i<CK_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
StoreNode n; int r=store_get_node(s,id,&n); if(r!=1){ miss++; } else store_node_free(&n); }
|
||||
for (int i=0;i<CK_EDGES;i++){ char id[32]; snprintf(id,sizeof id,"e%d",i);
|
||||
StoreEdge e; int r=store_get_edge(s,id,&e); if(r!=1){ miss++; } else store_edge_free(&e); }
|
||||
int chk = store_check(s, STORE_CHECK_CRC);
|
||||
engram_close(s);
|
||||
return (miss==0 && chk==0) ? 0 : 1;
|
||||
}
|
||||
static void test_checkpoint_crash(void){
|
||||
printf("\n== checkpoint-crash: kill at each phase → converge, no loss past fsync ==\n");
|
||||
for (int phase=0; phase<=4; phase++){
|
||||
char nm[32], dir[600]; snprintf(nm,sizeof nm,"ckpt%d",phase); mk_dir(nm, dir, sizeof dir);
|
||||
build_and_crash_at_phase(dir, phase);
|
||||
int rc = verify_full(dir);
|
||||
char msg[96]; snprintf(msg,sizeof msg,"phase %d (%s): full recover + crc clean", phase,
|
||||
phase==0?"pre-flush":phase==1?"post-flush":phase==2?"post-fsync":phase==3?"post-SB":"post-WAL-reclaim");
|
||||
ok(msg, rc==0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 4 — torn-page + WAL ═══════════════════════ */
|
||||
#define TP_NODES 45
|
||||
static void test_torn_page(void){
|
||||
printf("\n== torn-page + WAL: corrupt a store page under WAL coverage → redo ==\n");
|
||||
char dir[600]; mk_dir("tornpage", dir, sizeof dir);
|
||||
EngramPagedStore* s = engram_open(dir); /* fresh → auto checkpoint (C=0) */
|
||||
for (int i=0;i<TP_NODES;i++){ StoreNode n; gen_node(i,0,&n); store_put_node(s,&n); store_node_free(&n); }
|
||||
store__flush_pages(s); /* steal: post-checkpoint pages hit disk */
|
||||
store__crash(s);
|
||||
|
||||
/* corrupt the highest-id NODE data page on disk (its records are post-checkpoint,
|
||||
* so the WAL still covers them). */
|
||||
char sp[700]; snprintf(sp,sizeof sp,"%s/neuron.egm",dir);
|
||||
long slen; uint8_t* sb=read_file(sp,&slen);
|
||||
long pages = slen/16384;
|
||||
long victim = -1;
|
||||
for (long p=2;p<pages;p++){ if (sb[p*16384+8]==1 /*STORE_PT_NODE*/) victim=p; }
|
||||
ok("found a NODE page to corrupt", victim>=0);
|
||||
if (victim>=0){
|
||||
for (int k=0;k<64;k++) sb[victim*16384 + 200 + k] ^= 0xA5; /* trash record area → bad crc */
|
||||
write_file(sp, sb, slen);
|
||||
}
|
||||
free(sb);
|
||||
|
||||
EngramPagedStore* r = engram_open(dir); /* heal torn page + replay WAL */
|
||||
ok("reopened after page corruption", r!=NULL);
|
||||
if (r){
|
||||
int miss=0;
|
||||
for (int i=0;i<TP_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
StoreNode n; StoreNode ref; gen_node(i,0,&ref);
|
||||
int hit=store_get_node(r,id,&n);
|
||||
if (hit!=1 || !node_eq(&n,&ref)) miss++;
|
||||
if (hit==1) store_node_free(&n); store_node_free(&ref);
|
||||
}
|
||||
ok("every record re-derived via WAL redo", miss==0);
|
||||
engram_checkpoint(r);
|
||||
ok("store_check clean after heal + checkpoint", store_check(r, STORE_CHECK_CRC)==0);
|
||||
engram_close(r);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 5 — legacy import parity ═══════════════════ */
|
||||
#define LG_NODES 8
|
||||
#define LG_EDGES 6
|
||||
static void test_legacy_import(void){
|
||||
printf("\n== legacy import parity: snapshot.json → import once → bit-exact ==\n");
|
||||
char dir[600]; mk_dir("legacy", dir, sizeof dir);
|
||||
char snap[700]; snprintf(snap,sizeof snap,"%s/snapshot.json",dir);
|
||||
|
||||
/* build oracle nodes/edges, emit them as a legacy-format snapshot.json */
|
||||
StoreNode onodes[LG_NODES]; StoreEdge oedges[LG_EDGES];
|
||||
FILE* f = fopen(snap,"wb");
|
||||
fprintf(f, "{\"nodes\":[");
|
||||
for (int i=0;i<LG_NODES;i++){
|
||||
gen_node(i, 1, &onodes[i]);
|
||||
StoreNode* n=&onodes[i];
|
||||
/* finite emb values so JSON text round-trips bit-exact (random bit patterns
|
||||
* would be NaN/inf, which %g/strtof cannot preserve). %.9g round-trips a
|
||||
* float32 exactly; %.17g round-trips a double exactly. */
|
||||
{ uint64_t es = 0x5151ULL ^ ((uint64_t)(i+1)*0x2545F4914F6CDD1DULL);
|
||||
for (int k=0;k<n->emb_dim;k++) n->emb[k] = (float)((double)(xs(&es)%2000001)/1000000.0 - 1.0); }
|
||||
fprintf(f, "%s{\"id\":\"%s\",\"content\":\"%s\",\"node_type\":\"%s\",\"tier\":\"%s\","
|
||||
"\"salience\":%.17g,\"importance\":%.17g,\"confidence\":%.17g,"
|
||||
"\"activation_count\":%lld,\"created_at\":%lld,\"updated_at\":%lld,"
|
||||
"\"layer_id\":%u,\"wm_anchor\":%.17g,\"emb\":\"",
|
||||
i?",":"", n->id, n->content, n->node_type, n->tier,
|
||||
n->salience, n->importance, n->confidence,
|
||||
(long long)n->activation_count, (long long)n->created_at, (long long)n->updated_at,
|
||||
n->layer_id, n->wm_anchor);
|
||||
for (int k=0;k<n->emb_dim;k++) fprintf(f, "%s%.9g", k?",":"", (double)n->emb[k]); /* exact float32 repr */
|
||||
fprintf(f, "\"}");
|
||||
}
|
||||
fprintf(f, "],\"edges\":[");
|
||||
for (int i=0;i<LG_EDGES;i++){
|
||||
char from[32],to[32]; snprintf(from,sizeof from,"n%d",i%LG_NODES); snprintf(to,sizeof to,"n%d",(i+2)%LG_NODES);
|
||||
gen_edge(i, from, to, &oedges[i]); oedges[i].hebb = 0.100000 + i*0.010000; /* clean decimals */
|
||||
StoreEdge* e=&oedges[i];
|
||||
fprintf(f, "%s{\"id\":\"%s\",\"from_id\":\"%s\",\"to_id\":\"%s\",\"relation\":\"%s\","
|
||||
"\"weight\":%.17g,\"hebb\":%.17g,\"confidence\":%.17g,\"created_at\":%lld,"
|
||||
"\"last_fired\":%lld,\"inhibitory\":0,\"layer_id\":%u}",
|
||||
i?",":"", e->id, e->from_id, e->to_id, e->relation,
|
||||
e->weight, e->hebb, e->confidence, (long long)e->created_at, (long long)e->last_fired, e->layer_id);
|
||||
}
|
||||
fprintf(f, "],\"layers\":[");
|
||||
fprintf(f, "{\"layer_id\":0,\"name\":\"SAFETY\",\"activation_priority\":9,\"suppressible\":0,\"transparent\":0,\"injectable\":0},");
|
||||
fprintf(f, "{\"layer_id\":1,\"name\":\"CORE_IDENTITY\",\"activation_priority\":8,\"suppressible\":0,\"transparent\":1,\"injectable\":1}");
|
||||
fprintf(f, "]}");
|
||||
fclose(f);
|
||||
|
||||
EngramPagedStore* s = engram_open(dir); /* store absent + snapshot present → import */
|
||||
ok("engram_open imported the snapshot", s!=NULL);
|
||||
char sp[700]; snprintf(sp,sizeof sp,"%s/neuron.egm",dir); struct stat st;
|
||||
ok("neuron.egm created by import", stat(sp,&st)==0);
|
||||
if (!s) return;
|
||||
|
||||
int nmiss=0, embmiss=0;
|
||||
for (int i=0;i<LG_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
|
||||
StoreNode got; int hit=store_get_node(s,id,&got);
|
||||
if (hit!=1 || !node_eq(&got,&onodes[i])) nmiss++;
|
||||
if (hit==1){ if (got.emb_dim!=onodes[i].emb_dim || (got.emb_dim>0 && memcmp(got.emb,onodes[i].emb,sizeof(float)*got.emb_dim)!=0)) embmiss++; store_node_free(&got); }
|
||||
}
|
||||
int emiss=0, hebbmiss=0;
|
||||
for (int i=0;i<LG_EDGES;i++){ char id[32]; snprintf(id,sizeof id,"e%d",i);
|
||||
StoreEdge got; int hit=store_get_edge(s,id,&got);
|
||||
if (hit!=1 || !edge_eq(&got,&oedges[i])) emiss++;
|
||||
if (hit==1){ if (got.hebb!=oedges[i].hebb) hebbmiss++; store_edge_free(&got); }
|
||||
}
|
||||
StoreLayer *ll; size_t nll; store_list_layers(s,&ll,&nll);
|
||||
ok("all nodes imported & readback matches JSON", nmiss==0);
|
||||
ok("emb bit-exact through import", embmiss==0);
|
||||
ok("all edges imported & readback matches JSON", emiss==0);
|
||||
ok("hebb exact through import", hebbmiss==0);
|
||||
ok("layers imported (2)", nll==2);
|
||||
store_layers_free(ll,nll);
|
||||
engram_close(s);
|
||||
|
||||
/* JSON must NEVER be read as the store again: mutate snapshot.json, reopen,
|
||||
* and confirm the store is unaffected (still the imported data). */
|
||||
FILE* g=fopen(snap,"wb"); fprintf(g, "{\"nodes\":[{\"id\":\"BOGUS\",\"content\":\"x\"}],\"edges\":[],\"layers\":[]}"); fclose(g);
|
||||
EngramPagedStore* s2 = engram_open(dir);
|
||||
StoreNode bogus; int bhit = store_get_node(s2,"BOGUS",&bogus); if (bhit==1) store_node_free(&bogus);
|
||||
StoreNode n0; int n0hit = store_get_node(s2,"n0",&n0); if (n0hit==1) store_node_free(&n0);
|
||||
ok("reopen does NOT re-import mutated JSON (BOGUS absent)", bhit==0);
|
||||
ok("store remains authoritative (n0 still present)", n0hit==1);
|
||||
for (int i=0;i<LG_NODES;i++) store_node_free(&onodes[i]);
|
||||
for (int i=0;i<LG_EDGES;i++) store_edge_free(&oedges[i]);
|
||||
engram_close(s2);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════ TEST 6 — hebb survives crash ═══════════════════ */
|
||||
static void test_hebb_survives(void){
|
||||
printf("\n== hebb survives crash: WAL hebb write, crash before checkpoint ==\n");
|
||||
char dir[600]; mk_dir("hebb", dir, sizeof dir);
|
||||
EngramPagedStore* s = engram_open(dir);
|
||||
StoreEdge e; gen_edge(0,"n0","n1",&e); e.hebb=0.0; store_put_edge(s,&e); store_edge_free(&e);
|
||||
engram_checkpoint(s); /* edge durable with hebb 0 */
|
||||
/* now learn: bump hebb via a WAL HEBB_BATCH, crash BEFORE the next checkpoint */
|
||||
StoreHebbDelta d = { "e0", 0.777000, 1700000000000LL };
|
||||
store_hebb_batch(s, &d, 1);
|
||||
store__crash(s);
|
||||
|
||||
EngramPagedStore* r = engram_open(dir); /* recover from WAL */
|
||||
ok("reopened after crash", r!=NULL);
|
||||
if (r){
|
||||
StoreEdge got; int hit=store_get_edge(r,"e0",&got);
|
||||
ok("edge present after crash", hit==1);
|
||||
ok("learned hebb (0.777) survived the crash", hit==1 && got.hebb==0.777000);
|
||||
ok("exactly one live e0 (hebb update superseded old)", 1);
|
||||
if (hit==1){ printf(" recovered hebb = %.6f\n", got.hebb); store_edge_free(&got); }
|
||||
engram_close(r);
|
||||
}
|
||||
/* also: hebb written via store_put_edge, crash before any checkpoint */
|
||||
char dir2[600]; mk_dir("hebb2", dir2, sizeof dir2);
|
||||
EngramPagedStore* s2 = engram_open(dir2);
|
||||
StoreEdge e2; gen_edge(5,"nA","nB",&e2); e2.hebb=0.314159; store_put_edge(s2,&e2); store_edge_free(&e2);
|
||||
store__crash(s2);
|
||||
EngramPagedStore* r2 = engram_open(dir2);
|
||||
StoreEdge g2; int h2 = store_get_edge(r2,"e5",&g2);
|
||||
ok("edge+hebb from a pre-checkpoint put recovered", h2==1 && g2.hebb==0.314159);
|
||||
if (h2==1) store_edge_free(&g2);
|
||||
engram_close(r2);
|
||||
}
|
||||
|
||||
int main(void){
|
||||
mk_base();
|
||||
printf("engram M2 gate — WAL + checkpoint + recovery + legacy import\n");
|
||||
printf("throwaway dir: %s\n", g_base);
|
||||
test_replay_parity();
|
||||
test_torn_tail();
|
||||
test_checkpoint_crash();
|
||||
test_torn_page();
|
||||
test_legacy_import();
|
||||
test_hebb_survives();
|
||||
printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -17,16 +17,6 @@
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated install.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn manifest_name(src: String) -> String // manifest.el
|
||||
extern fn manifest_deps(src: String) -> String // manifest.el
|
||||
extern fn registry_token() -> String // registry.el
|
||||
extern fn registry_find(name: String, version: String) -> String // registry.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Install paths ─────────────────────────────────────────────────────────────
|
||||
|
||||
// packages_dir returns the root directory for installed vessels.
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// These symbols are defined in sibling epm modules or the El runtime and are
|
||||
// resolved at link time. The `extern fn` decls give elc the C prototype so the
|
||||
// generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang)
|
||||
// that reject implicit function declarations. Signature arity must match the
|
||||
// definition; return/param types are informational (all lower to el_val_t).
|
||||
extern fn config(key: String) -> String // El runtime builtin
|
||||
extern fn read_installed() -> String // install.el
|
||||
|
||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
||||
|
||||
@@ -6,15 +6,6 @@
|
||||
// Depends on: registry.el (registry_latest_version, registry_find),
|
||||
// install.el (read_installed, install_vessel, installed_version)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated update.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn read_installed() -> String // install.el
|
||||
extern fn installed_version(name: String) -> String // install.el
|
||||
extern fn install_vessel(name: String, version: String) -> Bool // install.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Semver helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// semver_part extracts the Nth dot-separated component from a semver string.
|
||||
|
||||
+6
-6
@@ -27,11 +27,11 @@ This is where almost all work belongs. El programs are source files that get com
|
||||
|
||||
**Do not add C code when El can express it.** If functionality can be built from existing El primitives (string ops, `exec`, `fs_read/write`, `http_post`, etc.), write it in El.
|
||||
|
||||
### Layer 2: The C seed (`runtime/el_seed.c`)
|
||||
### Layer 2: The C seed (`el-compiler/runtime/el_seed.c`)
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The old `el_runtime.c` has been archived to `runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
The old `el_runtime.c` has been archived to `el-compiler/runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
|
||||
**Only edit `el_seed.c` when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features). For everything else, write El.
|
||||
|
||||
@@ -50,9 +50,9 @@ After changing any `.el` source in `el-compiler/src/`:
|
||||
```bash
|
||||
cd /Users/will/Development/neuron-technologies/foundation/el
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c runtime/el_seed.c
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
# Verify self-hosting:
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
@@ -104,8 +104,8 @@ Use `exec()` (blocking) or `exec_bg()` (fire-and-forget) with shell scripts to r
|
||||
| `el-compiler/src/codegen.el` | Code generator — builtin arity table lives here |
|
||||
| `el-compiler/src/lexer.el` | Lexer |
|
||||
| `el-compiler/src/parser.el` | Parser |
|
||||
| `runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `el-compiler/runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `el-compiler/runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `spec/language.md` | Language specification |
|
||||
| `BOOTSTRAP.md` | How to recover the compiler from scratch |
|
||||
| `elc-cli.el` | Compiler entry point |
|
||||
|
||||
+12
-12
@@ -50,9 +50,9 @@ To rebuild the current binary from source using the current binary:
|
||||
```bash
|
||||
cd /path/to/el
|
||||
./dist/platform/elc elc-cli.el elc-new.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c runtime/el_runtime.c
|
||||
elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
Verify self-hosting by using `elc-new` to recompile itself and diffing the outputs.
|
||||
@@ -288,14 +288,14 @@ The codegen tracks declared names per C scope. When `count` is already in `decla
|
||||
|
||||
## 3. The Runtime API
|
||||
|
||||
All runtime functions are declared in `runtime/el_runtime.h`. Every compiled El program links against `runtime/el_runtime.c`.
|
||||
All runtime functions are declared in `el-compiler/runtime/el_runtime.h`. Every compiled El program links against `el-compiler/runtime/el_runtime.c`.
|
||||
|
||||
All values are `el_val_t` (`int64_t`). Strings are pointers cast through `int64_t` using `EL_STR(s)` / `EL_CSTR(v)` macros.
|
||||
|
||||
Canonical compile command:
|
||||
```bash
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o <out> <prog>.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### I/O
|
||||
@@ -794,8 +794,8 @@ Using your minimal implementation, compile `elc-cli.el` (which imports the entir
|
||||
python3 minimal_elc.py elc-cli.el > elc-new.c
|
||||
|
||||
# Build with the runtime
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o elc-new elc-new.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-new elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### Step 5: Verify Self-Hosting
|
||||
@@ -803,8 +803,8 @@ cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
```bash
|
||||
# Compile elc-cli.el with the new compiler
|
||||
./elc-new elc-cli.el elc-v2.c
|
||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
-o elc-v2 elc-v2.c runtime/el_runtime.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-v2 elc-v2.c el-compiler/runtime/el_runtime.c
|
||||
|
||||
# Compile again with the second-generation compiler
|
||||
./elc-v2 elc-cli.el elc-v3.c
|
||||
@@ -880,9 +880,9 @@ This is the planned path. It does not exist yet.
|
||||
| `el-compiler/src/parser.el` | Recursive descent parser. `parse(tokens)` → AST. All statement and expression forms | 1071 |
|
||||
| `el-compiler/src/codegen.el` | C code emitter. `codegen(stmts, source)` → (streams to stdout). Expression codegen, statement codegen, function codegen, type tracking, capability enforcement, temporal type dispatch | 2721 |
|
||||
| `el-compiler/src/codegen-js.el` | JavaScript backend. `codegen_js(stmts, source)` → JS source | ~500 |
|
||||
| `runtime/el_runtime.h` | Full runtime API declaration | 755 |
|
||||
| `runtime/el_runtime.c` | Full runtime implementation | large |
|
||||
| `runtime/el_runtime.js` | JS runtime | — |
|
||||
| `el-compiler/runtime/el_runtime.h` | Full runtime API declaration | 755 |
|
||||
| `el-compiler/runtime/el_runtime.c` | Full runtime implementation | large |
|
||||
| `el-compiler/runtime/el_runtime.js` | JS runtime | — |
|
||||
| `elb.el` | Build coordinator. Reads `manifest.el`, walks import graph, compiles modules, links binary. The `.NET`-style incremental build model | 367 |
|
||||
| `elc-combined.el` | Pre-merged single-file bootstrap edition (for early bootstrap iterations) | large |
|
||||
| `spec/language.md` | Language specification v1.2.0 | — |
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look
|
||||
* up El function symbols at runtime. This is the foundation of El's parallelism.
|
||||
*
|
||||
* Link: cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
* Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el_seed.c
|
||||
*/
|
||||
|
||||
@@ -1072,7 +1072,6 @@ el_val_t __engram_save(el_val_t path) { return engram_save
|
||||
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
||||
|
||||
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
||||
el_val_t __engram_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
|
||||
|
||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
||||
return engram_search_json(query, limit);
|
||||
@@ -1086,47 +1085,6 @@ el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el
|
||||
return engram_scan_nodes_by_type_json(node_type, limit, offset);
|
||||
}
|
||||
|
||||
el_val_t __engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset) {
|
||||
return engram_scan_nodes_emb_json(limit, offset);
|
||||
}
|
||||
|
||||
el_val_t __engram_dreams_json(el_val_t since_ms) {
|
||||
return engram_dreams_json(since_ms);
|
||||
}
|
||||
|
||||
/* §5 geometry operators — native wrappers (surfacing via engram.el + elc fold is
|
||||
* the cutover step; the C table wiring is registered here now, per P0/P5). */
|
||||
el_val_t __engram_geo_descriptor_json(el_val_t seeds) {
|
||||
return engram_geo_descriptor_json(seeds);
|
||||
}
|
||||
el_val_t __engram_geo_overlap_json(el_val_t a_seeds, el_val_t b_seeds) {
|
||||
return engram_geo_overlap_json(a_seeds, b_seeds);
|
||||
}
|
||||
el_val_t __engram_geo_subtract_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t mode) {
|
||||
return engram_geo_subtract_json(a_seeds, b_seeds, mode);
|
||||
}
|
||||
el_val_t __engram_geo_combine_json(el_val_t a_seeds, el_val_t b_seeds) {
|
||||
return engram_geo_combine_json(a_seeds, b_seeds);
|
||||
}
|
||||
el_val_t __engram_geo_distance_json(el_val_t a_seeds, el_val_t b_seeds) {
|
||||
return engram_geo_distance_json(a_seeds, b_seeds);
|
||||
}
|
||||
el_val_t __engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds) {
|
||||
return engram_geo_analogy_json(a_seeds, b_seeds);
|
||||
}
|
||||
/* reasoning layer — ANALOGY native wrapper (same C-table wiring as the §5 ops). */
|
||||
el_val_t __engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds) {
|
||||
return engram_reason_analogy_json(a_seeds, b_seeds, c_seeds);
|
||||
}
|
||||
|
||||
el_val_t __engram_consolidate_permanence(el_val_t node_id) {
|
||||
return engram_consolidate_permanence(node_id);
|
||||
}
|
||||
|
||||
el_val_t __engram_age_field(el_val_t delta_ms) { return engram_age_field(delta_ms); }
|
||||
el_val_t __engram_age_field_catchup(void) { return engram_age_field_catchup(); }
|
||||
el_val_t __engram_chrono_persist_tick(void) { return engram_chrono_persist_tick(); }
|
||||
|
||||
el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
|
||||
return engram_neighbors_json(node_id, max_depth, direction);
|
||||
}
|
||||
@@ -226,7 +226,6 @@ 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_get_node_json(el_val_t id);
|
||||
el_val_t __engram_get_node_by_label(el_val_t label);
|
||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
+88
-1563
File diff suppressed because it is too large
Load Diff
+7
-129
@@ -22,9 +22,6 @@
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
* EL_NULL null / zero value
|
||||
* EL_FALSE boolean false (0)
|
||||
* EL_TRUE boolean true (1)
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
@@ -56,8 +53,6 @@ typedef int64_t el_val_t;
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
#define EL_FALSE ((el_val_t)0)
|
||||
#define EL_TRUE ((el_val_t)1)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
@@ -81,8 +76,8 @@ extern "C" {
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t println(el_val_t s);
|
||||
el_val_t print(el_val_t s);
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
@@ -95,7 +90,6 @@ el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t native_str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
@@ -123,10 +117,6 @@ el_val_t el_min(el_val_t a, el_val_t b);
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */
|
||||
el_val_t el_arena_push(void);
|
||||
el_val_t el_arena_pop(el_val_t mark);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
@@ -150,11 +140,10 @@ el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler(el_val_t name);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
@@ -175,8 +164,8 @@ el_val_t http_set_handler(el_val_t name);
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler_v2(el_val_t name);
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
@@ -227,15 +216,12 @@ 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 ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_list_json(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
@@ -265,9 +251,6 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_escape_string(el_val_t sv);
|
||||
el_val_t json_build_object(el_val_t kvs);
|
||||
el_val_t json_build_array(el_val_t items);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -280,7 +263,6 @@ el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
el_val_t now_ns(void);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
@@ -437,8 +419,6 @@ el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
el_val_t state_has(el_val_t key);
|
||||
el_val_t state_get_or(el_val_t key, el_val_t default_val);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -530,15 +510,9 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t exit_program(el_val_t code);
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||
* during long compilation loops (e.g. after each function is compiled).
|
||||
* Returns 0 when memory is within bounds. */
|
||||
el_val_t el_mem_check(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
@@ -776,108 +750,12 @@ el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */
|
||||
el_val_t stdout_restore(void); /* restore process stdout to terminal */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
|
||||
* The El self-hosting compiler emits calls to __-prefixed names. These are
|
||||
* forwarding wrappers around the existing el_runtime functions above. */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s);
|
||||
el_val_t __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f);
|
||||
el_val_t __str_to_float(el_val_t s);
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b);
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t __str_alloc(el_val_t n);
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code);
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
el_val_t __fs_list_raw(el_val_t path);
|
||||
|
||||
/* HTTP server */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
|
||||
|
||||
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
|
||||
el_val_t __http_conn_fd(void);
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path);
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_len(el_val_t json);
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
|
||||
el_val_t __json_parse_map(el_val_t json_str);
|
||||
el_val_t __json_stringify_val(el_val_t val);
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
el_val_t __state_set(el_val_t key, el_val_t val);
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1202,7 +1202,7 @@ fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool
|
||||
js_emit_line(js_strip_es_exports(runtime_content))
|
||||
js_emit_line("")
|
||||
} else {
|
||||
js_emit_line("// Runtime: foundation/el/runtime/el_runtime.js")
|
||||
js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
|
||||
js_emit_line("import \"./el_runtime.js\";")
|
||||
}
|
||||
// In module mode: destructure all builtins off globalThis.__el so call
|
||||
|
||||
@@ -1292,43 +1292,6 @@ fn next_if_id() -> String {
|
||||
native_int_to_str(n)
|
||||
}
|
||||
|
||||
// is_void_builtin — true for runtime builtins declared `void` in el_runtime.h.
|
||||
// User `-> Void` functions are emitted as el_val_t (return 0) so they are safe
|
||||
// to assign; only these C-level void builtins are not.
|
||||
fn is_void_builtin(name: String) -> Bool {
|
||||
if str_eq(name, "println") { return true }
|
||||
if str_eq(name, "print") { return true }
|
||||
if str_eq(name, "engram_strengthen") { return true }
|
||||
if str_eq(name, "engram_forget") { return true }
|
||||
if str_eq(name, "engram_connect") { return true }
|
||||
if str_eq(name, "dharma_emit") { return true }
|
||||
if str_eq(name, "dharma_strengthen") { return true }
|
||||
if str_eq(name, "llm_register_tool") { return true }
|
||||
if str_eq(name, "exit_program") { return true }
|
||||
if str_eq(name, "http_serve") { return true }
|
||||
if str_eq(name, "http_set_handler") { return true }
|
||||
if str_eq(name, "http_serve_async") { return true }
|
||||
if str_eq(name, "el_cgi_init") { return true }
|
||||
if str_eq(name, "el_retain") { return true }
|
||||
if str_eq(name, "el_release") { return true }
|
||||
false
|
||||
}
|
||||
|
||||
// cg_expr_is_void — true if `val` is a direct call to a void builtin, so the
|
||||
// if-expression arm must emit it as a bare statement rather than assigning its
|
||||
// (nonexistent) value to the result var.
|
||||
fn cg_expr_is_void(val: Map<String, Any>) -> Bool {
|
||||
let vk: String = val["expr"]
|
||||
if str_eq(vk, "Call") {
|
||||
let f = val["func"]
|
||||
let fk: String = f["expr"]
|
||||
if str_eq(fk, "Ident") {
|
||||
return is_void_builtin(f["name"])
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Render a single arm of the if-as-expression: emit each statement-before-last
|
||||
// as a side-effecting expression, then assign the final Expr's value to the
|
||||
// result var. If the arm body is empty or its last stmt isn't an Expr, the
|
||||
@@ -1337,10 +1300,6 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
|
||||
let n: Int = native_list_len(stmts)
|
||||
// Collect statement fragments into a list to avoid O(n-) string growth.
|
||||
let parts: [String] = native_list_empty()
|
||||
// Track names already declared in this arm's C block. El permits `let x`
|
||||
// to redeclare/rebind x in the same scope, but C forbids redeclaring the
|
||||
// same name in one block: emit `el_val_t x = ...` first, `x = ...` after.
|
||||
let declared: [String] = native_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let s = native_list_get(stmts, i)
|
||||
@@ -1351,31 +1310,18 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
|
||||
let name: String = s["name"]
|
||||
let val = s["value"]
|
||||
let val_c: String = cg_expr(val)
|
||||
if list_contains(declared, name) {
|
||||
let parts = native_list_append(parts, name + " = " + val_c + "; ")
|
||||
} else {
|
||||
let declared = native_list_append(declared, name)
|
||||
let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ")
|
||||
}
|
||||
let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ")
|
||||
} else {
|
||||
if str_eq(sk, "Return") {
|
||||
let val = s["value"]
|
||||
let val_c: String = cg_expr(val)
|
||||
if cg_expr_is_void(val) {
|
||||
let parts = native_list_append(parts, val_c + "; ")
|
||||
} else {
|
||||
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
||||
}
|
||||
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
||||
} else {
|
||||
if str_eq(sk, "Expr") {
|
||||
let val = s["value"]
|
||||
let val_c: String = cg_expr(val)
|
||||
if is_last {
|
||||
if cg_expr_is_void(val) {
|
||||
let parts = native_list_append(parts, val_c + "; ")
|
||||
} else {
|
||||
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
||||
}
|
||||
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
||||
} else {
|
||||
let parts = native_list_append(parts, "(void)(" + val_c + "); ")
|
||||
}
|
||||
@@ -2556,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 }
|
||||
@@ -2633,12 +2580,6 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "__engram_neighbors_filtered") { return 3 }
|
||||
if str_eq(name, "__engram_activate") { return 2 }
|
||||
if str_eq(name, "__engram_activate_json") { return 2 }
|
||||
if str_eq(name, "__engram_geo_descriptor_json") { return 1 }
|
||||
if str_eq(name, "__engram_geo_overlap_json") { return 2 }
|
||||
if str_eq(name, "__engram_geo_subtract_json") { return 3 }
|
||||
if str_eq(name, "__engram_geo_combine_json") { return 2 }
|
||||
if str_eq(name, "__engram_geo_distance_json") { return 2 }
|
||||
if str_eq(name, "__engram_geo_analogy_json") { return 2 }
|
||||
if str_eq(name, "__engram_scan_nodes_json") { return 2 }
|
||||
if str_eq(name, "__generate") { return 1 }
|
||||
// Filesystem
|
||||
@@ -2729,21 +2670,11 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "engram_activate") { return 2 }
|
||||
if str_eq(name, "engram_save") { return 1 }
|
||||
if str_eq(name, "engram_load") { return 1 }
|
||||
if str_eq(name, "engram_store_boot") { return 1 }
|
||||
if str_eq(name, "engram_store_checkpoint") { return 0 }
|
||||
if str_eq(name, "engram_store_close") { return 0 }
|
||||
if str_eq(name, "engram_get_node_json") { return 1 }
|
||||
if str_eq(name, "engram_get_node_by_label") { return 1 }
|
||||
if str_eq(name, "engram_search_json") { return 2 }
|
||||
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
||||
if str_eq(name, "engram_neighbors_json") { return 3 }
|
||||
if str_eq(name, "engram_activate_json") { return 2 }
|
||||
if str_eq(name, "engram_geo_descriptor_json") { return 1 }
|
||||
if str_eq(name, "engram_geo_overlap_json") { return 2 }
|
||||
if str_eq(name, "engram_geo_subtract_json") { return 3 }
|
||||
if str_eq(name, "engram_geo_combine_json") { return 2 }
|
||||
if str_eq(name, "engram_geo_distance_json") { return 2 }
|
||||
if str_eq(name, "engram_geo_analogy_json") { return 2 }
|
||||
if str_eq(name, "engram_stats_json") { return 0 }
|
||||
// LLM
|
||||
if str_eq(name, "llm_call") { return 2 }
|
||||
|
||||
@@ -23,58 +23,19 @@ fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
}
|
||||
|
||||
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
||||
// Out-of-range reads must report the Eof sentinel so every `== "Eof"`
|
||||
// termination guard in the parser fires. Without this, reading past the
|
||||
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
|
||||
// which matches no delimiter, letting inner parse loops append AST nodes
|
||||
// forever on malformed input -> unbounded allocation -> OOM.
|
||||
let n: Int = native_list_len(tokens) / 2
|
||||
if pos < 0 {
|
||||
return "Eof"
|
||||
}
|
||||
if pos >= n {
|
||||
return "Eof"
|
||||
}
|
||||
native_list_get(tokens, pos * 2)
|
||||
}
|
||||
|
||||
fn tok_value(tokens: [Any], pos: Int) -> String {
|
||||
let n: Int = native_list_len(tokens) / 2
|
||||
if pos < 0 {
|
||||
return ""
|
||||
}
|
||||
if pos >= n {
|
||||
return ""
|
||||
}
|
||||
native_list_get(tokens, pos * 2 + 1)
|
||||
}
|
||||
|
||||
// parse_progress_fatal — robustness backstop. Called by the token-consuming
|
||||
// driver loops when they detect they have iterated more times than there are
|
||||
// tokens (impossible for a well-formed program, where every iteration consumes
|
||||
// at least one token). Names the offending token and exits non-zero instead of
|
||||
// looping forever / exhausting memory.
|
||||
fn parse_progress_fatal(where: String, tokens: [Any], pos: Int) -> Void {
|
||||
let k: String = tok_kind(tokens, pos)
|
||||
let v: String = tok_value(tokens, pos)
|
||||
println("elc: FATAL: parser made no forward progress in " + where
|
||||
+ " at token index " + native_int_to_str(pos) + " (kind=" + k + ")")
|
||||
println("elc: likely a malformed construct near '" + v
|
||||
+ "' — e.g. an unterminated string or an unescaped double-quote inside a string literal (use \\\" ).")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
|
||||
let k = tok_kind(tokens, pos)
|
||||
if k == kind {
|
||||
return pos + 1
|
||||
}
|
||||
// On mismatch, error recovery is best-effort. But never step PAST the Eof
|
||||
// sentinel: once at Eof a mismatch means the input ended early, and
|
||||
// advancing would run the cursor off the token list.
|
||||
if k == "Eof" {
|
||||
return pos
|
||||
}
|
||||
// On mismatch just advance; error recovery is best-effort
|
||||
pos + 1
|
||||
}
|
||||
|
||||
@@ -1227,16 +1188,7 @@ fn parse_block(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
let p = expect(tokens, pos, "LBrace")
|
||||
let stmts: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
// Runaway backstop: a block can hold at most (token count) statements, since
|
||||
// every iteration consumes >= 1 token. If we exceed that, the cursor has run
|
||||
// off the end without terminating (malformed input) -> fail fast, don't hang.
|
||||
let blk_total: Int = native_list_len(tokens) / 2
|
||||
let blk_iters: Int = 0
|
||||
while running {
|
||||
let blk_iters = blk_iters + 1
|
||||
if blk_iters > blk_total + 8 {
|
||||
parse_progress_fatal("parse_block", tokens, p)
|
||||
}
|
||||
let k = tok_kind(tokens, p)
|
||||
if k == "RBrace" {
|
||||
let running = false
|
||||
|
||||
+24
-9
@@ -283,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>.
|
||||
@@ -305,6 +311,10 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
// 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.
|
||||
@@ -312,7 +322,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
|
||||
let master_decls: String = out_dir + "/elp-c-decls.h"
|
||||
let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no"))
|
||||
let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" }
|
||||
let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let parts = native_list_append(parts, "cc -O2 -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)
|
||||
@@ -320,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, ossl_lib_flag + " -lcurl -lssl -lcrypto -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)
|
||||
@@ -368,13 +378,13 @@ fn main() -> Void {
|
||||
let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null"))
|
||||
if !str_eq(which_out, "") {
|
||||
let elc_dir: String = dirname_of(which_out)
|
||||
runtime_path = elc_dir + "/../runtime/el_runtime.c"
|
||||
runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c"
|
||||
}
|
||||
}
|
||||
// If --runtime points to a directory, auto-locate el_runtime.c inside it.
|
||||
// This lets both forms work:
|
||||
// --runtime=/opt/el/runtime (directory form)
|
||||
// --runtime=/opt/el/runtime/el_runtime.c (file form)
|
||||
// --runtime=/opt/el/el-compiler/runtime (directory form)
|
||||
// --runtime=/opt/el/el-compiler/runtime/el_runtime.c (file form)
|
||||
if !str_eq(runtime_path, "") {
|
||||
let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file"))
|
||||
if str_eq(is_dir, "dir") {
|
||||
@@ -432,18 +442,23 @@ fn main() -> Void {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// 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 c_files = native_list_append(c_files, ec)
|
||||
let link_files = native_list_append(link_files, ec)
|
||||
let ei = ei + 1
|
||||
}
|
||||
|
||||
// Link
|
||||
let out_bin: String = out_dir + "/" + pkg_name
|
||||
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
let linked: Bool = link_binary(link_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
if !linked {
|
||||
println("elb: link failed")
|
||||
exit(1)
|
||||
|
||||
@@ -3797,9 +3797,6 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "engram_activate") { return 2 }
|
||||
if str_eq(name, "engram_save") { return 1 }
|
||||
if str_eq(name, "engram_load") { return 1 }
|
||||
if str_eq(name, "engram_store_boot") { return 1 }
|
||||
if str_eq(name, "engram_store_checkpoint") { return 0 }
|
||||
if str_eq(name, "engram_store_close") { return 0 }
|
||||
if str_eq(name, "engram_get_node_json") { return 1 }
|
||||
if str_eq(name, "engram_search_json") { return 2 }
|
||||
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
||||
|
||||
+2
-141
@@ -1423,53 +1423,15 @@ el_val_t tok_at(el_val_t tokens, el_val_t pos) {
|
||||
}
|
||||
|
||||
el_val_t tok_kind(el_val_t tokens, el_val_t pos) {
|
||||
/* Out-of-range reads MUST report the Eof sentinel so every `== "Eof"`
|
||||
termination guard in the parser fires. Without this, reading past the
|
||||
trailing Eof token returns runtime null (native_list_get OOB -> 0), which
|
||||
matches no delimiter, letting inner parse loops (parse_block, parse_binop)
|
||||
append AST nodes forever on malformed input -> unbounded allocation -> OOM. */
|
||||
el_val_t n = (native_list_len(tokens) / 2);
|
||||
if (pos < 0) {
|
||||
return EL_STR("Eof");
|
||||
}
|
||||
if (pos >= n) {
|
||||
return EL_STR("Eof");
|
||||
}
|
||||
return native_list_get(tokens, (pos * 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t tok_value(el_val_t tokens, el_val_t pos) {
|
||||
el_val_t n = (native_list_len(tokens) / 2);
|
||||
if (pos < 0) {
|
||||
return EL_STR("");
|
||||
}
|
||||
if (pos >= n) {
|
||||
return EL_STR("");
|
||||
}
|
||||
return native_list_get(tokens, ((pos * 2) + 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* parse_progress_fatal — robustness backstop. Called by the token-consuming
|
||||
driver loops when they detect they have iterated more times than there are
|
||||
tokens (an impossibility for a well-formed program, where every iteration
|
||||
consumes at least one token). Names the offending token and exits non-zero
|
||||
instead of looping forever / exhausting memory. */
|
||||
el_val_t parse_progress_fatal(el_val_t where, el_val_t tokens, el_val_t pos) {
|
||||
el_val_t k = tok_kind(tokens, pos);
|
||||
el_val_t v = tok_value(tokens, pos);
|
||||
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(
|
||||
EL_STR("elc: FATAL: parser made no forward progress in "), where),
|
||||
EL_STR(" at token index ")), native_int_to_str(pos)),
|
||||
el_str_concat(EL_STR(" (kind="), el_str_concat(k, EL_STR(")")))));
|
||||
println(el_str_concat(el_str_concat(
|
||||
EL_STR("elc: likely a malformed construct near '"), v),
|
||||
EL_STR("' — e.g. an unterminated string or an unescaped double-quote inside a string literal (use \\\" ).")));
|
||||
exit(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind) {
|
||||
el_val_t k = tok_kind(tokens, pos);
|
||||
if (str_eq(k, kind)) {
|
||||
@@ -2727,16 +2689,7 @@ el_val_t parse_block(el_val_t tokens, el_val_t pos) {
|
||||
el_val_t p = expect(tokens, pos, EL_STR("LBrace"));
|
||||
el_val_t stmts = native_list_empty();
|
||||
el_val_t running = 1;
|
||||
/* Runaway backstop: a block can hold at most (token count) statements, since
|
||||
every iteration consumes >= 1 token. If we exceed that, the cursor has run
|
||||
off the end without terminating (malformed input) -> fail fast, don't hang. */
|
||||
el_val_t __blk_total = (native_list_len(tokens) / 2);
|
||||
el_val_t __blk_iters = 0;
|
||||
while (running) {
|
||||
__blk_iters = (__blk_iters + 1);
|
||||
if (__blk_iters > (__blk_total + 8)) {
|
||||
parse_progress_fatal(EL_STR("parse_block"), tokens, p);
|
||||
}
|
||||
el_val_t k = tok_kind(tokens, p);
|
||||
if (str_eq(k, EL_STR("RBrace"))) {
|
||||
running = 0;
|
||||
@@ -4885,51 +4838,9 @@ el_val_t next_if_id(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* is_void_builtin — true for runtime builtins declared `void` in el_runtime.h.
|
||||
User `-> Void` functions are emitted as el_val_t (return 0) so they are safe
|
||||
to assign; only these C-level void builtins are not. */
|
||||
el_val_t is_void_builtin(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("println"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("print"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("engram_strengthen"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("engram_forget"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("engram_connect"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("dharma_emit"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("dharma_strengthen"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("llm_register_tool"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("exit_program"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("http_serve"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("http_set_handler"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("http_serve_async"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("el_cgi_init"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("el_retain"))) { return 1; }
|
||||
if (str_eq(name, EL_STR("el_release"))) { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* cg_expr_is_void — true if `val` is a direct call to a void builtin, so the
|
||||
if-expression arm must emit it as a bare statement rather than assigning its
|
||||
(nonexistent) value to the result var. */
|
||||
el_val_t cg_expr_is_void(el_val_t val) {
|
||||
el_val_t vk = el_get_field(val, EL_STR("expr"));
|
||||
if (str_eq(vk, EL_STR("Call"))) {
|
||||
el_val_t f = el_get_field(val, EL_STR("func"));
|
||||
el_val_t fk = el_get_field(f, EL_STR("expr"));
|
||||
if (str_eq(fk, EL_STR("Ident"))) {
|
||||
return is_void_builtin(el_get_field(f, EL_STR("name")));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
|
||||
el_val_t n = native_list_len(stmts);
|
||||
el_val_t parts = native_list_empty();
|
||||
/* Track names already declared in this arm's C block. El permits `let x`
|
||||
to redeclare/rebind x in the same scope, but C forbids redeclaring the
|
||||
same name in one block. Emit `el_val_t x = ...` the first time and a
|
||||
plain `x = ...` reassignment thereafter (mirrors cg_stmt's `declared`). */
|
||||
el_val_t declared = native_list_empty();
|
||||
el_val_t i = 0;
|
||||
while (i < n) {
|
||||
el_val_t s = native_list_get(stmts, i);
|
||||
@@ -4942,31 +4853,18 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
|
||||
el_val_t name = el_get_field(s, EL_STR("name"));
|
||||
el_val_t val = el_get_field(s, EL_STR("value"));
|
||||
el_val_t val_c = cg_expr(val);
|
||||
if (list_contains(declared, name)) {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(name, EL_STR(" = ")), val_c), EL_STR("; ")));
|
||||
} else {
|
||||
declared = native_list_append(declared, name);
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), name), EL_STR(" = ")), val_c), EL_STR("; ")));
|
||||
}
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), name), EL_STR(" = ")), val_c), EL_STR("; ")));
|
||||
} else {
|
||||
if (str_eq(sk, EL_STR("Return"))) {
|
||||
el_val_t val = el_get_field(s, EL_STR("value"));
|
||||
el_val_t val_c = cg_expr(val);
|
||||
if (cg_expr_is_void(val)) {
|
||||
parts = native_list_append(parts, el_str_concat(val_c, EL_STR("; ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
|
||||
}
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
|
||||
} else {
|
||||
if (str_eq(sk, EL_STR("Expr"))) {
|
||||
el_val_t val = el_get_field(s, EL_STR("value"));
|
||||
el_val_t val_c = cg_expr(val);
|
||||
if (is_last) {
|
||||
if (cg_expr_is_void(val)) {
|
||||
parts = native_list_append(parts, el_str_concat(val_c, EL_STR("; ")));
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
|
||||
}
|
||||
} else {
|
||||
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("(void)("), val_c), EL_STR("); ")));
|
||||
}
|
||||
@@ -4985,7 +4883,6 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
|
||||
}
|
||||
el_val_t result = str_join(parts, EL_STR(""));
|
||||
el_release(parts);
|
||||
el_release(declared);
|
||||
return result;
|
||||
return 0;
|
||||
}
|
||||
@@ -6748,24 +6645,6 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("__engram_activate_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_descriptor_json"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_overlap_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_subtract_json"))) {
|
||||
return 3;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_combine_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_distance_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_geo_analogy_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("__engram_scan_nodes_json"))) {
|
||||
return 2;
|
||||
}
|
||||
@@ -7009,24 +6888,6 @@ el_val_t builtin_arity(el_val_t name) {
|
||||
if (str_eq(name, EL_STR("engram_activate_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_descriptor_json"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_overlap_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_subtract_json"))) {
|
||||
return 3;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_combine_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_distance_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_geo_analogy_json"))) {
|
||||
return 2;
|
||||
}
|
||||
if (str_eq(name, EL_STR("engram_stats_json"))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//
|
||||
// Compile and run:
|
||||
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
|
||||
// cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c runtime/el_runtime.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
|
||||
// /tmp/html-page
|
||||
|
||||
fn render_item(item: String) -> String {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
CompileFlags:
|
||||
Add:
|
||||
- -I/opt/homebrew/Cellar/liboqs/0.15.0/include
|
||||
- -I/opt/homebrew/opt/openssl@3/include
|
||||
- -std=c11
|
||||
@@ -0,0 +1,28 @@
|
||||
# El Compiler Release v1.0.0 — 2026-05-02
|
||||
|
||||
## Components
|
||||
- `bootstrap.py` — El language compiler (Python, recursive descent parser, emits C)
|
||||
- `el_runtime.c` — El runtime (C, HTTP server, engram, DHARMA, LLM chain)
|
||||
- `el_runtime.h` — Runtime public API header
|
||||
|
||||
## Changes in this release
|
||||
|
||||
### Critical bug fixes
|
||||
- `state_set`/`state_get` are now thread-safe (pthread_mutex). Was racing across 64 worker threads.
|
||||
- `looks_like_string` threshold raised from 1,000,000 to 4GB. Unix timestamps were being dereferenced as heap pointers.
|
||||
- `fs_read` guards against negative `ftell` result (pipe/special file overflow).
|
||||
|
||||
### Engram architecture (major)
|
||||
- Two-layer activation: `background_activation` (Layer 1, broad fan-out) + `working_memory_weight` (Layer 2, executive filter)
|
||||
- Inhibitory edges: `EngramEdge.inhibitory` flag suppresses working memory promotion without affecting background activation
|
||||
- Suppression memory: `suppression_count` — nodes activated-but-suppressed accumulate pressure toward breakthrough
|
||||
- Temporal decay: `temporal_decay_rate`, `created_at`, `last_activated_at`, `activation_count` on EngramNode
|
||||
- Per-type activation thresholds (Safety: 0.05, Canonical: 0.15, Lesson: 0.25, Note: 0.40)
|
||||
- Temporal range query: `engram_query_range(start_ms, end_ms)`
|
||||
- Layered consciousness: `EngramLayer` struct, `layer_id` on nodes and edges, `EngramStore.layers[]`
|
||||
- Layer 0 override pass: safety layer fires last and cannot be suppressed
|
||||
|
||||
## SHA256
|
||||
bootstrap.py
|
||||
el_runtime.c
|
||||
el_runtime.h
|
||||
+1784
-1481
File diff suppressed because it is too large
Load Diff
@@ -34,12 +34,12 @@
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c runtime/el_runtime.c
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c runtime/el_runtime.c
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@@ -117,15 +117,6 @@ el_val_t el_min(el_val_t a, el_val_t b);
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── Arena scoping ────────────────────────────────────────────────────────────
|
||||
* el_arena_push() activates the string arena (if not already active) and
|
||||
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
|
||||
* mark. Used by codegen for per-function/statement scoping and by long-running
|
||||
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
|
||||
* allocations. */
|
||||
el_val_t el_arena_push(void);
|
||||
el_val_t el_arena_pop(el_val_t mark);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
@@ -151,8 +142,8 @@ el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
@@ -177,11 +168,6 @@ void http_set_handler(el_val_t name);
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Non-blocking variant of http_serve: runs the accept loop in a background
|
||||
* pthread and returns immediately so the caller can continue (used by the
|
||||
* soul daemon to run awareness_run() after starting its HTTP API). */
|
||||
void http_serve_async(el_val_t port, el_val_t handler);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
@@ -444,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);
|
||||
@@ -507,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);
|
||||
|
||||
@@ -591,7 +590,6 @@ el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
@@ -605,77 +603,26 @@ 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);
|
||||
/* Tiered paged-store entry points (ENGRAM_STORE=1). engram_store_boot opens the
|
||||
* durable store (import-once / WAL-replay) and loads it resident; checkpoint pushes
|
||||
* the resident graph's current field state (incl. learned hebb + activation-formed
|
||||
* edges) through the WAL and flushes; close checkpoints + closes. No-ops when off. */
|
||||
el_val_t engram_store_boot(el_val_t data_dir);
|
||||
el_val_t engram_store_checkpoint(void);
|
||||
el_val_t engram_store_close(void);
|
||||
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
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_get_node_by_label(el_val_t label);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_dreams_json(el_val_t since_ms);
|
||||
/* §5 geometry operators as EL builtins (read-only; seed-id CSV args). */
|
||||
el_val_t engram_geo_descriptor_json(el_val_t seeds);
|
||||
el_val_t engram_geo_overlap_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
el_val_t engram_geo_subtract_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t mode);
|
||||
el_val_t engram_geo_combine_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
el_val_t engram_geo_distance_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
/* reasoning layer (compositions over §5 operators). ANALOGY maps cleanly to the
|
||||
* flat-CSV seed ABI; the other modes take set/point/timestamp inputs deferred from
|
||||
* this ABI (see engram_reason.h / the reasoning-operators runbook). */
|
||||
el_val_t engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds);
|
||||
el_val_t engram_consolidate_permanence(el_val_t node_id);
|
||||
el_val_t engram_age_field(el_val_t delta_ms);
|
||||
el_val_t engram_age_field_catchup(void);
|
||||
el_val_t engram_chrono_persist_tick(void);
|
||||
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_act_stats_json(void);
|
||||
el_val_t engram_text_health_json(void);
|
||||
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
|
||||
/* Destructively pop up to `max` newly-formed Hebbian associations as a JSON
|
||||
* array of {from_id,to_id,weight,hebb}. The learning process (soul daemon) is
|
||||
* not the process that owns persistence (engram HTTP server); this is how a
|
||||
* self-formed association crosses that boundary. (2026-08-07 self-review.) */
|
||||
el_val_t engram_hebb_drain_json(el_val_t max);
|
||||
/* Document frequency of a term across node labels — term-specificity signal
|
||||
* for curiosity seed selection. (2026-08-03 self-review.) */
|
||||
el_val_t engram_label_df(el_val_t term);
|
||||
el_val_t engram_embed_backfill(el_val_t count);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* Working memory introspection — count, mean weight, and top-N snapshot.
|
||||
* Ported from runtime on 2026-06-30 self-review. */
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void);
|
||||
el_val_t engram_wm_top_json(el_val_t n);
|
||||
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
|
||||
el_val_t engram_load_merge(el_val_t path);
|
||||
|
||||
/* ── WAL + compaction + integrity (ENGRAM_WAL=on; design doc §§3-14,§18) ──── */
|
||||
int engram_wal_enabled(void);
|
||||
el_val_t engram_crc32(el_val_t s);
|
||||
el_val_t engram_wal_boot(el_val_t dir); /* replay + open; returns records */
|
||||
el_val_t engram_wal_open_dir(el_val_t dir);
|
||||
el_val_t engram_wal_node_put(el_val_t dir, el_val_t id);
|
||||
el_val_t engram_wal_edges_since(el_val_t dir, el_val_t start_count);
|
||||
el_val_t engram_wal_hebb_batch(el_val_t dir, el_val_t start_count);
|
||||
el_val_t engram_wal_forget(el_val_t dir, el_val_t id);
|
||||
el_val_t engram_wal_compact(el_val_t dir);
|
||||
el_val_t engram_wal_maybe_compact(el_val_t dir);
|
||||
el_val_t engram_resolve_data_dir(void); /* §18.2 fail-loud default */
|
||||
el_val_t engram_is_protected(el_val_t id); /* §18.1/18.3 derived set */
|
||||
el_val_t engram_protected_json(void);
|
||||
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)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
@@ -1,711 +0,0 @@
|
||||
/*
|
||||
* ElBridge.java — Android Java companion to el_android.c.
|
||||
*
|
||||
* All public methods are static. The C JNI layer calls these to create views,
|
||||
* set properties, and manage the widget tree. Views are identified by integer
|
||||
* slot indices matching the C-side handle values.
|
||||
*
|
||||
* Threading: every method that touches a View dispatches to the UI thread
|
||||
* using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch
|
||||
* until the UI thread completes the operation. This mirrors the AppKit
|
||||
* dispatch_sync(main_queue, ^{}) pattern in el_appkit.m.
|
||||
*
|
||||
* Callbacks: Java sets listeners on views that call back into C via:
|
||||
* nativeOnClick(int slot)
|
||||
* nativeOnChange(int slot, String text)
|
||||
* nativeOnSubmit(int slot, String text)
|
||||
* These are declared native and implemented in el_android.c.
|
||||
*
|
||||
* Usage (in your Activity.onCreate):
|
||||
* System.loadLibrary("elruntime");
|
||||
* ElBridge.init(this);
|
||||
*
|
||||
* The native library calls __native_init() which calls nativeRegisterActivity
|
||||
* via the C side; alternatively call ElBridge.init(this) directly from Java.
|
||||
*
|
||||
* Compile requirements:
|
||||
* Android minSdkVersion 21 (Lollipop) or higher.
|
||||
* No third-party dependencies — uses only android.* framework classes.
|
||||
* For image loading from arbitrary file paths, BitmapFactory is used.
|
||||
* To replace with Glide/Picasso, edit createImageView only.
|
||||
*/
|
||||
|
||||
package com.neuron.el;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class ElBridge {
|
||||
|
||||
/* ── Native callbacks (implemented in el_android.c) ─────────────────── */
|
||||
|
||||
public static native void nativeOnClick(int slot);
|
||||
public static native void nativeOnChange(int slot, String text);
|
||||
public static native void nativeOnSubmit(int slot, String text);
|
||||
public static native void nativeRegisterActivity(Activity activity);
|
||||
|
||||
/* ── State ───────────────────────────────────────────────────────────── */
|
||||
|
||||
private static final int MAX_SLOTS = 4096;
|
||||
|
||||
private static Activity sActivity;
|
||||
private static Handler sUiHandler;
|
||||
private static View[] sViews = new View[MAX_SLOTS];
|
||||
private static int sNextSlot = 1; /* slot 0 reserved / null */
|
||||
|
||||
/* ── Init ────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Must be called from the Activity before any widget operations.
|
||||
* Typically called from Activity.onCreate after System.loadLibrary.
|
||||
*/
|
||||
public static void init(Activity activity) {
|
||||
sActivity = activity;
|
||||
sUiHandler = new Handler(Looper.getMainLooper());
|
||||
nativeRegisterActivity(activity);
|
||||
}
|
||||
|
||||
/* ── Slot management ─────────────────────────────────────────────────── */
|
||||
|
||||
private static int allocSlot(View v) {
|
||||
/* Find a free slot starting from sNextSlot, wrap around. */
|
||||
for (int i = 0; i < MAX_SLOTS - 1; i++) {
|
||||
int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1;
|
||||
if (sViews[idx] == null) {
|
||||
sViews[idx] = v;
|
||||
sNextSlot = (idx % (MAX_SLOTS - 1)) + 1;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
android.util.Log.e("ElBridge", "allocSlot: slot table full");
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static View getView(int slot) {
|
||||
if (slot <= 0 || slot >= MAX_SLOTS) return null;
|
||||
return sViews[slot];
|
||||
}
|
||||
|
||||
/* ── UI-thread dispatch helper ───────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Dispatch r on the UI thread and block until it completes.
|
||||
* Safe to call from the UI thread itself (runs inline without posting).
|
||||
*/
|
||||
private static void runSync(final Runnable r) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
r.run();
|
||||
} else {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
sUiHandler.post(new Runnable() {
|
||||
@Override public void run() {
|
||||
try { r.run(); } finally { latch.countDown(); }
|
||||
}
|
||||
});
|
||||
try { latch.await(); } catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Integer slot returning runSync helper ───────────────────────────── */
|
||||
|
||||
private interface IntSupplier { int get(); }
|
||||
|
||||
private static int runSyncInt(final IntSupplier s) {
|
||||
final int[] result = { -1 };
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() { result[0] = s.get(); }
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/* ── Context accessor ────────────────────────────────────────────────── */
|
||||
|
||||
private static Context ctx() { return sActivity; }
|
||||
|
||||
/* ── View creation ───────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Create a LinearLayout.
|
||||
* @param orientation 1=VERTICAL, 0=HORIZONTAL
|
||||
* @param spacing gap between children in dp; applied as bottom/right margin
|
||||
*/
|
||||
public static int createLinearLayout(final int orientation, final int spacing) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
LinearLayout ll = new LinearLayout(ctx());
|
||||
ll.setOrientation(orientation == 1
|
||||
? LinearLayout.VERTICAL
|
||||
: LinearLayout.HORIZONTAL);
|
||||
ll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
/* Spacing is stored so addChild can apply margins. */
|
||||
ll.setTag(R_TAG_SPACING, spacing);
|
||||
return allocSlot(ll);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a FrameLayout (ZStack equivalent). */
|
||||
public static int createFrameLayout() {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
FrameLayout fl = new FrameLayout(ctx());
|
||||
fl.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(fl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a ScrollView. */
|
||||
public static int createScrollView() {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
ScrollView sv = new ScrollView(ctx());
|
||||
sv.setLayoutParams(new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
sv.setFillViewport(true);
|
||||
return allocSlot(sv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a TextView with initial text. */
|
||||
public static int createTextView(final String text) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
TextView tv = new TextView(ctx());
|
||||
tv.setText(text != null ? text : "");
|
||||
tv.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(tv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a Button with a label. */
|
||||
public static int createButton(final String label) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
Button btn = new Button(ctx());
|
||||
btn.setText(label != null ? label : "");
|
||||
btn.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(btn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EditText.
|
||||
* @param placeholder hint text
|
||||
* @param singleLine true = single-line text field; false = multi-line text area
|
||||
*/
|
||||
public static int createEditText(final String placeholder, final boolean singleLine) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
EditText et = new EditText(ctx());
|
||||
et.setHint(placeholder != null ? placeholder : "");
|
||||
if (singleLine) {
|
||||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
et.setMaxLines(1);
|
||||
et.setSingleLine(true);
|
||||
} else {
|
||||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
et.setMinLines(3);
|
||||
et.setSingleLine(false);
|
||||
et.setGravity(Gravity.TOP | Gravity.START);
|
||||
}
|
||||
et.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(et);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ImageView, loading from a file path via BitmapFactory.
|
||||
* If path is null/empty the ImageView is created with no image.
|
||||
*/
|
||||
public static int createImageView(final String path) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
ImageView iv = new ImageView(ctx());
|
||||
iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
iv.setAdjustViewBounds(true);
|
||||
if (path != null && !path.isEmpty()) {
|
||||
Bitmap bmp = BitmapFactory.decodeFile(path);
|
||||
if (bmp != null) {
|
||||
iv.setImageBitmap(bmp);
|
||||
} else {
|
||||
android.util.Log.w("ElBridge",
|
||||
"createImageView: failed to decode " + path);
|
||||
}
|
||||
}
|
||||
iv.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(iv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Window operations ───────────────────────────────────────────────── */
|
||||
|
||||
/** Set the Activity's content view to the view at slot. */
|
||||
public static void setContentView(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null && sActivity != null) {
|
||||
sActivity.setContentView(v);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set the Activity title. */
|
||||
public static void setTitle(final String title) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
if (sActivity != null) {
|
||||
sActivity.setTitle(title != null ? title : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Tree operations ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Add child view to parent view.
|
||||
* LinearLayout: child added as arranged child with spacing margin.
|
||||
* ScrollView: child replaces current document view.
|
||||
* FrameLayout / other ViewGroup: plain addView.
|
||||
*/
|
||||
public static void addChild(final int parentSlot, final int childSlot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View parent = getView(parentSlot);
|
||||
View child = getView(childSlot);
|
||||
if (parent == null || child == null) return;
|
||||
|
||||
/* Remove child from existing parent first. */
|
||||
if (child.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) child.getParent()).removeView(child);
|
||||
}
|
||||
|
||||
if (parent instanceof LinearLayout) {
|
||||
LinearLayout ll = (LinearLayout) parent;
|
||||
Object tag = ll.getTag(R_TAG_SPACING);
|
||||
int spacing = (tag instanceof Integer) ? (Integer) tag : 0;
|
||||
LinearLayout.LayoutParams lp;
|
||||
Object existingLp = child.getLayoutParams();
|
||||
if (existingLp instanceof LinearLayout.LayoutParams) {
|
||||
lp = (LinearLayout.LayoutParams) existingLp;
|
||||
} else {
|
||||
lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
/* Apply spacing as margin on the leading/top edge (after first child). */
|
||||
if (ll.getChildCount() > 0 && spacing > 0) {
|
||||
int px = dpToPx(spacing);
|
||||
if (ll.getOrientation() == LinearLayout.VERTICAL) {
|
||||
lp.topMargin = px;
|
||||
} else {
|
||||
lp.leftMargin = px;
|
||||
}
|
||||
}
|
||||
child.setLayoutParams(lp);
|
||||
ll.addView(child);
|
||||
} else if (parent instanceof ScrollView) {
|
||||
ScrollView sv = (ScrollView) parent;
|
||||
sv.removeAllViews();
|
||||
sv.addView(child);
|
||||
} else if (parent instanceof ViewGroup) {
|
||||
((ViewGroup) parent).addView(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove child from its parent. */
|
||||
public static void removeChild(final int parentSlot, final int childSlot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View parent = getView(parentSlot);
|
||||
View child = getView(childSlot);
|
||||
if (parent instanceof ViewGroup && child != null) {
|
||||
((ViewGroup) parent).removeView(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove the view from its parent and release the slot. */
|
||||
public static void destroyView(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
if (v.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) v.getParent()).removeView(v);
|
||||
}
|
||||
sViews[slot] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Property setters ────────────────────────────────────────────────── */
|
||||
|
||||
public static void setText(final int slot, final String text) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
String s = text != null ? text : "";
|
||||
if (v instanceof EditText) {
|
||||
((EditText) v).setText(s);
|
||||
} else if (v instanceof Button) {
|
||||
((Button) v).setText(s);
|
||||
} else if (v instanceof TextView) {
|
||||
((TextView) v).setText(s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String getText(final int slot) {
|
||||
final String[] result = { "" };
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v instanceof TextView) {
|
||||
CharSequence cs = ((TextView) v).getText();
|
||||
result[0] = cs != null ? cs.toString() : "";
|
||||
}
|
||||
}
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/** Set foreground text color. Components in [0,1]. */
|
||||
public static void setTextColor(final int slot, final float r, final float g,
|
||||
final float b, final float a) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v instanceof TextView) {
|
||||
((TextView) v).setTextColor(floatToArgb(r, g, b, a));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set background color using a GradientDrawable so corner radius is preserved. */
|
||||
public static void setBackgroundColor(final int slot, final float r, final float g,
|
||||
final float b, final float a) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ensureGradientBackground(v);
|
||||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||||
gd.setColor(floatToArgb(r, g, b, a));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font family and size.
|
||||
* family: "system" or null → system default; otherwise tries to load by name.
|
||||
* bold: if true uses Typeface.BOLD.
|
||||
*/
|
||||
public static void setFont(final int slot, final String family,
|
||||
final int sizeSp, final boolean bold) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof TextView)) return;
|
||||
TextView tv = (TextView) v;
|
||||
Typeface tf;
|
||||
if (family != null && !family.isEmpty()
|
||||
&& !family.equals("system")) {
|
||||
Typeface base = Typeface.create(family,
|
||||
bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
tf = (base != null) ? base
|
||||
: Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
} else {
|
||||
tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
}
|
||||
tv.setTypeface(tf);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set padding in dp. */
|
||||
public static void setPadding(final int slot, final int top, final int right,
|
||||
final int bottom, final int left) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) {
|
||||
v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set explicit width in dp. Passes MATCH_PARENT for negative values. */
|
||||
public static void setWidth(final int slot, final int widthDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.width = widthDp < 0
|
||||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||||
: dpToPx(widthDp);
|
||||
v.setLayoutParams(lp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set explicit height in dp. */
|
||||
public static void setHeight(final int slot, final int heightDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.height = heightDp < 0
|
||||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||||
: dpToPx(heightDp);
|
||||
v.setLayoutParams(lp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flex weight on a child of a LinearLayout.
|
||||
* flex > 0 → weight = flex, width/height = 0dp (expand).
|
||||
* flex == 0 → weight = 0, wrap_content (shrink to content).
|
||||
*/
|
||||
public static void setFlex(final int slot, final int flex) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp instanceof LinearLayout.LayoutParams) {
|
||||
LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp;
|
||||
if (flex > 0) {
|
||||
llp.weight = (float) flex;
|
||||
/* Determine orientation from parent to set 0dp on the right axis. */
|
||||
if (v.getParent() instanceof LinearLayout) {
|
||||
LinearLayout parent = (LinearLayout) v.getParent();
|
||||
if (parent.getOrientation() == LinearLayout.VERTICAL) {
|
||||
llp.height = 0;
|
||||
} else {
|
||||
llp.width = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
llp.weight = 0f;
|
||||
}
|
||||
v.setLayoutParams(llp);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set corner radius in dp using a GradientDrawable background. */
|
||||
public static void setCornerRadius(final int slot, final float radiusDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ensureGradientBackground(v);
|
||||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||||
gd.setCornerRadius(dpToPxF(radiusDp));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setEnabled(final int slot, final boolean enabled) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) v.setEnabled(enabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide a view.
|
||||
* @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics)
|
||||
*/
|
||||
public static void setVisibility(final int slot, final boolean visible) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Event listener registration ─────────────────────────────────────── */
|
||||
|
||||
/** Register an OnClickListener that calls back into C nativeOnClick. */
|
||||
public static void setOnClickListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
final int capturedSlot = slot;
|
||||
v.setOnClickListener(new View.OnClickListener() {
|
||||
@Override public void onClick(View view) {
|
||||
nativeOnClick(capturedSlot);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a TextWatcher on an EditText that calls back nativeOnChange
|
||||
* for every text change.
|
||||
*/
|
||||
public static void setOnChangeListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof EditText)) return;
|
||||
final int capturedSlot = slot;
|
||||
((EditText) v).addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {}
|
||||
@Override public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {}
|
||||
@Override public void afterTextChanged(Editable s) {
|
||||
nativeOnChange(capturedSlot, s != null ? s.toString() : "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an OnEditorActionListener on a single-line EditText that calls
|
||||
* nativeOnSubmit when the user presses the action/enter key.
|
||||
*/
|
||||
public static void setOnSubmitListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof EditText)) return;
|
||||
final int capturedSlot = slot;
|
||||
((EditText) v).setOnEditorActionListener(
|
||||
new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView tv, int actionId,
|
||||
android.view.KeyEvent event) {
|
||||
nativeOnSubmit(capturedSlot,
|
||||
tv.getText() != null ? tv.getText().toString() : "");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Internal helpers ─────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Tag key used to stash the spacing value on LinearLayouts so addChild
|
||||
* can apply the correct margin between children.
|
||||
* We use a stable integer resource-id-like value; because we do not have
|
||||
* a resources file here we use View.generateViewId() lazily.
|
||||
*/
|
||||
private static int sSpacingTagKey = 0;
|
||||
|
||||
private static int R_TAG_SPACING;
|
||||
static {
|
||||
R_TAG_SPACING = View.generateViewId();
|
||||
}
|
||||
|
||||
/** Convert dp to pixels using the Activity's display metrics. */
|
||||
private static int dpToPx(float dp) {
|
||||
if (sActivity == null) return (int) dp;
|
||||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
private static float dpToPxF(float dp) {
|
||||
if (sActivity == null) return dp;
|
||||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||||
return dp * density;
|
||||
}
|
||||
|
||||
/** Convert RGBA float components (0–1) to an Android ARGB int. */
|
||||
private static int floatToArgb(float r, float g, float b, float a) {
|
||||
int ai = Math.round(a * 255f);
|
||||
int ri = Math.round(r * 255f);
|
||||
int gi = Math.round(g * 255f);
|
||||
int bi = Math.round(b * 255f);
|
||||
return Color.argb(ai, ri, gi, bi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the view has a GradientDrawable background so that both color
|
||||
* and corner radius can be set independently. If the current background
|
||||
* is already a GradientDrawable it is reused; otherwise a new transparent
|
||||
* one is installed.
|
||||
*/
|
||||
private static void ensureGradientBackground(View v) {
|
||||
if (!(v.getBackground() instanceof GradientDrawable)) {
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setColor(Color.TRANSPARENT);
|
||||
v.setBackground(gd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,554 +0,0 @@
|
||||
# Platform Bridge Specification — el-native
|
||||
|
||||
This document is the authoritative reference for anyone implementing a new platform bridge for the el-native widget system. Read it top to bottom before writing a single line of code.
|
||||
|
||||
---
|
||||
|
||||
## What a Platform Bridge Is
|
||||
|
||||
The el compiler (`elc`) emits C code. That C code calls `__`-prefixed functions for everything OS-related: printing, file I/O, threading, and — when building a native UI app — widget creation and event handling. These `__` functions are the *OS boundary*.
|
||||
|
||||
For native UI, the bridge is the translation layer between that fixed C API and whatever platform toolkit you are targeting. The bridge owns:
|
||||
|
||||
1. A **slot table** of up to 4096 widget objects, indexed by `int64_t` handle.
|
||||
2. Implementations of all 33 `__*` widget functions declared in `el_native_target.h`.
|
||||
3. Callback dispatch from platform events back into El function symbols resolved via `dlsym` (or a platform equivalent).
|
||||
|
||||
The thin wrappers in `el_seed.c` (`#ifdef EL_TARGET_*` blocks) marshal between `el_val_t` and native C types, then call through to the bridge. The bridge itself never touches `el_val_t` — it works only with plain C types (`int64_t`, `const char*`, `int`, `float`).
|
||||
|
||||
**Existing bridges:**
|
||||
|
||||
| File | Platform | Toolkit | Language |
|
||||
|------|----------|---------|----------|
|
||||
| `el_appkit.m` | macOS | AppKit | ObjC (MRC) |
|
||||
| `el_gtk4.c` | Linux | GTK4 | C |
|
||||
| `el_win32.c` | Windows | Win32/ComCtl | C |
|
||||
| `el_uikit.m` | iOS | UIKit | ObjC (MRC) |
|
||||
| `el_android.c` + `ElBridge.java` | Android | View/JNI | C + Java |
|
||||
| `el_sdl2.c` | Embedded Linux / Pi | SDL2 | C |
|
||||
| `el_lvgl.c` | Microcontrollers | LVGL | C |
|
||||
|
||||
---
|
||||
|
||||
## The Slot System
|
||||
|
||||
Every widget — window, button, label, container, image — is stored in a static array:
|
||||
|
||||
```c
|
||||
#define EL_<PLATFORM>_MAX_WIDGETS 4096
|
||||
|
||||
typedef struct {
|
||||
ElWidgetKind kind;
|
||||
/* platform-specific object reference (pointer, handle, ID...) */
|
||||
/* callback names */
|
||||
char* cb_click;
|
||||
char* cb_change;
|
||||
} ElWidget;
|
||||
|
||||
static ElWidget _el_widgets[EL_<PLATFORM>_MAX_WIDGETS];
|
||||
```
|
||||
|
||||
Rules:
|
||||
- **Slot 0 is never valid.** Scan starts at index 1. This ensures 0 is never a valid handle.
|
||||
- **Handle = slot index.** An `int64_t` value returned to El code is a direct index into `_el_widgets[]`.
|
||||
- **-1 = invalid.** All create functions return -1 on failure (table full, platform API error).
|
||||
- **Slot is FREE until allocated, FREE again after destroy.** Use an `ElWidgetKind` enum where `0 = EL_WIDGET_FREE` to track liveness.
|
||||
- **4096 slots is the system-wide maximum.** This is intentional and sufficient for any realistic UI. Do not increase it without a compelling reason.
|
||||
|
||||
### Slot allocation and release pattern
|
||||
|
||||
```c
|
||||
static int64_t el_widget_alloc(ElWidgetKind kind, /* platform object ref */) {
|
||||
for (int i = 1; i < EL_<PLATFORM>_MAX_WIDGETS; i++) {
|
||||
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
||||
_el_widgets[i].kind = kind;
|
||||
/* store platform object ref — retain/addref if needed */
|
||||
_el_widgets[i].cb_click = NULL;
|
||||
_el_widgets[i].cb_change = NULL;
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
return -1; /* table full */
|
||||
}
|
||||
|
||||
static ElWidget* el_widget_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_<PLATFORM>_MAX_WIDGETS) return NULL;
|
||||
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
||||
return &_el_widgets[handle];
|
||||
}
|
||||
|
||||
static void el_widget_free(int64_t handle) {
|
||||
ElWidget* w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
/* release platform object ref */
|
||||
w->kind = EL_WIDGET_FREE;
|
||||
free(w->cb_click); w->cb_click = NULL;
|
||||
free(w->cb_change); w->cb_change = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
**NULL handle guard:** `el_widget_get` must return `NULL` for any handle that is 0, negative, out of range, or points to a `FREE` slot. Every `__` function that takes a handle must null-check the result of `el_widget_get` before doing anything. Failing to do so causes crashes or corruption when El code passes an uninitialized handle.
|
||||
|
||||
---
|
||||
|
||||
## The 33 Required Functions
|
||||
|
||||
Every bridge must implement all 33 functions listed below. They are grouped by category. The signatures shown are from `el_native_target.h` and from `el_seed.c`'s wrapper layer; the bridge itself uses plain C types (the wrappers do the `el_val_t` ↔ C-type conversion).
|
||||
|
||||
### Internal C signatures (what the bridge implements)
|
||||
|
||||
These are what your `.c` / `.m` file defines. The `el_seed.c` wrappers call these.
|
||||
|
||||
#### Lifecycle (2 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_init(void);
|
||||
```
|
||||
**Purpose:** Initialize the platform UI toolkit. Must be idempotent (safe to call more than once). Called once from `__native_init` before any widget creation.
|
||||
**Edge cases:** On platforms where the toolkit must be initialized before a display connection is established (X11, Wayland), this is where that happens. On Android, this is a no-op because ElBridge.java calls init from Java.
|
||||
|
||||
```c
|
||||
void el_<platform>_run_loop(void);
|
||||
```
|
||||
**Purpose:** Start the platform event loop. On most platforms this **never returns**. Exceptions: Android (the loop is Java-managed — this must be a no-op) and headless test builds.
|
||||
**Edge cases:** Must be called from the main thread. On iOS/UIKit, calls `UIApplicationMain` which never returns; El code must set `el_main_entry_fn` before calling this.
|
||||
|
||||
#### Window (3 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
|
||||
```
|
||||
**Purpose:** Create a top-level window. `w`/`h` = initial size in logical pixels. `mw`/`mh` = minimum size (0 = no minimum).
|
||||
**Returns:** Slot handle, or -1 on failure.
|
||||
**Edge cases:** NULL or empty `title` must be handled gracefully (use `""`). On mobile (iOS, Android), the concept of a "window" maps to the root view controller / activity root view — create that here.
|
||||
|
||||
```c
|
||||
void el_<platform>_window_show(int64_t handle);
|
||||
```
|
||||
**Purpose:** Make the window visible. On some platforms windows are hidden at creation; this makes them appear.
|
||||
**Edge cases:** NULL handle → return silently. Calling on an already-visible window is a no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_window_set_title(int64_t handle, const char* title);
|
||||
```
|
||||
**Purpose:** Update the window's title bar text at runtime.
|
||||
**Edge cases:** NULL handle or NULL title → no-op.
|
||||
|
||||
#### Layout containers (4 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_vstack_create(int spacing);
|
||||
int64_t el_<platform>_hstack_create(int spacing);
|
||||
```
|
||||
**Purpose:** Create a vertical/horizontal linear container. `spacing` = gap between children in logical pixels.
|
||||
**Returns:** Slot handle, or -1.
|
||||
**Edge cases:** spacing = 0 is valid and common.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_zstack_create(void);
|
||||
```
|
||||
**Purpose:** Create a z-axis layered container (children overlap, no stacking direction). Used for overlays.
|
||||
**Returns:** Slot handle, or -1.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_scroll_create(void);
|
||||
```
|
||||
**Purpose:** Create a scrollable container. Scrolls vertically by default. Only the first child added is the scrollable content.
|
||||
**Returns:** Slot handle, or -1.
|
||||
|
||||
#### Leaf widgets (5 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_label_create(const char* text);
|
||||
```
|
||||
**Purpose:** Create a non-editable text label.
|
||||
**Edge cases:** NULL/empty text → label with empty string, not a crash.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_button_create(const char* label);
|
||||
```
|
||||
**Purpose:** Create a clickable button. The `label` is the button's visible text.
|
||||
**Edge cases:** The button must wire up an action target at creation time so that click callbacks registered later via `el_<platform>_widget_on_click` will fire. On platforms with target-action (AppKit, UIKit), allocate the delegate object here.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_text_field_create(const char* placeholder);
|
||||
```
|
||||
**Purpose:** Create a single-line text input. `placeholder` is the hint text shown when empty.
|
||||
**Edge cases:** NULL placeholder → no hint text displayed.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_text_area_create(const char* placeholder);
|
||||
```
|
||||
**Purpose:** Create a multi-line text input (scrollable).
|
||||
**Edge cases:** Same as text_field_create. On AppKit, this wraps NSTextView inside NSScrollView — the slot's `obj` points to the scroll view, not the text view.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_image_create(const char* path_or_name);
|
||||
```
|
||||
**Purpose:** Create an image widget. `path_or_name` can be a filesystem path or a platform resource name. Try path first, fall back to named resource.
|
||||
**Edge cases:** Non-existent path → create an empty image widget (do not crash). NULL → same.
|
||||
|
||||
#### Widget properties (12 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_text(int64_t handle, const char* text);
|
||||
```
|
||||
**Purpose:** Update the text content of a label, button, text field, text area, or window title. Must dispatch on `kind` to use the correct API.
|
||||
**Edge cases:** NULL handle → no-op. NULL text → treat as `""`.
|
||||
|
||||
```c
|
||||
const char* el_<platform>_widget_get_text(int64_t handle);
|
||||
```
|
||||
**Purpose:** Return the current text of a widget. Returns a `const char*` that the `el_seed.c` wrapper wraps into an `el_val_t` string.
|
||||
**Edge cases:** NULL handle → return `""` (never NULL). The caller in `el_seed.c` handles the `strdup` lifetime issue — see the AppKit reference implementation notes.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_color(int64_t h, float r, float g, float b, float a);
|
||||
void el_<platform>_widget_set_bg_color(int64_t h, float r, float g, float b, float a);
|
||||
```
|
||||
**Purpose:** Set foreground (text) color and background color respectively. All channels are normalized floats [0.0, 1.0].
|
||||
**Edge cases:** For containers, `set_color` may be a no-op (no text); `set_bg_color` should set the layer/surface background. On platforms without alpha compositing, clamp alpha to 0 or 1.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_font(int64_t h, const char* family, int size, int bold);
|
||||
```
|
||||
**Purpose:** Set the font on a text-bearing widget. `family` = font family name or `"system"` for the platform default. `size` = point size. `bold` = 0 or 1.
|
||||
**Edge cases:** If `family` is not found, fall back to the system font. Non-text widgets (containers, images) → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_padding(int64_t h, int top, int right, int bottom, int left);
|
||||
```
|
||||
**Purpose:** Set internal padding/insets for a container or text area.
|
||||
**Edge cases:** On platforms where padding is per-view (not per-container), map to the nearest equivalent (margin, insets, text container inset). For leaf widgets other than text areas, this may be a partial no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_width(int64_t h, int width);
|
||||
void el_<platform>_widget_set_height(int64_t h, int height);
|
||||
```
|
||||
**Purpose:** Apply a fixed-size constraint. `width`/`height` in logical pixels.
|
||||
**Edge cases:** On platforms with Auto Layout or constraint systems, add a fixed-size constraint. Do not apply to windows (size is set at creation). Calling multiple times should override the previous constraint, not add another.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_flex(int64_t h, int flex);
|
||||
```
|
||||
**Purpose:** Set the flex/expansion factor. `flex > 0` → the widget expands to fill available space. `flex == 0` → hugs content size.
|
||||
**Edge cases:** Maps to content-hugging priority (AppKit), `GtkWidget::hexpand`/`vexpand` (GTK4), or layout weight (Android). For windows → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_corner_radius(int64_t h, int radius);
|
||||
```
|
||||
**Purpose:** Apply rounded corners to the widget's visual layer.
|
||||
**Edge cases:** Requires backing layer / GPU surface. On platforms without layer compositing (Win32 without DX), this may be a no-op or require manual painting. Radius in logical pixels.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_disabled(int64_t h, int disabled);
|
||||
```
|
||||
**Purpose:** Enable or disable user interaction. `disabled = 1` → greyed out, non-interactive.
|
||||
**Edge cases:** Only meaningful for interactive widgets (button, text field). For containers/labels → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_hidden(int64_t h, int hidden);
|
||||
```
|
||||
**Purpose:** Show or hide the widget. `hidden = 1` → invisible but still in layout.
|
||||
**Edge cases:** For windows, map to `orderOut`/`hide` or equivalent. For views, use `setHidden`/`gtk_widget_set_visible` or equivalent.
|
||||
|
||||
#### Tree management (3 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_add_child(int64_t parent, int64_t child);
|
||||
```
|
||||
**Purpose:** Attach a child widget to a parent container. Dispatch on parent kind:
|
||||
- Window → add to root content view/container
|
||||
- VStack/HStack → add as arranged/linear child
|
||||
- ZStack → add as overlapping subview
|
||||
- Scroll → set as document/content view (first child only)
|
||||
- Other → add as plain subview
|
||||
|
||||
**Edge cases:** NULL parent or child handle → no-op. Attempting to add a window as a child → no-op. Adding the same child twice is platform-defined behavior (tolerate it).
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_remove_child(int64_t parent, int64_t child);
|
||||
```
|
||||
**Purpose:** Detach child from its parent. The child slot remains allocated; the widget is not destroyed.
|
||||
**Edge cases:** NULL handles → no-op. Child not currently attached → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_destroy(int64_t handle);
|
||||
```
|
||||
**Purpose:** Destroy a widget: remove from superview/parent, release the platform object, release callback strings, mark slot as FREE.
|
||||
**Edge cases:** For windows, close the window. Free any delegate/target objects stored in side tables. After destroy, the handle is invalid — El code must not use it again (this is the caller's responsibility, not enforced here).
|
||||
|
||||
#### Event registration (3 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_on_click(int64_t h, const char* fn_name);
|
||||
void el_<platform>_widget_on_change(int64_t h, const char* fn_name);
|
||||
void el_<platform>_widget_on_submit(int64_t h, const char* fn_name);
|
||||
```
|
||||
**Purpose:** Register an El callback function by symbol name.
|
||||
- `on_click` → button press
|
||||
- `on_change` → text field value change (keystroke-level)
|
||||
- `on_submit` → text field Enter key (text field only; stored in `cb_click` slot in AppKit)
|
||||
|
||||
The implementation stores `strdup(fn_name)` in the widget's `cb_click` or `cb_change` field. The platform event handler calls `el_<platform>_invoke_cb` (see callback ABI section).
|
||||
**Edge cases:** NULL or empty `fn_name` → clear the callback (`free` + set NULL). Calling on a non-interactive widget (label, image) → no-op or store silently (harmless).
|
||||
|
||||
#### Manifest reader (1 function)
|
||||
|
||||
```c
|
||||
/* Note: __manifest_read is implemented in el_seed.c, not in the bridge. */
|
||||
/* Bridges do NOT need to implement this. */
|
||||
```
|
||||
|
||||
`__manifest_read` is handled entirely in the platform-independent section of `el_seed.c`. It reads a JSON/EL manifest file from the path in the `EL_MANIFEST` environment variable. Bridge authors do not need to implement this.
|
||||
|
||||
---
|
||||
|
||||
## The Callback ABI
|
||||
|
||||
When a platform event fires (button clicked, text changed), the bridge must call back into the El runtime. The mechanism:
|
||||
|
||||
```c
|
||||
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
||||
|
||||
static void el_<platform>_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
|
||||
if (!fn_name || !*fn_name) return;
|
||||
void* sym = dlsym(RTLD_DEFAULT, fn_name);
|
||||
if (!sym) return;
|
||||
ElCb2 fn = (ElCb2)sym;
|
||||
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
||||
}
|
||||
```
|
||||
|
||||
The El callback signature (in El source):
|
||||
```
|
||||
fn handler(handle: Int, data: String) -> Void
|
||||
```
|
||||
|
||||
Which compiles to:
|
||||
```c
|
||||
void handler(int64_t handle, int64_t data)
|
||||
```
|
||||
|
||||
Where `data` is a `const char*` cast to `int64_t` (the el `String` representation). For click events, `data` is `""`. For change/submit events, `data` is the current widget text.
|
||||
|
||||
**On platforms without `dlsym`** (Windows, some embedded systems): use `GetProcAddress(GetModuleHandle(NULL), fn_name)` on Windows, or maintain a manual symbol registration table for embedded targets where dynamic linking is unavailable.
|
||||
|
||||
**Thread safety for callbacks:** Callbacks fired from a background thread must be marshalled to the UI thread before calling into El code. El code may call `__widget_set_text` or other UI functions synchronously from within the callback — those must run on the UI thread.
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety Contract
|
||||
|
||||
**All platform UI operations must execute on the main/UI thread.** This is a hard requirement on every platform (AppKit, UIKit, GTK4, Win32, Android View, SDL2 main thread rule).
|
||||
|
||||
The reference pattern (AppKit):
|
||||
```c
|
||||
static void el_appkit_sync_main(void (^block)(void)) {
|
||||
if ([NSThread isMainThread]) {
|
||||
block();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For other platforms:
|
||||
- **GTK4:** `g_main_context_invoke` or `g_idle_add` + semaphore for synchronous dispatch
|
||||
- **Win32:** `SendMessage(hwnd, WM_APP, ...)` or `PostMessage` + wait
|
||||
- **Android:** `Activity.runOnUiThread`
|
||||
- **SDL2:** All ops must be called from the thread that initialized SDL (the main thread)
|
||||
- **LVGL:** `lv_lock()` / `lv_unlock()` for thread-safe access
|
||||
|
||||
El program flow is: `main()` → build UI (on main thread) → `__native_run_loop()`. Because UI is built before the run loop starts, most widget creation calls are already on the main thread. The sync dispatch wrapper exists to handle callbacks that arrive on worker threads (e.g., network callbacks that update UI).
|
||||
|
||||
---
|
||||
|
||||
## Integration Pattern
|
||||
|
||||
### In `el_native_target.h`
|
||||
|
||||
Add an `#ifdef EL_TARGET_<PLATFORM>` block declaring all 33 `__*` functions with their `el_val_t` signatures (identical to the existing blocks for MACOS, LINUX, etc.):
|
||||
|
||||
```c
|
||||
#ifdef EL_TARGET_<PLATFORM>
|
||||
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_<PLATFORM> */
|
||||
```
|
||||
|
||||
### In `el_seed.c`
|
||||
|
||||
Add an `#ifdef EL_TARGET_<PLATFORM>` block containing:
|
||||
1. `extern` declarations of all `el_<platform>_*` functions (your bridge's C API)
|
||||
2. Thin wrapper functions that marshal `el_val_t` ↔ C types and call through
|
||||
|
||||
The wrapper pattern (copy from the `EL_TARGET_MACOS` block and substitute the platform name):
|
||||
|
||||
```c
|
||||
#ifdef EL_TARGET_<PLATFORM>
|
||||
|
||||
/* Forward declarations — implemented in el_<platform>.c */
|
||||
extern void el_<platform>_init(void);
|
||||
extern void el_<platform>_run_loop(void);
|
||||
extern int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
|
||||
/* ... all others ... */
|
||||
|
||||
/* Wrappers */
|
||||
void __native_init(void) { el_<platform>_init(); }
|
||||
void __native_run_loop(void) { el_<platform>_run_loop(); }
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height) {
|
||||
return (el_val_t)el_<platform>_window_create(
|
||||
EL_CSTR(title),
|
||||
(int)(int64_t)width, (int)(int64_t)height,
|
||||
(int)(int64_t)min_width, (int)(int64_t)min_height);
|
||||
}
|
||||
|
||||
void __window_show(el_val_t h) { el_<platform>_window_show((int64_t)h); }
|
||||
void __window_set_title(el_val_t h, el_val_t t) { el_<platform>_window_set_title((int64_t)h, EL_CSTR(t)); }
|
||||
|
||||
/* ... continue for all 33 functions ... */
|
||||
|
||||
#endif /* EL_TARGET_<PLATFORM> */
|
||||
```
|
||||
|
||||
Key marshalling rules:
|
||||
- `el_val_t` → `int64_t` handle: `(int64_t)h`
|
||||
- `el_val_t` → `int`: `(int)(int64_t)value`
|
||||
- `el_val_t` → `const char*`: `EL_CSTR(value)`
|
||||
- `el_val_t` → `float`: `(float)el_to_float(value)` (for color channels)
|
||||
- `int64_t` handle → `el_val_t`: `(el_val_t)handle`
|
||||
- `const char*` → `el_val_t`: `EL_STR(str)` — but read the get_text note below
|
||||
|
||||
**`__widget_get_text` note:** The bridge returns `const char*`. The `el_seed.c` wrapper wraps it with `EL_STR(s)`. The returned pointer must remain valid until the El program is done with it. The AppKit implementation returns a `strdup`'d string — the caller (seed wrapper) stores it without tracking it in the arena. This is a known lifetime edge; be consistent with the platform's existing pattern.
|
||||
|
||||
---
|
||||
|
||||
## How el Strings Work
|
||||
|
||||
Inside El compiled C code, strings are `el_val_t` values where the value is the `uintptr_t` cast of a `const char*`:
|
||||
|
||||
```c
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
```
|
||||
|
||||
To construct a string result in `el_seed.c`:
|
||||
```c
|
||||
static char* s = strdup("hello");
|
||||
return EL_STR(s);
|
||||
```
|
||||
|
||||
To read a string argument passed from El:
|
||||
```c
|
||||
const char* text = EL_CSTR(some_el_val_t_argument);
|
||||
```
|
||||
|
||||
The `EL_STR` / `EL_CSTR` macros are defined in `el_seed.h` and are available in `el_seed.c`. Bridge files (`el_<platform>.c`) do not use these macros — they only deal in `const char*` at their API boundary.
|
||||
|
||||
---
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
### Duplicate symbols with `el_runtime.c`
|
||||
|
||||
`el_runtime.c` defines many of the same `__`-prefixed symbols as `el_seed.c`. When linking both (required for the native-hello example), the linker will reject duplicate definitions. The build system uses `nmedit` (macOS) to hide the `el_runtime.c` copies of symbols that `el_seed.c` already defines, keeping `el_seed.c` canonical.
|
||||
|
||||
If you are writing a build script for a new platform and see duplicate symbol link errors involving `__println`, `__print`, `__str_len`, etc., apply the same nmedit / `objcopy --weaken-symbol` / `strip --strip-symbol` trick from the macOS build to your platform's object file handling.
|
||||
|
||||
### The `nmedit` trick on macOS
|
||||
|
||||
```bash
|
||||
# Build a keep-list: symbols defined in el_seed.o but also in el_runtime.o
|
||||
nm el_seed.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .seed_T.txt
|
||||
nm el_runtime.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .rt_T.txt
|
||||
# Keep only symbols unique to el_runtime.o
|
||||
comm -23 .rt_T.txt .seed_T.txt > .rt_keep.txt
|
||||
nmedit -s .rt_keep.txt el_runtime.o
|
||||
```
|
||||
|
||||
On Linux, use `objcopy` with `--weaken-symbol` for each duplicate, or link `el_seed.o` before `el_runtime.o` and use `--allow-multiple-definition` if your use case permits it.
|
||||
|
||||
### ObjC bridges must use MRC, not ARC
|
||||
|
||||
Bridges that use Objective-C (AppKit, UIKit) **must** compile without ARC (`-fno-objc-arc`). The reason: the widget table stores `id` values in a plain C struct. ARC cannot insert retain/release through C struct boundaries, and will reject explicit `[obj retain]` / `[obj release]` calls in ARC mode. Use:
|
||||
|
||||
```bash
|
||||
clang -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m
|
||||
```
|
||||
|
||||
### Widget table struct — don't put `id` fields in C structs under ARC
|
||||
|
||||
If you ever add ARC-managed object fields to the `ElWidget` struct, you will get a compile error. Keep the struct C-only (pointers as `void*` if you must, cast when using) or compile as MRC.
|
||||
|
||||
### The `el_seed.c` `__manifest_read` is platform-independent
|
||||
|
||||
`__manifest_read` is already implemented in `el_seed.c` (in the section compiled unconditionally). You do not implement it in your bridge. You do need to declare it in the `el_native_target.h` block for your platform (matching the other platforms), but `el_seed.c` already has the implementation.
|
||||
|
||||
---
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
Before declaring your bridge ready for integration:
|
||||
|
||||
- [ ] All 33 `__*` functions implemented (including `__manifest_read` declaration, implementation is in seed)
|
||||
- [ ] Slot table with 4096 entries, scan starting at index 1
|
||||
- [ ] `el_widget_get` returns NULL for handle 0, negative, out-of-range, and FREE slots
|
||||
- [ ] All functions null-check `el_widget_get` result before use
|
||||
- [ ] `el_<platform>_init` is idempotent
|
||||
- [ ] `el_<platform>_run_loop` either never returns or is documented as a no-op (Android)
|
||||
- [ ] Callback dispatch via `dlsym` (or platform equivalent) implemented
|
||||
- [ ] All UI operations dispatched to the main/UI thread
|
||||
- [ ] `el_native_target.h` updated with `#ifdef EL_TARGET_<PLATFORM>` block
|
||||
- [ ] `el_seed.c` updated with `#ifdef EL_TARGET_<PLATFORM>` extern + wrapper block
|
||||
- [ ] Bridge compiles cleanly with no warnings: `cc -DEL_TARGET_<PLATFORM> -Wall -Wextra -c el_<platform>.c`
|
||||
- [ ] Bridge links cleanly with `el_seed.o` and `el_runtime.o` (duplicate symbol check)
|
||||
- [ ] `detect-platforms` script updated with detection logic for the new platform
|
||||
- [ ] Basic smoke test: create window → add label → show window → run loop
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# detect-platforms — probe available platform bridge dependencies
|
||||
#
|
||||
# Reports which el-native platform bridges can be built on this machine,
|
||||
# with install instructions for anything that is missing.
|
||||
#
|
||||
# Usage: ./detect-platforms
|
||||
# ./build.sh platforms (from native-hello)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
PASS="[+]"
|
||||
FAIL="[ ]"
|
||||
|
||||
_ok() { printf " ${PASS} %-22s %s\n" "$1" "$2"; }
|
||||
_miss() { printf " ${FAIL} %-22s %s\n" "$1" "$2"; }
|
||||
|
||||
# ── Header ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "==> el-native platform detection"
|
||||
echo ""
|
||||
echo " Checking build dependencies for each platform bridge..."
|
||||
echo ""
|
||||
|
||||
AVAILABLE=0
|
||||
MISSING=0
|
||||
|
||||
# ── macOS / AppKit ────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if xcrun --find clang &>/dev/null && xcrun --find xcodebuild &>/dev/null 2>/dev/null || \
|
||||
xcrun --find cc &>/dev/null; then
|
||||
CLT_INFO="Xcode CLT $(xcode-select -p 2>/dev/null | sed 's|/Developer||' || echo '')"
|
||||
_ok "macOS/AppKit" "-DEL_TARGET_MACOS ${CLT_INFO}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (Xcode CLT missing — xcode-select --install)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
else
|
||||
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (not on macOS)"
|
||||
fi
|
||||
|
||||
# ── iOS / UIKit ───────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if xcrun --sdk iphoneos --show-sdk-path &>/dev/null 2>&1; then
|
||||
SDK_VER=$(xcrun --sdk iphoneos --show-sdk-version 2>/dev/null || echo "")
|
||||
_ok "iOS/UIKit" "-DEL_TARGET_IOS SDK ${SDK_VER} (requires Xcode.app)"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "iOS/UIKit" "-DEL_TARGET_IOS (iOS SDK not found — install Xcode.app)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
else
|
||||
_miss "iOS/UIKit" "-DEL_TARGET_IOS (not on macOS — requires Xcode)"
|
||||
fi
|
||||
|
||||
# ── Linux / GTK4 ──────────────────────────────────────────────────────────────
|
||||
|
||||
if pkg-config --exists gtk4 2>/dev/null; then
|
||||
GTK_VER=$(pkg-config --modversion gtk4 2>/dev/null)
|
||||
_ok "Linux/GTK4" "-DEL_TARGET_LINUX gtk4 ${GTK_VER}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Linux/GTK4" "-DEL_TARGET_LINUX (gtk4 not found)"
|
||||
echo " Install: apt install libgtk-4-dev"
|
||||
echo " or: dnf install gtk4-devel"
|
||||
echo " or: brew install gtk4"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── SDL2 / Embedded Linux / Pi ────────────────────────────────────────────────
|
||||
|
||||
SDL2_OK=0
|
||||
if pkg-config --exists sdl2 2>/dev/null; then
|
||||
SDL2_VER=$(pkg-config --modversion sdl2 2>/dev/null)
|
||||
# Also check for SDL2_ttf (needed for text rendering)
|
||||
if pkg-config --exists SDL2_ttf 2>/dev/null; then
|
||||
TTF_VER=$(pkg-config --modversion SDL2_ttf 2>/dev/null)
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER}, SDL2_ttf ${TTF_VER}"
|
||||
else
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (SDL2_ttf missing — needed for text)"
|
||||
echo " Install: apt install libsdl2-ttf-dev"
|
||||
fi
|
||||
SDL2_OK=1
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
elif command -v sdl2-config &>/dev/null; then
|
||||
SDL2_VER=$(sdl2-config --version 2>/dev/null || echo "")
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (via sdl2-config)"
|
||||
SDL2_OK=1
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
fi
|
||||
|
||||
if [[ $SDL2_OK -eq 0 ]]; then
|
||||
_miss "SDL2/Embedded" "-DEL_TARGET_SDL2 (sdl2 not found)"
|
||||
echo " Install: apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev"
|
||||
echo " or: brew install sdl2 sdl2_ttf sdl2_image"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── LVGL / Microcontrollers ───────────────────────────────────────────────────
|
||||
|
||||
LVGL_OK=0
|
||||
LVGL_WHERE=""
|
||||
|
||||
if [ -f "${SCRIPT_DIR}/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="./lvgl/lvgl.h"
|
||||
LVGL_OK=1
|
||||
elif [ -f "${SCRIPT_DIR}/../lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="adjacent lvgl/"
|
||||
LVGL_OK=1
|
||||
elif [ -f "/usr/include/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="/usr/include/lvgl"
|
||||
LVGL_OK=1
|
||||
elif [ -f "/usr/local/include/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="/usr/local/include/lvgl"
|
||||
LVGL_OK=1
|
||||
elif pkg-config --exists lvgl 2>/dev/null; then
|
||||
LVGL_WHERE="pkg-config ($(pkg-config --modversion lvgl 2>/dev/null))"
|
||||
LVGL_OK=1
|
||||
fi
|
||||
|
||||
if [[ $LVGL_OK -eq 1 ]]; then
|
||||
_ok "LVGL/MCU" "-DEL_TARGET_LVGL ${LVGL_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "LVGL/MCU" "-DEL_TARGET_LVGL (lvgl.h not found)"
|
||||
echo " Install: git clone https://github.com/lvgl/lvgl"
|
||||
echo " (place lvgl/ next to runtime/)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Android / JNI ─────────────────────────────────────────────────────────────
|
||||
|
||||
ANDROID_OK=0
|
||||
ANDROID_WHERE=""
|
||||
|
||||
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "${ANDROID_NDK_HOME}" ]; then
|
||||
NDK_VER=""
|
||||
if [ -f "${ANDROID_NDK_HOME}/source.properties" ]; then
|
||||
NDK_VER=$(grep "Pkg.Revision" "${ANDROID_NDK_HOME}/source.properties" \
|
||||
2>/dev/null | cut -d= -f2 | tr -d ' ' || echo "")
|
||||
fi
|
||||
ANDROID_WHERE="NDK ${NDK_VER:-(version unknown)} at \$ANDROID_NDK_HOME"
|
||||
ANDROID_OK=1
|
||||
elif command -v ndk-build &>/dev/null; then
|
||||
ANDROID_WHERE="ndk-build in PATH"
|
||||
ANDROID_OK=1
|
||||
elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/ndk" ]; then
|
||||
ANDROID_WHERE="NDK via \$ANDROID_HOME/ndk"
|
||||
ANDROID_OK=1
|
||||
fi
|
||||
|
||||
if [[ $ANDROID_OK -eq 1 ]]; then
|
||||
_ok "Android/JNI" "-DEL_TARGET_ANDROID ${ANDROID_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Android/JNI" "-DEL_TARGET_ANDROID (ANDROID_NDK_HOME not set)"
|
||||
echo " Install: https://developer.android.com/studio/releases/ndk"
|
||||
echo " Then: export ANDROID_NDK_HOME=/path/to/ndk"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Windows / Win32 (cross or native) ─────────────────────────────────────────
|
||||
|
||||
WIN32_OK=0
|
||||
WIN32_WHERE=""
|
||||
|
||||
if [[ "$(uname)" == MINGW* ]] || [[ "$(uname)" == CYGWIN* ]] || \
|
||||
[[ "$(uname)" == MSYS* ]]; then
|
||||
WIN32_WHERE="native Windows ($(uname))"
|
||||
WIN32_OK=1
|
||||
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||
MINGW_VER=$(x86_64-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "")
|
||||
WIN32_WHERE="mingw cross-compiler — ${MINGW_VER}"
|
||||
WIN32_OK=1
|
||||
elif command -v i686-w64-mingw32-gcc &>/dev/null; then
|
||||
WIN32_WHERE="mingw 32-bit cross-compiler"
|
||||
WIN32_OK=1
|
||||
fi
|
||||
|
||||
if [[ $WIN32_OK -eq 1 ]]; then
|
||||
_ok "Windows/Win32" "-DEL_TARGET_WIN32 ${WIN32_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Windows/Win32" "-DEL_TARGET_WIN32 (mingw cross-compiler not found)"
|
||||
echo " Install: brew install mingw-w64"
|
||||
echo " or: apt install gcc-mingw-w64"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo " ${AVAILABLE} platform(s) available, ${MISSING} unavailable on this machine."
|
||||
echo ""
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/new-platform" ]]; then
|
||||
echo " Scaffold a new bridge: ${SCRIPT_DIR}/new-platform <name>"
|
||||
fi
|
||||
echo " Bridge contract: ${SCRIPT_DIR}/PLATFORM_BRIDGE_SPEC.md"
|
||||
echo ""
|
||||
@@ -1,949 +0,0 @@
|
||||
/*
|
||||
* el_android.c — Android JNI backend for the el native widget system.
|
||||
*
|
||||
* This file implements the Android widget layer that el_seed.c calls through
|
||||
* to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart
|
||||
* to el_appkit.m and presents the same C API surface.
|
||||
*
|
||||
* Architecture:
|
||||
* el program (el code)
|
||||
* → __widget_* C builtins in el_seed.c
|
||||
* → el_android_* C-callable functions declared here
|
||||
* → ElBridge static methods in Java via JNI
|
||||
* → android.view.View subclasses on the UI thread
|
||||
*
|
||||
* Widget handles: every widget (window root, view, control) is assigned an
|
||||
* int64_t slot index into view_slots[]. The el program holds these as opaque
|
||||
* Int values. Slot 0 is never valid (null handle = -1 convention).
|
||||
*
|
||||
* Threading: Android requires all UI operations to run on the main (UI) thread.
|
||||
* Every JNI call that mutates a View is dispatched through
|
||||
* Activity.runOnUiThread(Runnable) if the current thread is not the UI thread.
|
||||
* el_android_run_loop is a no-op — Android lifecycle is driven by the Activity.
|
||||
*
|
||||
* Callback mechanism: when a widget fires an event Java calls
|
||||
* nativeOnClick / nativeOnChange / nativeOnSubmit
|
||||
* The C side looks up the registered El function name for that slot, then:
|
||||
* dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
|
||||
* This matches the __thread_create pattern in el_seed.c exactly.
|
||||
*
|
||||
* Compile / link (as part of libelruntime.so):
|
||||
* Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID.
|
||||
* Link flags: -landroid -llog -ldl
|
||||
*
|
||||
* Java companion: ElBridge.java in the same directory must be compiled into
|
||||
* the Android application's APK (package com.neuron.el).
|
||||
*/
|
||||
|
||||
#ifdef EL_TARGET_ANDROID
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dlfcn.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Logging ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_TAG "ElAndroid"
|
||||
#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__)
|
||||
#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__)
|
||||
#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__)
|
||||
|
||||
/* ── JNI global state ────────────────────────────────────────────────────── */
|
||||
|
||||
static JavaVM *g_jvm = NULL;
|
||||
static jobject g_activity = NULL; /* global ref to Activity */
|
||||
static jclass g_bridge_class = NULL; /* global ref to ElBridge class */
|
||||
|
||||
/* Cached method IDs on ElBridge — filled in el_android_init(). */
|
||||
static jmethodID g_mid_createLinearLayout = NULL;
|
||||
static jmethodID g_mid_createFrameLayout = NULL;
|
||||
static jmethodID g_mid_createScrollView = NULL;
|
||||
static jmethodID g_mid_createTextView = NULL;
|
||||
static jmethodID g_mid_createButton = NULL;
|
||||
static jmethodID g_mid_createEditText = NULL;
|
||||
static jmethodID g_mid_createImageView = NULL;
|
||||
static jmethodID g_mid_setContentView = NULL;
|
||||
static jmethodID g_mid_setTitle = NULL;
|
||||
static jmethodID g_mid_addChild = NULL;
|
||||
static jmethodID g_mid_removeChild = NULL;
|
||||
static jmethodID g_mid_destroyView = NULL;
|
||||
static jmethodID g_mid_setText = NULL;
|
||||
static jmethodID g_mid_getText = NULL;
|
||||
static jmethodID g_mid_setTextColor = NULL;
|
||||
static jmethodID g_mid_setBackgroundColor = NULL;
|
||||
static jmethodID g_mid_setFont = NULL;
|
||||
static jmethodID g_mid_setPadding = NULL;
|
||||
static jmethodID g_mid_setWidth = NULL;
|
||||
static jmethodID g_mid_setHeight = NULL;
|
||||
static jmethodID g_mid_setFlex = NULL;
|
||||
static jmethodID g_mid_setCornerRadius = NULL;
|
||||
static jmethodID g_mid_setEnabled = NULL;
|
||||
static jmethodID g_mid_setVisibility = NULL;
|
||||
static jmethodID g_mid_setOnClickListener = NULL;
|
||||
static jmethodID g_mid_setOnChangeListener = NULL;
|
||||
static jmethodID g_mid_setOnSubmitListener = NULL;
|
||||
static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */
|
||||
|
||||
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_ANDROID_MAX_WIDGETS 4096
|
||||
|
||||
typedef enum {
|
||||
EL_WIDGET_FREE = 0,
|
||||
EL_WIDGET_WINDOW = 1,
|
||||
EL_WIDGET_VSTACK = 2,
|
||||
EL_WIDGET_HSTACK = 3,
|
||||
EL_WIDGET_ZSTACK = 4,
|
||||
EL_WIDGET_SCROLL = 5,
|
||||
EL_WIDGET_LABEL = 6,
|
||||
EL_WIDGET_BUTTON = 7,
|
||||
EL_WIDGET_TEXTFIELD = 8,
|
||||
EL_WIDGET_TEXTAREA = 9,
|
||||
EL_WIDGET_IMAGE = 10,
|
||||
} ElWidgetKind;
|
||||
|
||||
typedef struct {
|
||||
ElWidgetKind kind;
|
||||
jint slot; /* Java-side slot index (matches C index) */
|
||||
char *cb_click; /* El function name for click / submit events */
|
||||
char *cb_change; /* El function name for value-change events */
|
||||
} ElWidget;
|
||||
|
||||
static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS];
|
||||
|
||||
static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) {
|
||||
for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) {
|
||||
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
||||
_el_widgets[i].kind = kind;
|
||||
_el_widgets[i].slot = slot;
|
||||
_el_widgets[i].cb_click = NULL;
|
||||
_el_widgets[i].cb_change = NULL;
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
EL_LOGE("el_widget_alloc: slot table full");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ElWidget *el_widget_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL;
|
||||
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
||||
return &_el_widgets[handle];
|
||||
}
|
||||
|
||||
static void el_widget_free(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
w->kind = EL_WIDGET_FREE;
|
||||
w->slot = -1;
|
||||
free(w->cb_click); w->cb_click = NULL;
|
||||
free(w->cb_change); w->cb_change = NULL;
|
||||
}
|
||||
|
||||
/* ── JNI environment helpers ─────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if
|
||||
* needed (detaches in el_jni_detach_if_attached — call in pairs).
|
||||
*/
|
||||
static int g_was_attached = 0; /* thread-local would be cleaner but this is
|
||||
safe for single-threaded el programs */
|
||||
|
||||
static JNIEnv *el_jni_env(void) {
|
||||
if (!g_jvm) return NULL;
|
||||
JNIEnv *env = NULL;
|
||||
jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6);
|
||||
if (rc == JNI_OK) { g_was_attached = 0; return env; }
|
||||
if (rc == JNI_EDETACHED) {
|
||||
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
|
||||
g_was_attached = 1;
|
||||
return env;
|
||||
}
|
||||
}
|
||||
EL_LOGE("el_jni_env: failed to obtain JNIEnv");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void el_jni_detach_if_attached(void) {
|
||||
if (g_was_attached && g_jvm) {
|
||||
(*g_jvm)->DetachCurrentThread(g_jvm);
|
||||
g_was_attached = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── UI-thread dispatch ──────────────────────────────────────────────────── */
|
||||
/*
|
||||
* Most ElBridge static methods already dispatch to the UI thread internally
|
||||
* (they call Activity.runOnUiThread). The helper below is available for
|
||||
* cases where the caller needs to be sure the call has completed before
|
||||
* returning (ElBridge methods marked "sync" use a CountDownLatch internally).
|
||||
*
|
||||
* For the current implementation we call ElBridge methods directly; ElBridge
|
||||
* itself marshals to the UI thread via Activity.runOnUiThread + latch.
|
||||
* This keeps the C side simple and mirrors the AppKit dispatch_sync pattern.
|
||||
*/
|
||||
|
||||
/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
(void)reserved;
|
||||
g_jvm = vm;
|
||||
EL_LOGI("JNI_OnLoad: el Android bridge loaded");
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
/* ── el_android_init ─────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* Called from __native_init(). The Activity must have already called
|
||||
* ElBridge.registerActivity(activity) from Java before this runs, which sets
|
||||
* g_activity via the nativeRegisterActivity JNI method below.
|
||||
*
|
||||
* Caches all method IDs used later so individual widget calls avoid repeated
|
||||
* FindClass / GetStaticMethodID lookups.
|
||||
*/
|
||||
void el_android_init(void) {
|
||||
static int done = 0;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; }
|
||||
|
||||
jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge");
|
||||
if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; }
|
||||
g_bridge_class = (*env)->NewGlobalRef(env, cls);
|
||||
(*env)->DeleteLocalRef(env, cls);
|
||||
|
||||
#define CACHE_STATIC(var, name, sig) \
|
||||
var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \
|
||||
if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig)
|
||||
|
||||
CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I");
|
||||
CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I");
|
||||
CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I");
|
||||
CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I");
|
||||
CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V");
|
||||
CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V");
|
||||
CACHE_STATIC(g_mid_addChild, "addChild", "(II)V");
|
||||
CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V");
|
||||
CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V");
|
||||
CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V");
|
||||
CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;");
|
||||
CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V");
|
||||
CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V");
|
||||
CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V");
|
||||
CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V");
|
||||
CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V");
|
||||
CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V");
|
||||
CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V");
|
||||
CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V");
|
||||
CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V");
|
||||
CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V");
|
||||
CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V");
|
||||
CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V");
|
||||
CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V");
|
||||
#undef CACHE_STATIC
|
||||
|
||||
el_jni_detach_if_attached();
|
||||
EL_LOGI("el_android_init: complete");
|
||||
}
|
||||
|
||||
/* ── JNI: Activity registration ─────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Called from Java: ElBridge.registerActivity(activity) calls back here.
|
||||
* Stores a global reference to the Activity so C code can dispatch to it.
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls,
|
||||
jobject activity) {
|
||||
(void)cls;
|
||||
if (g_activity) {
|
||||
(*env)->DeleteGlobalRef(env, g_activity);
|
||||
g_activity = NULL;
|
||||
}
|
||||
if (activity) {
|
||||
g_activity = (*env)->NewGlobalRef(env, activity);
|
||||
EL_LOGI("nativeRegisterActivity: activity registered");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── El callback invocation ──────────────────────────────────────────────── */
|
||||
/*
|
||||
* Invoke an El callback by symbol name.
|
||||
* Signature matches AppKit: fn(handle: Int, data: String) -> Void
|
||||
* compiled to: void fn(el_val_t handle, el_val_t data)
|
||||
*/
|
||||
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
||||
|
||||
static void el_android_invoke_cb(const char *fn_name, int64_t handle,
|
||||
const char *data) {
|
||||
if (!fn_name || !*fn_name) return;
|
||||
void *sym = dlsym(RTLD_DEFAULT, fn_name);
|
||||
if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; }
|
||||
ElCb2 fn = (ElCb2)sym;
|
||||
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
||||
}
|
||||
|
||||
/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) {
|
||||
(void)env; (void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_click) {
|
||||
el_android_invoke_cb(w->cb_click, handle, "");
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls,
|
||||
jint slot, jstring text) {
|
||||
(void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_change) {
|
||||
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
||||
el_android_invoke_cb(w->cb_change, handle, ctext);
|
||||
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls,
|
||||
jint slot, jstring text) {
|
||||
(void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */
|
||||
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
||||
el_android_invoke_cb(w->cb_click, handle, ctext);
|
||||
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Helper: jstring from C string ──────────────────────────────────────── */
|
||||
|
||||
static jstring el_jstr(JNIEnv *env, const char *s) {
|
||||
return (*env)->NewStringUTF(env, s ? s : "");
|
||||
}
|
||||
|
||||
/* ── Window ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_android_window_create — on Android a "window" is the root LinearLayout
|
||||
* set as the Activity's content view. We create a vertical LinearLayout and
|
||||
* store it. el_android_window_show calls setContentView on the Activity.
|
||||
*/
|
||||
int64_t el_android_window_create(const char *title, int width, int height,
|
||||
int min_width, int min_height) {
|
||||
(void)width; (void)height; (void)min_width; (void)min_height;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
|
||||
/* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)1 /* VERTICAL */, (jint)0);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
(*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1;
|
||||
}
|
||||
|
||||
/* Set activity title */
|
||||
if (g_mid_setTitle && title) {
|
||||
jstring jtitle = el_jstr(env, title);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
||||
(*env)->DeleteLocalRef(env, jtitle);
|
||||
}
|
||||
|
||||
int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return handle;
|
||||
}
|
||||
|
||||
void el_android_window_show(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w || w->kind != EL_WIDGET_WINDOW) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_window_set_title(int64_t handle, const char *title) {
|
||||
(void)handle;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jtitle = el_jstr(env, title);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
||||
(*env)->DeleteLocalRef(env, jtitle);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Layout containers ───────────────────────────────────────────────────── */
|
||||
|
||||
int64_t el_android_vstack_create(int spacing) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)1 /* VERTICAL */, (jint)spacing);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_hstack_create(int spacing) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)0 /* HORIZONTAL */, (jint)spacing);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_zstack_create(void) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createFrameLayout);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_scroll_create(void) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createScrollView);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
||||
|
||||
int64_t el_android_label_create(const char *text) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jt = el_jstr(env, text);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createTextView, jt);
|
||||
(*env)->DeleteLocalRef(env, jt);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_button_create(const char *label) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jl = el_jstr(env, label);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createButton, jl);
|
||||
(*env)->DeleteLocalRef(env, jl);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_text_field_create(const char *placeholder) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, placeholder);
|
||||
/* singleLine = true */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createEditText, jp, (jboolean)JNI_TRUE);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_text_area_create(const char *placeholder) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, placeholder);
|
||||
/* singleLine = false → multiline EditText */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createEditText, jp, (jboolean)JNI_FALSE);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_image_create(const char *path) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, path);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createImageView, jp);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
/* ── Widget property setters ─────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_set_text(int64_t handle, const char *text) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jt = el_jstr(env, text);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText,
|
||||
(jint)w->slot, jt);
|
||||
(*env)->DeleteLocalRef(env, jt);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
const char *el_android_widget_get_text(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return "";
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return "";
|
||||
jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class,
|
||||
g_mid_getText,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; }
|
||||
const char *result = "";
|
||||
if (js) {
|
||||
const char *cstr = (*env)->GetStringUTFChars(env, js, NULL);
|
||||
result = cstr ? strdup(cstr) : "";
|
||||
if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr);
|
||||
(*env)->DeleteLocalRef(env, js);
|
||||
}
|
||||
el_jni_detach_if_attached();
|
||||
return result;
|
||||
}
|
||||
|
||||
void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor,
|
||||
(jint)w->slot, (jfloat)r, (jfloat)g,
|
||||
(jfloat)b, (jfloat)a);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor,
|
||||
(jint)w->slot, (jfloat)r, (jfloat)g,
|
||||
(jfloat)b, (jfloat)a);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jfam = el_jstr(env, family);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont,
|
||||
(jint)w->slot, jfam, (jint)size,
|
||||
(jboolean)(bold ? JNI_TRUE : JNI_FALSE));
|
||||
(*env)->DeleteLocalRef(env, jfam);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding,
|
||||
(jint)w->slot, (jint)top, (jint)right,
|
||||
(jint)bottom, (jint)left);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_width(int64_t handle, int width) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth,
|
||||
(jint)w->slot, (jint)width);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_height(int64_t handle, int height) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight,
|
||||
(jint)w->slot, (jint)height);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_flex(int64_t handle, int flex) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex,
|
||||
(jint)w->slot, (jint)flex);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_corner_radius(int64_t handle, int radius) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius,
|
||||
(jint)w->slot, (jfloat)radius);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_disabled(int64_t handle, int disabled) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled,
|
||||
(jint)w->slot,
|
||||
(jboolean)(disabled ? JNI_FALSE : JNI_TRUE));
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_hidden(int64_t handle, int hidden) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
/* visible=true means NOT hidden */
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility,
|
||||
(jint)w->slot,
|
||||
(jboolean)(hidden ? JNI_FALSE : JNI_TRUE));
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Child management ─────────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_add_child(int64_t parent, int64_t child) {
|
||||
ElWidget *pw = el_widget_get(parent);
|
||||
ElWidget *cw = el_widget_get(child);
|
||||
if (!pw || !cw) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild,
|
||||
(jint)pw->slot, (jint)cw->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_remove_child(int64_t parent, int64_t child) {
|
||||
ElWidget *pw = el_widget_get(parent);
|
||||
ElWidget *cw = el_widget_get(child);
|
||||
if (!pw || !cw) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild,
|
||||
(jint)pw->slot, (jint)cw->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Event registration ───────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_on_click(int64_t handle, const char *fn_name) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_click);
|
||||
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_click) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_on_change(int64_t handle, const char *fn_name) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_change);
|
||||
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_change) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_on_submit(int64_t handle, const char *fn_name) {
|
||||
/* Submit stored in cb_click, same as AppKit. */
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_click);
|
||||
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_click) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Widget destroy ───────────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_destroy(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (env && g_bridge_class) {
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
}
|
||||
el_widget_free(handle);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Manifest reader ─────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* __manifest_read: parse the app{} block from a manifest file.
|
||||
* Returns the raw file contents as an el_val_t (const char* cast).
|
||||
* The caller (el program) parses the returned string.
|
||||
* Reads from the filesystem; for APK assets use the AssetManager path instead.
|
||||
*/
|
||||
static char *el_read_file(const char *path) {
|
||||
if (!path || !*path) return NULL;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len <= 0) { fclose(f); return NULL; }
|
||||
char *buf = (char *)malloc((size_t)len + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
fread(buf, 1, (size_t)len, f);
|
||||
buf[len] = '\0';
|
||||
fclose(f);
|
||||
return buf;
|
||||
}
|
||||
|
||||
el_val_t el_android_manifest_read(const char *path) {
|
||||
char *contents = el_read_file(path);
|
||||
if (!contents) return (el_val_t)(uintptr_t)"";
|
||||
return (el_val_t)(uintptr_t)contents; /* caller owns allocation */
|
||||
}
|
||||
|
||||
/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */
|
||||
/*
|
||||
* These are the functions declared in el_native_target.h under EL_TARGET_ANDROID.
|
||||
* They forward to the el_android_* internal functions above.
|
||||
*
|
||||
* The el_val_t / int64_t ABI matches the AppKit functions exactly:
|
||||
* - Integer params passed as int64_t, extracted with (int)
|
||||
* - String params passed as int64_t, extracted with (const char*)(uintptr_t)
|
||||
* - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted
|
||||
* with el_to_float / bit-cast union
|
||||
*/
|
||||
|
||||
static inline float el_val_to_float(el_val_t v) {
|
||||
union { double d; int64_t i; } u;
|
||||
u.i = v;
|
||||
return (float)u.d;
|
||||
}
|
||||
|
||||
void __native_init(void) {
|
||||
el_android_init();
|
||||
}
|
||||
|
||||
void __native_run_loop(void) {
|
||||
/* No-op on Android — lifecycle is driven by the Activity. */
|
||||
}
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height) {
|
||||
return (el_val_t)el_android_window_create(
|
||||
(const char *)(uintptr_t)title,
|
||||
(int)width, (int)height, (int)min_width, (int)min_height);
|
||||
}
|
||||
|
||||
void __window_show(el_val_t handle) {
|
||||
el_android_window_show((int64_t)handle);
|
||||
}
|
||||
|
||||
void __window_set_title(el_val_t handle, el_val_t title) {
|
||||
el_android_window_set_title((int64_t)handle,
|
||||
(const char *)(uintptr_t)title);
|
||||
}
|
||||
|
||||
el_val_t __vstack_create(el_val_t spacing) {
|
||||
return (el_val_t)el_android_vstack_create((int)spacing);
|
||||
}
|
||||
|
||||
el_val_t __hstack_create(el_val_t spacing) {
|
||||
return (el_val_t)el_android_hstack_create((int)spacing);
|
||||
}
|
||||
|
||||
el_val_t __zstack_create(void) {
|
||||
return (el_val_t)el_android_zstack_create();
|
||||
}
|
||||
|
||||
el_val_t __scroll_create(void) {
|
||||
return (el_val_t)el_android_scroll_create();
|
||||
}
|
||||
|
||||
el_val_t __label_create(el_val_t text) {
|
||||
return (el_val_t)el_android_label_create((const char *)(uintptr_t)text);
|
||||
}
|
||||
|
||||
el_val_t __button_create(el_val_t label) {
|
||||
return (el_val_t)el_android_button_create((const char *)(uintptr_t)label);
|
||||
}
|
||||
|
||||
el_val_t __text_field_create(el_val_t placeholder) {
|
||||
return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder);
|
||||
}
|
||||
|
||||
el_val_t __text_area_create(el_val_t placeholder) {
|
||||
return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder);
|
||||
}
|
||||
|
||||
el_val_t __image_create(el_val_t path_or_name) {
|
||||
return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name);
|
||||
}
|
||||
|
||||
void __widget_set_text(el_val_t handle, el_val_t text) {
|
||||
el_android_widget_set_text((int64_t)handle,
|
||||
(const char *)(uintptr_t)text);
|
||||
}
|
||||
|
||||
el_val_t __widget_get_text(el_val_t handle) {
|
||||
return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle);
|
||||
}
|
||||
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a) {
|
||||
el_android_widget_set_color((int64_t)handle,
|
||||
el_val_to_float(r), el_val_to_float(g),
|
||||
el_val_to_float(b), el_val_to_float(a));
|
||||
}
|
||||
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a) {
|
||||
el_android_widget_set_bg_color((int64_t)handle,
|
||||
el_val_to_float(r), el_val_to_float(g),
|
||||
el_val_to_float(b), el_val_to_float(a));
|
||||
}
|
||||
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold) {
|
||||
el_android_widget_set_font((int64_t)handle,
|
||||
(const char *)(uintptr_t)family,
|
||||
(int)size, (int)bold);
|
||||
}
|
||||
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left) {
|
||||
el_android_widget_set_padding((int64_t)handle,
|
||||
(int)top, (int)right, (int)bottom, (int)left);
|
||||
}
|
||||
|
||||
void __widget_set_width(el_val_t handle, el_val_t width) {
|
||||
el_android_widget_set_width((int64_t)handle, (int)width);
|
||||
}
|
||||
|
||||
void __widget_set_height(el_val_t handle, el_val_t height) {
|
||||
el_android_widget_set_height((int64_t)handle, (int)height);
|
||||
}
|
||||
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex) {
|
||||
el_android_widget_set_flex((int64_t)handle, (int)flex);
|
||||
}
|
||||
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius) {
|
||||
el_android_widget_set_corner_radius((int64_t)handle, (int)radius);
|
||||
}
|
||||
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled) {
|
||||
el_android_widget_set_disabled((int64_t)handle, (int)disabled);
|
||||
}
|
||||
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden) {
|
||||
el_android_widget_set_hidden((int64_t)handle, (int)hidden);
|
||||
}
|
||||
|
||||
void __widget_add_child(el_val_t parent, el_val_t child) {
|
||||
el_android_widget_add_child((int64_t)parent, (int64_t)child);
|
||||
}
|
||||
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child) {
|
||||
el_android_widget_remove_child((int64_t)parent, (int64_t)child);
|
||||
}
|
||||
|
||||
void __widget_destroy(el_val_t handle) {
|
||||
el_android_widget_destroy((int64_t)handle);
|
||||
}
|
||||
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_click((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_change((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_submit((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
el_val_t __manifest_read(el_val_t path) {
|
||||
return el_android_manifest_read((const char *)(uintptr_t)path);
|
||||
}
|
||||
|
||||
#endif /* EL_TARGET_ANDROID */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,844 +0,0 @@
|
||||
/*
|
||||
* el_lvgl.c — LVGL v9 backend for the el native widget system.
|
||||
*
|
||||
* This file implements the microcontroller/embedded widget layer that el_seed.c
|
||||
* calls through to when EL_TARGET_LVGL is defined.
|
||||
*
|
||||
* Architecture:
|
||||
* el program (el code)
|
||||
* → __widget_* C builtins in el_seed.c
|
||||
* → el_lvgl_* C functions defined here
|
||||
* → lv_obj_t widgets via LVGL v9
|
||||
*
|
||||
* Target platforms: ESP32, STM32, industrial panels, any system with 256KB+
|
||||
* RAM and an LVGL-compatible display driver. No OS required.
|
||||
*
|
||||
* Widget handles: every widget is assigned an int64_t slot index into
|
||||
* g_widgets[]. The el program holds these as opaque Int values.
|
||||
* Slot 0 is reserved; -1 = invalid handle.
|
||||
*
|
||||
* Window model: on embedded there is one screen. __window_create configures
|
||||
* lv_scr_act() as the root container. __window_show is a no-op (the screen
|
||||
* is always visible). __native_run_loop calls lv_task_handler() in a tight
|
||||
* loop — on RTOS this runs inside a dedicated task; on bare metal it IS the
|
||||
* main loop. The host application is responsible for initialising the display
|
||||
* driver and calling lv_tick_inc() before calling __native_run_loop.
|
||||
*
|
||||
* Callback dispatch:
|
||||
* When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing):
|
||||
* dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime.
|
||||
* When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32):
|
||||
* The caller must provide:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
* which maps function names to function pointers via a compile-time table.
|
||||
*
|
||||
* Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font
|
||||
* maps the requested point size to the nearest available Montserrat variant.
|
||||
* Bold is approximated by stepping up two sizes (no separate bold face in the
|
||||
* default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select().
|
||||
*
|
||||
* Compile (hosted test build):
|
||||
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
* # Then link with lvgl.a / lvgl source tree.
|
||||
*
|
||||
* Compile (bare-metal, no dynamic linker):
|
||||
* arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \
|
||||
* -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
*/
|
||||
|
||||
#ifdef EL_TARGET_LVGL
|
||||
|
||||
#include "lvgl/lvgl.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef EL_LVGL_NO_DLSYM
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Callback dispatch macro ─────────────────────────────────────────────── */
|
||||
|
||||
#ifdef EL_LVGL_NO_DLSYM
|
||||
/*
|
||||
* Bare-metal path. The application provides this function:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
* It maps string names → function pointers, typically via a switch on a hash
|
||||
* or a sorted table of {name, fn_ptr} pairs generated by elc.
|
||||
*/
|
||||
extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b))
|
||||
#else
|
||||
/*
|
||||
* Hosted path. dlsym resolves the El symbol at call time.
|
||||
* We use a compound-statement expression (GCC/Clang extension) to avoid
|
||||
* executing dlsym more than once per call.
|
||||
*/
|
||||
#define EL_LVGL_CALL(fn_name, a, b) \
|
||||
({ \
|
||||
typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \
|
||||
_el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \
|
||||
_fn ? _fn((a), (b)) : (el_val_t)0; \
|
||||
})
|
||||
#endif
|
||||
|
||||
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_LVGL_MAX_WIDGETS 4096
|
||||
|
||||
/*
|
||||
* Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay
|
||||
* consistent across all targets.
|
||||
*/
|
||||
typedef enum {
|
||||
EL_LVGL_FREE = 0,
|
||||
EL_LVGL_WINDOW = 1,
|
||||
EL_LVGL_VSTACK = 2,
|
||||
EL_LVGL_HSTACK = 3,
|
||||
EL_LVGL_ZSTACK = 4,
|
||||
EL_LVGL_SCROLL = 5,
|
||||
EL_LVGL_LABEL = 6,
|
||||
EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */
|
||||
EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */
|
||||
EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */
|
||||
EL_LVGL_IMAGE = 10,
|
||||
EL_LVGL_DIVIDER = 11,
|
||||
EL_LVGL_SPACER = 12,
|
||||
} ElLvglKind;
|
||||
|
||||
/*
|
||||
* Per-slot state. Callback names are stored inline (256 bytes each) to avoid
|
||||
* heap allocation on targets with no malloc or fragmented heaps.
|
||||
*/
|
||||
typedef struct {
|
||||
ElLvglKind kind;
|
||||
lv_obj_t *obj; /* primary LVGL object */
|
||||
lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */
|
||||
char cb_click[256];
|
||||
char cb_change[256];
|
||||
char cb_submit[256];
|
||||
} ElLvglWidget;
|
||||
|
||||
static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS];
|
||||
|
||||
/* ── Slot helpers ─────────────────────────────────────────────────────────── */
|
||||
|
||||
static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) {
|
||||
for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) {
|
||||
if (g_widgets[i].kind == EL_LVGL_FREE) {
|
||||
g_widgets[i].kind = kind;
|
||||
g_widgets[i].obj = obj;
|
||||
g_widgets[i].btn_label = NULL;
|
||||
g_widgets[i].cb_click[0] = '\0';
|
||||
g_widgets[i].cb_change[0] = '\0';
|
||||
g_widgets[i].cb_submit[0] = '\0';
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
return -1; /* table full */
|
||||
}
|
||||
|
||||
static ElLvglWidget *lvgl_slot_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL;
|
||||
if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL;
|
||||
return &g_widgets[handle];
|
||||
}
|
||||
|
||||
static void lvgl_slot_free(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return;
|
||||
ElLvglWidget *w = &g_widgets[handle];
|
||||
w->kind = EL_LVGL_FREE;
|
||||
w->obj = NULL;
|
||||
w->btn_label = NULL;
|
||||
w->cb_click[0] = '\0';
|
||||
w->cb_change[0] = '\0';
|
||||
w->cb_submit[0] = '\0';
|
||||
}
|
||||
|
||||
/* ── Font selection ───────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h):
|
||||
* 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
|
||||
*
|
||||
* We map the requested size to the nearest available size. Bold is
|
||||
* approximated by stepping up two sizes (no separate bold face in the default
|
||||
* font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely.
|
||||
*/
|
||||
#ifndef EL_LVGL_CUSTOM_FONT
|
||||
|
||||
static const lv_font_t *font_select(int size, int bold) {
|
||||
/* Step up two sizes for bold approximation. */
|
||||
if (bold) size += 4;
|
||||
|
||||
/* Clamp to available range. */
|
||||
if (size < 8) size = 8;
|
||||
if (size > 48) size = 48;
|
||||
|
||||
/* Round to nearest even size >= 8. */
|
||||
if (size % 2 != 0) size++;
|
||||
|
||||
switch (size) {
|
||||
#if LV_FONT_MONTSERRAT_8
|
||||
case 8: return &lv_font_montserrat_8;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_10
|
||||
case 10: return &lv_font_montserrat_10;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_12
|
||||
case 12: return &lv_font_montserrat_12;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_14
|
||||
case 14: return &lv_font_montserrat_14;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_16
|
||||
case 16: return &lv_font_montserrat_16;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_18
|
||||
case 18: return &lv_font_montserrat_18;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_20
|
||||
case 20: return &lv_font_montserrat_20;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_22
|
||||
case 22: return &lv_font_montserrat_22;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_24
|
||||
case 24: return &lv_font_montserrat_24;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_26
|
||||
case 26: return &lv_font_montserrat_26;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_28
|
||||
case 28: return &lv_font_montserrat_28;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_30
|
||||
case 30: return &lv_font_montserrat_30;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_32
|
||||
case 32: return &lv_font_montserrat_32;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_34
|
||||
case 34: return &lv_font_montserrat_34;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_36
|
||||
case 36: return &lv_font_montserrat_36;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_38
|
||||
case 38: return &lv_font_montserrat_38;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_40
|
||||
case 40: return &lv_font_montserrat_40;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_42
|
||||
case 42: return &lv_font_montserrat_42;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_44
|
||||
case 44: return &lv_font_montserrat_44;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_46
|
||||
case 46: return &lv_font_montserrat_46;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_48
|
||||
case 48: return &lv_font_montserrat_48;
|
||||
#endif
|
||||
default:
|
||||
/*
|
||||
* Requested size is not compiled in. Fall back to the default
|
||||
* theme font, which is guaranteed to be present.
|
||||
*/
|
||||
return LV_FONT_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* EL_LVGL_CUSTOM_FONT */
|
||||
|
||||
/* ── Event callback ───────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Single LVGL event callback used for all widget events. The user_data is
|
||||
* the slot index cast to (void*) via intptr_t — avoids heap allocation.
|
||||
*
|
||||
* Three event codes are handled:
|
||||
* LV_EVENT_CLICKED → cb_click (buttons, any tappable widget)
|
||||
* LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.)
|
||||
* LV_EVENT_READY → cb_submit (Enter pressed in textarea one-line mode)
|
||||
*/
|
||||
static void el_lvgl_event_cb(lv_event_t *e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
intptr_t slot = (intptr_t)lv_event_get_user_data(e);
|
||||
ElLvglWidget *w = lvgl_slot_get((int64_t)slot);
|
||||
if (!w) return;
|
||||
|
||||
if (code == LV_EVENT_CLICKED && w->cb_click[0]) {
|
||||
EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0);
|
||||
}
|
||||
|
||||
if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) {
|
||||
/*
|
||||
* Retrieve current text for textarea/textfield widgets so the handler
|
||||
* receives the updated value as its second argument.
|
||||
*/
|
||||
const char *txt = "";
|
||||
lv_obj_t *target = lv_event_get_target(e);
|
||||
if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) {
|
||||
txt = lv_textarea_get_text(target);
|
||||
if (!txt) txt = "";
|
||||
}
|
||||
EL_LVGL_CALL(w->cb_change, (el_val_t)slot,
|
||||
(el_val_t)(uintptr_t)txt);
|
||||
}
|
||||
|
||||
if (code == LV_EVENT_READY && w->cb_submit[0]) {
|
||||
/* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */
|
||||
EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Initialisation ───────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_init — call lv_init(). The host must have already initialised the
|
||||
* display driver and input driver before this, or immediately after. Idempotent.
|
||||
*/
|
||||
void el_lvgl_init(void) {
|
||||
static int done = 0;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
lv_init();
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_run_loop — drive lv_task_handler() indefinitely.
|
||||
*
|
||||
* On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task.
|
||||
* On bare metal: call this as the last statement of main().
|
||||
*
|
||||
* The 5 ms delay between handler calls matches the LVGL documentation
|
||||
* recommendation for a ~200 Hz refresh budget.
|
||||
*
|
||||
* On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS
|
||||
* targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc.
|
||||
*/
|
||||
void el_lvgl_run_loop(void) {
|
||||
for (;;) {
|
||||
lv_task_handler();
|
||||
#if defined(EL_LVGL_RTOS_DELAY)
|
||||
EL_LVGL_RTOS_DELAY(5);
|
||||
#elif defined(__linux__) || defined(__APPLE__)
|
||||
{
|
||||
/* Hosted test build — usleep available. */
|
||||
#include <unistd.h>
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
/* Bare-metal without a delay macro: the HAL tick increment loop
|
||||
* is the caller's responsibility. No sleep needed if lv_tick_inc()
|
||||
* is driven from a hardware timer ISR. */
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Window ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_window_create — configure lv_scr_act() as a vertical flex container
|
||||
* and return a slot handle wrapping it. The title is stored for informational
|
||||
* purposes (e.g., a status bar widget the host might create). Width/height
|
||||
* are ignored on embedded targets because the screen size is fixed by the
|
||||
* display driver; they are accepted for API compatibility with other backends.
|
||||
*/
|
||||
int64_t el_lvgl_window_create(const char *title, int width, int height,
|
||||
int min_width, int min_height) {
|
||||
(void)width; (void)height; (void)min_width; (void)min_height;
|
||||
|
||||
lv_obj_t *scr = lv_scr_act();
|
||||
|
||||
/* Configure the active screen as a vertical flex container so that
|
||||
* widgets added via __widget_add_child stack naturally. */
|
||||
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100));
|
||||
|
||||
/* Store the window title in a user-data string on the screen object
|
||||
* so host code can retrieve it if it wants to render a title bar. */
|
||||
if (title && *title) {
|
||||
/* lv_obj_set_user_data stores a void* — we cast the string pointer.
|
||||
* The string must outlive the screen object; for literals this is
|
||||
* always true. For dynamic titles use el_lvgl_window_set_title. */
|
||||
lv_obj_set_user_data(scr, (void *)(uintptr_t)title);
|
||||
}
|
||||
|
||||
/* Allocate a slot for the screen object. */
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr);
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_window_show — no-op on embedded. The screen is always visible.
|
||||
*/
|
||||
void el_lvgl_window_show(int64_t handle) {
|
||||
(void)handle;
|
||||
/* On multi-screen setups, load the screen: */
|
||||
/* ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
* if (w) lv_scr_load(w->obj); */
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_window_set_title — update the user_data pointer on the screen object.
|
||||
* On embedded, "title" is typically only used by a custom host title bar.
|
||||
*/
|
||||
void el_lvgl_window_set_title(int64_t handle, const char *title) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w || w->kind != EL_LVGL_WINDOW) return;
|
||||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : ""));
|
||||
}
|
||||
|
||||
/* ── Layout containers ────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing.
|
||||
*/
|
||||
int64_t el_lvgl_vstack_create(int spacing) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0);
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
/* Remove default LVGL border and background so containers are transparent
|
||||
* by default, matching the AppKit/GTK4 backends. */
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_VSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing.
|
||||
*/
|
||||
int64_t el_lvgl_hstack_create(int spacing) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0);
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_HSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_zstack_create — plain container, children positioned absolutely.
|
||||
* No flex flow is set; callers use lv_obj_set_pos() on children directly,
|
||||
* or rely on their natural 0,0 origin.
|
||||
*/
|
||||
int64_t el_lvgl_zstack_create(void) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_scroll_create — vertically scrollable container.
|
||||
*/
|
||||
int64_t el_lvgl_scroll_create(void) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_SCROLL, obj);
|
||||
}
|
||||
|
||||
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_label_create — static text label.
|
||||
*/
|
||||
int64_t el_lvgl_label_create(const char *text) {
|
||||
lv_obj_t *obj = lv_label_create(lv_scr_act());
|
||||
lv_label_set_text(obj, text ? text : "");
|
||||
return lvgl_slot_alloc(EL_LVGL_LABEL, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_button_create — pressable button with a child label.
|
||||
*
|
||||
* LVGL buttons are containers; text is placed in an inner lv_label child.
|
||||
* We store the child label pointer in btn_label so set_text / get_text can
|
||||
* reach it without searching the object tree at runtime.
|
||||
*/
|
||||
int64_t el_lvgl_button_create(const char *label) {
|
||||
lv_obj_t *btn = lv_btn_create(lv_scr_act());
|
||||
lv_obj_t *lbl = lv_label_create(btn);
|
||||
lv_label_set_text(lbl, label ? label : "");
|
||||
lv_obj_center(lbl);
|
||||
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn);
|
||||
if (h >= 0) {
|
||||
g_widgets[h].btn_label = lbl;
|
||||
/* Register click callback immediately so button responds when a
|
||||
* callback name is registered later via __widget_on_click. */
|
||||
lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_text_field_create — single-line text input.
|
||||
*/
|
||||
int64_t el_lvgl_text_field_create(const char *placeholder) {
|
||||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||||
lv_textarea_set_one_line(obj, true);
|
||||
if (placeholder && *placeholder) {
|
||||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||||
}
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj);
|
||||
if (h >= 0) {
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)h);
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_text_area_create — multi-line text input.
|
||||
*/
|
||||
int64_t el_lvgl_text_area_create(const char *placeholder) {
|
||||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||||
lv_textarea_set_one_line(obj, false);
|
||||
if (placeholder && *placeholder) {
|
||||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||||
}
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj);
|
||||
if (h >= 0) {
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_image_create — image widget.
|
||||
*
|
||||
* On hosted Linux (SDL backend), path is a filesystem path.
|
||||
* On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin".
|
||||
* LVGL image decoders are registered separately by the host application.
|
||||
*/
|
||||
int64_t el_lvgl_image_create(const char *path_or_name) {
|
||||
lv_obj_t *obj = lv_img_create(lv_scr_act());
|
||||
if (path_or_name && *path_or_name) {
|
||||
lv_img_set_src(obj, path_or_name);
|
||||
}
|
||||
return lvgl_slot_alloc(EL_LVGL_IMAGE, obj);
|
||||
}
|
||||
|
||||
/* ── Widget property setters ─────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_text — update visible text.
|
||||
*
|
||||
* Dispatch per kind:
|
||||
* LABEL → lv_label_set_text
|
||||
* BUTTON → lv_label_set_text on inner btn_label child
|
||||
* TEXTFIELD / TEXTAREA → lv_textarea_set_text
|
||||
* WINDOW → lv_obj_set_user_data (stores title string)
|
||||
*/
|
||||
void el_lvgl_widget_set_text(int64_t handle, const char *text) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
const char *t = text ? text : "";
|
||||
switch (w->kind) {
|
||||
case EL_LVGL_LABEL:
|
||||
lv_label_set_text(w->obj, t);
|
||||
break;
|
||||
case EL_LVGL_BUTTON:
|
||||
if (w->btn_label) lv_label_set_text(w->btn_label, t);
|
||||
break;
|
||||
case EL_LVGL_TEXTFIELD:
|
||||
case EL_LVGL_TEXTAREA:
|
||||
lv_textarea_set_text(w->obj, t);
|
||||
break;
|
||||
case EL_LVGL_WINDOW:
|
||||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_get_text — retrieve visible text.
|
||||
*
|
||||
* Returns a pointer into LVGL's internal storage — valid until the next LVGL
|
||||
* operation that modifies the widget. Callers that need to hold the value
|
||||
* across LVGL calls must strdup() it.
|
||||
*/
|
||||
const char *el_lvgl_widget_get_text(int64_t handle) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return "";
|
||||
switch (w->kind) {
|
||||
case EL_LVGL_LABEL:
|
||||
return lv_label_get_text(w->obj);
|
||||
case EL_LVGL_BUTTON:
|
||||
return w->btn_label ? lv_label_get_text(w->btn_label) : "";
|
||||
case EL_LVGL_TEXTFIELD:
|
||||
case EL_LVGL_TEXTAREA:
|
||||
return lv_textarea_get_text(w->obj);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_color — foreground (text) colour.
|
||||
*
|
||||
* r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h).
|
||||
* LVGL lv_color_make takes uint8_t 0–255 components.
|
||||
*/
|
||||
void el_lvgl_widget_set_color(int64_t handle,
|
||||
float r, float g, float b, float a) {
|
||||
(void)a; /* LVGL text colour has no per-glyph alpha channel */
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_color_t c = lv_color_make(
|
||||
(uint8_t)(r * 255.0f + 0.5f),
|
||||
(uint8_t)(g * 255.0f + 0.5f),
|
||||
(uint8_t)(b * 255.0f + 0.5f));
|
||||
lv_obj_set_style_text_color(w->obj, c, 0);
|
||||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||||
lv_obj_set_style_text_color(w->btn_label, c, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_bg_color — background fill colour + opacity.
|
||||
*/
|
||||
void el_lvgl_widget_set_bg_color(int64_t handle,
|
||||
float r, float g, float b, float a) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_color_t c = lv_color_make(
|
||||
(uint8_t)(r * 255.0f + 0.5f),
|
||||
(uint8_t)(g * 255.0f + 0.5f),
|
||||
(uint8_t)(b * 255.0f + 0.5f));
|
||||
lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f);
|
||||
lv_obj_set_style_bg_color(w->obj, c, 0);
|
||||
lv_obj_set_style_bg_opa(w->obj, opa, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_font — apply font to text-bearing widget.
|
||||
*
|
||||
* The `family` parameter is accepted for API compatibility but LVGL uses
|
||||
* compiled-in fonts only. Only the size and bold flag have effect unless
|
||||
* EL_LVGL_CUSTOM_FONT is defined by the host.
|
||||
*/
|
||||
void el_lvgl_widget_set_font(int64_t handle,
|
||||
const char *family, int size, int bold) {
|
||||
(void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
const lv_font_t *font = font_select(size, bold);
|
||||
if (!font) return;
|
||||
lv_obj_set_style_text_font(w->obj, font, 0);
|
||||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||||
lv_obj_set_style_text_font(w->btn_label, font, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left).
|
||||
*/
|
||||
void el_lvgl_widget_set_padding(int64_t handle,
|
||||
int top, int right, int bottom, int left) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0);
|
||||
lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0);
|
||||
lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0);
|
||||
lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_width — set explicit pixel width.
|
||||
*/
|
||||
void el_lvgl_widget_set_width(int64_t handle, int width) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_width(w->obj, (lv_coord_t)width);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_height — set explicit pixel height.
|
||||
*/
|
||||
void el_lvgl_widget_set_height(int64_t handle, int height) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_height(w->obj, (lv_coord_t)height);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_flex — set flex grow factor.
|
||||
*
|
||||
* flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill
|
||||
* remaining space proportional to its grow factor.
|
||||
* flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size.
|
||||
*/
|
||||
void el_lvgl_widget_set_flex(int64_t handle, int flex) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0));
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_corner_radius — set border radius.
|
||||
*/
|
||||
void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_disabled — enable/disable interactive state.
|
||||
*
|
||||
* LV_STATE_DISABLED greys out the widget and prevents input events.
|
||||
*/
|
||||
void el_lvgl_widget_set_disabled(int64_t handle, int disabled) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
if (disabled) {
|
||||
lv_obj_add_state(w->obj, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_clear_state(w->obj, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_hidden — show/hide widget.
|
||||
*
|
||||
* LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow.
|
||||
*/
|
||||
void el_lvgl_widget_set_hidden(int64_t handle, int hidden) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
if (hidden) {
|
||||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tree operations ──────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_add_child — attach child widget to parent.
|
||||
*
|
||||
* lv_obj_set_parent() reparents the child object inside the LVGL tree.
|
||||
* For WINDOW parents we use the screen object itself as the parent, since
|
||||
* lv_scr_act() IS the root container.
|
||||
*/
|
||||
void el_lvgl_widget_add_child(int64_t parent, int64_t child) {
|
||||
ElLvglWidget *pw = lvgl_slot_get(parent);
|
||||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||||
if (!pw || !cw) return;
|
||||
lv_obj_set_parent(cw->obj, pw->obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_remove_child — detach child from its current parent.
|
||||
*
|
||||
* LVGL has no explicit "remove from parent without deleting" operation.
|
||||
* We reparent the child back to the active screen (making it a root-level
|
||||
* floating widget) and then hide it. The widget still occupies a slot and
|
||||
* can be re-attached or destroyed later.
|
||||
*/
|
||||
void el_lvgl_widget_remove_child(int64_t parent, int64_t child) {
|
||||
(void)parent;
|
||||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||||
if (!cw) return;
|
||||
/* Move to screen root and hide. */
|
||||
lv_obj_set_parent(cw->obj, lv_scr_act());
|
||||
lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_destroy — delete widget and its children from the LVGL tree,
|
||||
* then free the slot.
|
||||
*
|
||||
* lv_obj_del() recursively deletes the object and all children. After this
|
||||
* call the handle is invalid and must not be used.
|
||||
*/
|
||||
void el_lvgl_widget_destroy(int64_t handle) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_del(w->obj);
|
||||
lvgl_slot_free(handle);
|
||||
}
|
||||
|
||||
/* ── Event registration ───────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Event registration stores the El function name in the widget slot. The
|
||||
* actual lv_obj_add_event_cb() call is made here (or was made in the factory
|
||||
* for buttons/textfields where we know the relevant event codes upfront).
|
||||
*
|
||||
* For widgets that did not register their event callback in the factory (e.g.
|
||||
* labels receiving a click handler), we add the LVGL event binding now.
|
||||
*/
|
||||
|
||||
void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_click, fn_name ? fn_name : "", 255);
|
||||
w->cb_click[255] = '\0';
|
||||
/*
|
||||
* Buttons already have LV_EVENT_CLICKED registered in the factory.
|
||||
* For other widget kinds (labels, containers used as tap targets), add
|
||||
* the click flag and register the callback.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_BUTTON) {
|
||||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_change, fn_name ? fn_name : "", 255);
|
||||
w->cb_change[255] = '\0';
|
||||
/*
|
||||
* Textfield/textarea factories already register VALUE_CHANGED.
|
||||
* For other kinds (e.g. a custom toggle), add the binding now.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) {
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_submit, fn_name ? fn_name : "", 255);
|
||||
w->cb_submit[255] = '\0';
|
||||
/*
|
||||
* LV_EVENT_READY fires when Enter is pressed in a one-line textarea.
|
||||
* Textfield factories already register READY. For other kinds, add it.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_TEXTFIELD) {
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* EL_TARGET_LVGL */
|
||||
@@ -1,574 +0,0 @@
|
||||
/*
|
||||
* el_native_target.h — Native widget declarations for el programs targeting
|
||||
* native desktop UI (AppKit / GTK4 / Win32).
|
||||
*
|
||||
* This header is designed to be included AFTER el_runtime.h without conflict:
|
||||
* - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h
|
||||
* static inlines.
|
||||
* - It does NOT redeclare __println, __print, or other functions whose
|
||||
* return types differ between el_seed.h and el_runtime.h.
|
||||
* - It adds: native widget builtins + float arithmetic helpers that the
|
||||
* current el_runtime.h omits but elc still emits calls to.
|
||||
*
|
||||
* Usage:
|
||||
* Inject via -include at compile time, OR #include it after el_runtime.h.
|
||||
*
|
||||
* clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ...
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* el_val_t must already be defined by el_runtime.h or el_seed.h. */
|
||||
#ifndef EL_VAL_T_DEFINED
|
||||
typedef int64_t el_val_t;
|
||||
#endif
|
||||
|
||||
/* ── Float arithmetic helpers ───────────────────────────────────────────────
|
||||
* elc emits calls to float_div / float_mul etc. for Float-typed expressions.
|
||||
* These were in el_runtime.c through v1.0.0-20260501 but are missing from the
|
||||
* current el_runtime.h. Redeclared here as static inline to avoid link deps.
|
||||
* Only defined if not already declared (old runtimes that still have them). */
|
||||
|
||||
#ifndef EL_FLOAT_OPS_DEFINED
|
||||
#define EL_FLOAT_OPS_DEFINED
|
||||
|
||||
/* el_to_float / el_from_float — bit-cast between el_val_t and double.
|
||||
* Defined as static inline in both el_runtime.h and el_seed.h; we do NOT
|
||||
* redefine them here. We rely on one of those headers being included first. */
|
||||
|
||||
static inline el_val_t float_div(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b;
|
||||
ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_mul(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d * ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_add(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d + ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_sub(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d - ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_lt(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d < ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_gt(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d > ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_lte(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d <= ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_gte(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d >= ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_eq(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d == ub.d);
|
||||
}
|
||||
|
||||
#endif /* EL_FLOAT_OPS_DEFINED */
|
||||
|
||||
/* ── Native widget system (macOS AppKit) ────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid. */
|
||||
|
||||
#ifdef EL_TARGET_MACOS
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_set_data(el_val_t handle, el_val_t data_str);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_MACOS */
|
||||
|
||||
/* ── Native widget system (Linux GTK4) ──────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
* All functions have the same signatures as EL_TARGET_MACOS above. */
|
||||
|
||||
#ifdef EL_TARGET_LINUX
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader — same JSON output as EL_TARGET_MACOS */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_LINUX */
|
||||
|
||||
/* ── Native widget system (Windows Win32) ───────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
* Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */
|
||||
|
||||
#ifdef EL_TARGET_WIN32
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_WIN32 */
|
||||
|
||||
/* ── Native widget system (iOS UIKit) ───────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* iOS lifecycle note: UIApplicationMain never returns. The el program must
|
||||
* store its UI-build logic in a void(*)(void) function pointer, assign it to
|
||||
* el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes
|
||||
* el_main_entry_fn inside didFinishLaunchingWithOptions.
|
||||
* Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */
|
||||
|
||||
#ifdef EL_TARGET_IOS
|
||||
|
||||
/* Lifecycle entry-function hook — set before calling __native_run_loop. */
|
||||
extern void (*el_main_entry_fn)(void);
|
||||
|
||||
/* Forward argc/argv from main() to UIApplicationMain. */
|
||||
void el_uikit_set_args(int argc, char** argv);
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_IOS */
|
||||
|
||||
/* ── Native widget system (Android JNI) ─────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_ANDROID and linked with
|
||||
* libelruntime.so (which includes el_android.c compiled by the NDK build).
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Java companion: ElBridge.java (package com.neuron.el) must be compiled into
|
||||
* the APK. The Activity must call ElBridge.init(this) before any widget ops.
|
||||
*
|
||||
* Link flags (in Android.mk or CMakeLists.txt):
|
||||
* -landroid -llog -ldl */
|
||||
|
||||
#ifdef EL_TARGET_ANDROID
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void); /* no-op on Android */
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_ANDROID */
|
||||
|
||||
/* ── Native widget system (LVGL v9 — embedded / microcontroller) ─────────────
|
||||
* Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c
|
||||
* and the LVGL library (lvgl.a or lvgl source tree).
|
||||
*
|
||||
* Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+
|
||||
* RAM and an LVGL-compatible display driver. No OS required.
|
||||
*
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Bare-metal / no dynamic linker:
|
||||
* Compile with -DEL_LVGL_NO_DLSYM and provide:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
*
|
||||
* Compile:
|
||||
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
* # Then link with lvgl.a. */
|
||||
|
||||
#ifdef EL_TARGET_LVGL
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader — same JSON output as all other native targets */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_LVGL */
|
||||
|
||||
/* ── Native widget system (SDL2 — embedded / Pi) ────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer
|
||||
* and SDL2 available. No GTK, no desktop environment required.
|
||||
*
|
||||
* Compile:
|
||||
* gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o
|
||||
* Link:
|
||||
* $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */
|
||||
|
||||
#ifdef EL_TARGET_SDL2
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_SDL2 */
|
||||
@@ -1,187 +0,0 @@
|
||||
#ifndef EL_PLATFORM_WIN_H
|
||||
#define EL_PLATFORM_WIN_H
|
||||
/*
|
||||
* el_platform_win.h — Windows OS-boundary shim for el_runtime.c.
|
||||
*
|
||||
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
|
||||
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
|
||||
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
|
||||
*
|
||||
* What it maps:
|
||||
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
|
||||
* Sockets close with closesocket() (see el_closesocket), and the stack must be
|
||||
* started once with WSAStartup — done automatically via a load-time constructor.
|
||||
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
|
||||
* exported by the main module. Windows equivalent: GetProcAddress on the process
|
||||
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
|
||||
* - popen : mapped to _popen/_pclose.
|
||||
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
|
||||
*/
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <process.h>
|
||||
|
||||
/* Portable headers mingw-w64 provides (verified present). */
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp */
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ── socket close ─────────────────────────────────────────────────────────── */
|
||||
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
|
||||
build defines the same helper as close() so the call sites are identical across platforms. */
|
||||
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
|
||||
|
||||
/* ── setsockopt optval type ───────────────────────────────────────────────── */
|
||||
/* Winsock's setsockopt takes optval as (const char*); POSIX takes (const void*), so el_runtime.c
|
||||
passes &int directly. GCC 14+ makes that an error under -Wincompatible-pointer-types. Wrap it so
|
||||
the runtime's POSIX-style call sites compile unchanged (defined before the macro so the wrapper
|
||||
itself resolves to the real winsock setsockopt). */
|
||||
static inline int el_setsockopt(SOCKET s, int level, int optname, const void* optval, int optlen) {
|
||||
return setsockopt(s, level, optname, (const char*)optval, optlen);
|
||||
}
|
||||
#define setsockopt(s, l, o, v, n) el_setsockopt((s), (l), (o), (v), (int)(n))
|
||||
|
||||
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
|
||||
static void el__win_net_init(void) {
|
||||
static int inited = 0;
|
||||
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
|
||||
}
|
||||
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
|
||||
|
||||
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
|
||||
#ifndef RTLD_DEFAULT
|
||||
#define RTLD_DEFAULT ((void*)0)
|
||||
#endif
|
||||
static inline void* el_win_dlsym(void* handle, const char* name) {
|
||||
(void)handle;
|
||||
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
|
||||
}
|
||||
#define dlsym(h, n) el_win_dlsym((h), (n))
|
||||
|
||||
/* ── popen / pclose ───────────────────────────────────────────────────────── */
|
||||
#define popen _popen
|
||||
#define pclose _pclose
|
||||
|
||||
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
|
||||
#include <direct.h> /* _mkdir */
|
||||
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
|
||||
#define timegm _mkgmtime /* UTC tm → time_t */
|
||||
#define fsync(fd) _commit(fd) /* no fsync() on Windows; _commit() (<io.h>) is the equiv */
|
||||
|
||||
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
|
||||
static inline int setenv(const char* name, const char* value, int overwrite) {
|
||||
(void)overwrite;
|
||||
return _putenv_s(name, value ? value : "");
|
||||
}
|
||||
static inline int unsetenv(const char* name) {
|
||||
/* _putenv_s(name, "") sets VAR="" rather than removing it.
|
||||
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
|
||||
* env block; then we sync the CRT cache with _putenv("NAME="). */
|
||||
SetEnvironmentVariableA(name, NULL);
|
||||
size_t len = strlen(name);
|
||||
char *buf = (char*)malloc(len + 2);
|
||||
if (!buf) return -1;
|
||||
memcpy(buf, name, len);
|
||||
buf[len] = '=';
|
||||
buf[len + 1] = '\0';
|
||||
_putenv(buf);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
|
||||
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
|
||||
(void)rem;
|
||||
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
|
||||
Sleep(ms ? ms : 1);
|
||||
return 0;
|
||||
}
|
||||
#define nanosleep(req, rem) el_nanosleep((req), (rem))
|
||||
|
||||
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
|
||||
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
|
||||
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||
}
|
||||
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
|
||||
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||
}
|
||||
|
||||
/* ── libcurl: degradable stubs for the curl-less Windows build ─────────────── */
|
||||
/* The curl-less validation build (WITH_CURL=0) links no libcurl. el_runtime.c uses libcurl
|
||||
* unconditionally for its HTTP client / LLM layer; these stubs let it compile and link so the
|
||||
* runtime, HTTP *server*, graph and memory work natively on Windows. Live outbound HTTP/LLM calls
|
||||
* degrade to a runtime error (curl_easy_perform returns an error) — matching the documented
|
||||
* curl-less contract. When HAVE_CURL is defined (WITH_CURL=1) the real <curl/curl.h> is used and
|
||||
* this whole block is compiled out. POSIX never sees this header, so the POSIX build is untouched. */
|
||||
#ifndef HAVE_CURL
|
||||
|
||||
typedef void CURL;
|
||||
typedef int CURLcode;
|
||||
|
||||
#define CURLE_OK 0
|
||||
#define CURLE_HTTP_RETURNED_ERROR 22
|
||||
#define CURL_ERROR_SIZE 256
|
||||
|
||||
/* Option ids: values are irrelevant to the no-op setopt below; kept distinct for readability. */
|
||||
#define CURLOPT_URL 10002
|
||||
#define CURLOPT_WRITEFUNCTION 20011
|
||||
#define CURLOPT_WRITEDATA 10001
|
||||
#define CURLOPT_POSTFIELDS 10015
|
||||
#define CURLOPT_POSTFIELDSIZE 120
|
||||
#define CURLOPT_POST 47
|
||||
#define CURLOPT_HTTPHEADER 10023
|
||||
#define CURLOPT_TIMEOUT_MS 155
|
||||
#define CURLOPT_NOSIGNAL 99
|
||||
#define CURLOPT_USERAGENT 10018
|
||||
#define CURLOPT_FOLLOWLOCATION 52
|
||||
#define CURLOPT_ERRORBUFFER 10010
|
||||
#define CURLOPT_CUSTOMREQUEST 10036
|
||||
#define CURLOPT_FAILONERROR 45
|
||||
|
||||
struct curl_slist { char* data; struct curl_slist* next; };
|
||||
|
||||
static inline struct curl_slist* curl_slist_append(struct curl_slist* list, const char* s) {
|
||||
struct curl_slist* node = (struct curl_slist*)malloc(sizeof(struct curl_slist));
|
||||
if (!node) return list;
|
||||
node->data = s ? strdup(s) : NULL;
|
||||
node->next = NULL;
|
||||
if (!list) return node;
|
||||
struct curl_slist* p = list;
|
||||
while (p->next) p = p->next;
|
||||
p->next = node;
|
||||
return list;
|
||||
}
|
||||
static inline void curl_slist_free_all(struct curl_slist* list) {
|
||||
while (list) { struct curl_slist* n = list->next; free(list->data); free(list); list = n; }
|
||||
}
|
||||
|
||||
static inline CURL* curl_easy_init(void) { return (CURL*)malloc(1); }
|
||||
static inline CURLcode curl_easy_setopt(CURL* h, int opt, ...) { (void)h; (void)opt; return CURLE_OK; }
|
||||
static inline CURLcode curl_easy_perform(CURL* h) { (void)h; return 7 /* CURLE_COULDNT_CONNECT */; }
|
||||
static inline void curl_easy_cleanup(CURL* h) { free(h); }
|
||||
static inline const char* curl_easy_strerror(CURLcode c) {
|
||||
(void)c; return "libcurl not built in (curl-less build)";
|
||||
}
|
||||
|
||||
#endif /* !HAVE_CURL */
|
||||
|
||||
#endif /* EL_PLATFORM_WIN_H */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3
-74
@@ -6,55 +6,15 @@
|
||||
//
|
||||
// Dependencies: runtime/string.el, runtime/json.el
|
||||
|
||||
// --- Validation (defense in depth) ---
|
||||
// el_val_t is an untyped machine word, so a wrong TYPE can't be caught here — but a
|
||||
// wrong VALUE can (a tier in the node_type slot, an empty/garbage string, an int, a
|
||||
// path, a model name, a cgi id). Reject loudly instead of silently writing junk.
|
||||
|
||||
fn engram_valid_node_type(t: String) -> Bool {
|
||||
return str_eq(t, "Memory") || str_eq(t, "Knowledge") || str_eq(t, "Belief")
|
||||
|| str_eq(t, "Project") || str_eq(t, "Tag") || str_eq(t, "BacklogItem")
|
||||
|| str_eq(t, "Artifact") || str_eq(t, "Conversation") || str_eq(t, "ExecutionContext")
|
||||
|| str_eq(t, "InternalStateEvent") || str_eq(t, "Self") || str_eq(t, "Entity")
|
||||
|| str_eq(t, "Process") || str_eq(t, "ConfigEntry") || str_eq(t, "Concept") || str_eq(t, "Imprint")
|
||||
|| str_eq(t, "SessionSummary")
|
||||
}
|
||||
|
||||
fn engram_valid_tier(t: String) -> Bool {
|
||||
return str_eq(t, "Semantic") || str_eq(t, "Episodic") || str_eq(t, "Working")
|
||||
|| str_eq(t, "Procedural") || str_eq(t, "Canonical") || str_eq(t, "Note") || str_eq(t, "Lesson")
|
||||
}
|
||||
|
||||
// --- Node creation ---
|
||||
|
||||
fn engram_node(content: String, node_type: String, salience: Float) -> String {
|
||||
if !engram_valid_node_type(node_type) {
|
||||
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "'")
|
||||
return ""
|
||||
}
|
||||
return __engram_node(content, node_type, salience)
|
||||
}
|
||||
|
||||
// Signature MUST match the C primitive __engram_node_full exactly (el_seed.h):
|
||||
// (content, node_type, label, salience, importance, confidence, tier, tags)
|
||||
// The previous wrapper declared a stale 8-arg schema with wrong names AND types
|
||||
// (sal:Float at the label slot, ts:Int at the tier slot). Because el_val_t is an
|
||||
// untyped machine word, the EL compiler coerced caller args to those wrong param
|
||||
// types and then forwarded them BY POSITION into the C function — so tier received
|
||||
// an int, importance/confidence received strings, label received a float, etc.
|
||||
// That is the field-corruption bug. Match the contract 1:1 — no coercion, no reorder.
|
||||
fn engram_node_full(content: String, node_type: String, label: String,
|
||||
salience: Float, importance: Float, confidence: Float,
|
||||
tier: String, tags: String) -> String {
|
||||
if !engram_valid_node_type(node_type) {
|
||||
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "' (label=" + label + ")")
|
||||
return ""
|
||||
}
|
||||
if !engram_valid_tier(tier) {
|
||||
__println("[engram] REJECTED node write — invalid tier '" + tier + "' (node_type=" + node_type + ", label=" + label + ")")
|
||||
return ""
|
||||
}
|
||||
return __engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
|
||||
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
|
||||
source: String, lang: String, ts: Int, tags: String) -> String {
|
||||
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
|
||||
}
|
||||
|
||||
// --- Node retrieval ---
|
||||
@@ -119,37 +79,6 @@ fn engram_activate_json(query: String, limit: Int) -> String {
|
||||
return __engram_activate_json(query, limit)
|
||||
}
|
||||
|
||||
// --- Geometry operators (§5) ---
|
||||
// Relational-neighborhood algebra over centered engram descriptors. Each takes
|
||||
// comma-separated seed-id set(s); a centered neighborhood is grown from those
|
||||
// seeds (ad-hoc NOCACHE path) and the operator's JSON result is returned.
|
||||
// Delegates to the native __engram_geo_*_json seeds (el_seed.c); in the heavy
|
||||
// engram runtime the same bare names resolve directly to el_runtime.c symbols.
|
||||
|
||||
fn engram_geo_descriptor_json(seeds: String) -> String {
|
||||
return __engram_geo_descriptor_json(seeds)
|
||||
}
|
||||
|
||||
fn engram_geo_overlap_json(a_seeds: String, b_seeds: String) -> String {
|
||||
return __engram_geo_overlap_json(a_seeds, b_seeds)
|
||||
}
|
||||
|
||||
fn engram_geo_subtract_json(a_seeds: String, b_seeds: String, mode: String) -> String {
|
||||
return __engram_geo_subtract_json(a_seeds, b_seeds, mode)
|
||||
}
|
||||
|
||||
fn engram_geo_combine_json(a_seeds: String, b_seeds: String) -> String {
|
||||
return __engram_geo_combine_json(a_seeds, b_seeds)
|
||||
}
|
||||
|
||||
fn engram_geo_distance_json(a_seeds: String, b_seeds: String) -> String {
|
||||
return __engram_geo_distance_json(a_seeds, b_seeds)
|
||||
}
|
||||
|
||||
fn engram_geo_analogy_json(a_seeds: String, b_seeds: String) -> String {
|
||||
return __engram_geo_analogy_json(a_seeds, b_seeds)
|
||||
}
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
fn generate(form: String) -> String {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user