Compare commits
72 Commits
feat/el-speaks
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 0032e8f8c4 | |||
| daad2fa2a0 | |||
| 9e4160f279 | |||
| 4aadd9300e | |||
| 44e2e17973 | |||
| 511db25230 | |||
| e9eac46be1 | |||
| aa570b6899 | |||
| 98da70f650 | |||
| d1489a2568 | |||
| 923f6a4bed | |||
| f1a7e224a7 | |||
| c6ba0677f0 | |||
| 3049a70837 | |||
| 9a6c161ba9 | |||
| cb7289f065 | |||
| 6c975b1d50 | |||
| 1086ac9658 | |||
| f23cb2b948 | |||
| 79f6cb7985 | |||
| bb040ad2c8 | |||
| 97f741e9c2 | |||
| c48db6c2a8 | |||
| 93aa96cfaf | |||
| 067dd40317 | |||
| 0143cc458a | |||
| 15d4352bac | |||
| 2fcc1c287c | |||
| 505e5e74d9 | |||
| cbef1c1ebb | |||
| 50425f375d | |||
| e8e25a07b4 | |||
| e01e079bda | |||
| d2d89fcb60 | |||
| d9e301be6d | |||
| 9cc6040df2 | |||
| 29f78f9f67 | |||
| c2d9596e76 | |||
| 60c07ad784 | |||
| c741cfe928 | |||
| c04d68f9ce | |||
| bc2f26ddfc | |||
| b40754f07b | |||
| 285166c25c | |||
| 24f7fb5143 | |||
| 8bbb750c2c | |||
| 28d19da7f1 | |||
| 886626a64e | |||
| 82e998273b | |||
| 35b07bade2 | |||
| 1b324a071f | |||
| 7d01608a9d | |||
| 2bed8483f7 | |||
| 4f7568b07f | |||
| 60737b0305 | |||
| 5718943f2e | |||
| dcaa77d77b | |||
| 409bf57341 | |||
| b212e9443c | |||
| addd51209f | |||
| 9a13547fe2 | |||
| 481badf1d1 | |||
| b92ec92c48 | |||
| e1bc6fe944 | |||
| eb13dace9c | |||
| 8c2406ff6b | |||
| 99ef855b98 | |||
| 45325f7391 | |||
| c23c5112a7 | |||
| b40af4a5f3 | |||
| c7a78ab1eb | |||
| 8c94d92033 |
@@ -19,6 +19,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Guards must run from the REPO ROOT — override the job's
|
||||||
|
# defaults.run.working-directory: lang
|
||||||
|
- name: Guard - single canonical runtime source
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-single-runtime.sh
|
||||||
|
|
||||||
|
- name: Guard - el_runtime.c growth budget
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-runtime-growth.sh
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -41,7 +51,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elc-gen2.c \
|
dist/elc-gen2.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/platform/elc
|
-o dist/platform/elc
|
||||||
chmod +x dist/platform/elc
|
chmod +x dist/platform/elc
|
||||||
@@ -56,7 +66,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elb.c \
|
dist/elb.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/bin/elb
|
-o dist/bin/elb
|
||||||
chmod +x dist/bin/elb
|
chmod +x dist/bin/elb
|
||||||
@@ -87,14 +97,20 @@ jobs:
|
|||||||
bash tests/html_sanitizer/run.sh
|
bash tests/html_sanitizer/run.sh
|
||||||
|
|
||||||
# Native El test suites (elc --test, compile-link-run)
|
# Native El test suites (elc --test, compile-link-run)
|
||||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
# The runtime is MULTI-FILE (see lang/runtime/SOURCES). Every .c is compiled
|
||||||
- name: Precompile el_runtime.o
|
# once into /tmp/libel.a and reused by all 8 test modules — compile-once,
|
||||||
|
# link-many, as prescribed in DESIGN.md. Linking el_runtime.c alone fails
|
||||||
|
# at `ld`: it calls into all six engram sibling TUs.
|
||||||
|
- name: Precompile runtime into libel.a
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
rm -rf /tmp/elrt && mkdir -p /tmp/elrt
|
||||||
-o /tmp/el_runtime.o
|
for src in $(../scripts/el-runtime-sources.sh --check "$RUNTIME"); do
|
||||||
echo "el_runtime.o compiled"
|
gcc -O2 -c -I "$RUNTIME" "$src" -o "/tmp/elrt/$(basename "${src%.c}").o"
|
||||||
|
done
|
||||||
|
ar rcs /tmp/libel.a /tmp/elrt/*.o
|
||||||
|
echo "libel.a built from $(ls /tmp/elrt/*.o | wc -l) translation units"
|
||||||
|
|
||||||
- name: Run tests - native (core)
|
- name: Run tests - native (core)
|
||||||
run: |
|
run: |
|
||||||
@@ -102,7 +118,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||||
/tmp/el_native_core
|
/tmp/el_native_core
|
||||||
|
|
||||||
@@ -112,7 +128,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||||
/tmp/el_native_text
|
/tmp/el_native_text
|
||||||
|
|
||||||
@@ -122,7 +138,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||||
/tmp/el_native_string
|
/tmp/el_native_string
|
||||||
|
|
||||||
@@ -132,7 +148,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||||
/tmp/el_native_math
|
/tmp/el_native_math
|
||||||
|
|
||||||
@@ -142,7 +158,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||||
/tmp/el_native_state
|
/tmp/el_native_state
|
||||||
|
|
||||||
@@ -152,7 +168,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||||
/tmp/el_native_time
|
/tmp/el_native_time
|
||||||
|
|
||||||
@@ -162,7 +178,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||||
/tmp/el_native_json
|
/tmp/el_native_json
|
||||||
|
|
||||||
@@ -172,7 +188,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||||
/tmp/el_native_env
|
/tmp/el_native_env
|
||||||
|
|
||||||
@@ -182,7 +198,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/libel.a \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||||
/tmp/el_native_fs
|
/tmp/el_native_fs
|
||||||
|
|
||||||
@@ -306,8 +322,9 @@ jobs:
|
|||||||
FROM ${BASE}
|
FROM ${BASE}
|
||||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||||
|
COPY runtime/ /opt/el/runtime/
|
||||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Source branch check passed: ${SOURCE} -> stage"
|
echo "Source branch check passed: ${SOURCE} -> stage"
|
||||||
|
|
||||||
|
# Guards must run from the REPO ROOT — override the job's
|
||||||
|
# defaults.run.working-directory: lang
|
||||||
|
- name: Guard - single canonical runtime source
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-single-runtime.sh
|
||||||
|
|
||||||
|
- name: Guard - el_runtime.c growth budget
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-runtime-growth.sh
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -48,7 +58,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elc-gen2.c \
|
dist/elc-gen2.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/platform/elc
|
-o dist/platform/elc
|
||||||
chmod +x dist/platform/elc
|
chmod +x dist/platform/elc
|
||||||
@@ -86,7 +96,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||||
/tmp/el_native_core
|
/tmp/el_native_core
|
||||||
|
|
||||||
@@ -96,7 +106,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||||
/tmp/el_native_text
|
/tmp/el_native_text
|
||||||
|
|
||||||
@@ -106,7 +116,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||||
/tmp/el_native_string
|
/tmp/el_native_string
|
||||||
|
|
||||||
@@ -116,7 +126,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||||
/tmp/el_native_math
|
/tmp/el_native_math
|
||||||
|
|
||||||
@@ -126,7 +136,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||||
/tmp/el_native_state
|
/tmp/el_native_state
|
||||||
|
|
||||||
@@ -136,7 +146,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||||
/tmp/el_native_time
|
/tmp/el_native_time
|
||||||
|
|
||||||
@@ -146,7 +156,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||||
/tmp/el_native_json
|
/tmp/el_native_json
|
||||||
|
|
||||||
@@ -156,7 +166,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||||
/tmp/el_native_env
|
/tmp/el_native_env
|
||||||
|
|
||||||
@@ -166,7 +176,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||||
/tmp/el_native_fs
|
/tmp/el_native_fs
|
||||||
|
|
||||||
@@ -178,7 +188,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elb.c \
|
dist/elb.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/bin/elb
|
-o dist/bin/elb
|
||||||
chmod +x dist/bin/elb
|
chmod +x dist/bin/elb
|
||||||
@@ -290,8 +300,9 @@ jobs:
|
|||||||
FROM ${BASE}
|
FROM ${BASE}
|
||||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||||
|
COPY runtime/ /opt/el/runtime/
|
||||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Source branch check passed: ${SOURCE} -> main"
|
echo "Source branch check passed: ${SOURCE} -> main"
|
||||||
|
|
||||||
|
# Guards must run from the REPO ROOT — override the job's
|
||||||
|
# defaults.run.working-directory: lang
|
||||||
|
- name: Guard - single canonical runtime source
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-single-runtime.sh
|
||||||
|
|
||||||
|
- name: Guard - el_runtime.c growth budget
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: bash scripts/check-runtime-growth.sh
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -49,7 +59,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elc-gen2.c \
|
dist/elc-gen2.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/platform/elc
|
-o dist/platform/elc
|
||||||
chmod +x dist/platform/elc
|
chmod +x dist/platform/elc
|
||||||
@@ -64,7 +74,7 @@ jobs:
|
|||||||
gcc -O2 \
|
gcc -O2 \
|
||||||
-I runtime \
|
-I runtime \
|
||||||
dist/elb.c \
|
dist/elb.c \
|
||||||
runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/bin/elb
|
-o dist/bin/elb
|
||||||
chmod +x dist/bin/elb
|
chmod +x dist/bin/elb
|
||||||
@@ -123,7 +133,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||||
/tmp/el_native_core
|
/tmp/el_native_core
|
||||||
|
|
||||||
@@ -133,7 +143,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||||
/tmp/el_native_text
|
/tmp/el_native_text
|
||||||
|
|
||||||
@@ -143,7 +153,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||||
/tmp/el_native_string
|
/tmp/el_native_string
|
||||||
|
|
||||||
@@ -153,7 +163,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||||
/tmp/el_native_math
|
/tmp/el_native_math
|
||||||
|
|
||||||
@@ -163,7 +173,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||||
/tmp/el_native_state
|
/tmp/el_native_state
|
||||||
|
|
||||||
@@ -173,7 +183,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||||
/tmp/el_native_time
|
/tmp/el_native_time
|
||||||
|
|
||||||
@@ -183,7 +193,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||||
/tmp/el_native_json
|
/tmp/el_native_json
|
||||||
|
|
||||||
@@ -193,7 +203,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||||
/tmp/el_native_env
|
/tmp/el_native_env
|
||||||
|
|
||||||
@@ -203,7 +213,7 @@ jobs:
|
|||||||
ELC="$(pwd)/dist/platform/elc"
|
ELC="$(pwd)/dist/platform/elc"
|
||||||
RUNTIME="$(pwd)/runtime"
|
RUNTIME="$(pwd)/runtime"
|
||||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||||
/tmp/el_native_fs
|
/tmp/el_native_fs
|
||||||
|
|
||||||
@@ -216,10 +226,17 @@ jobs:
|
|||||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||||
cp lang/runtime/el_runtime.c dist/sdk/runtime/
|
# Ship the WHOLE runtime link set, not el_runtime.c alone. el_runtime.c
|
||||||
cp lang/runtime/el_runtime.h dist/sdk/runtime/
|
# #includes six engram headers and calls into all six sibling .c files,
|
||||||
cp lang/runtime/engram_store.c dist/sdk/runtime/
|
# so an SDK carrying only el_runtime.c{,.h} + engram_store.c{,.h} cannot
|
||||||
cp lang/runtime/engram_store.h dist/sdk/runtime/
|
# link — downstream `ld` fails on engram_ground_json, eg_find_relation,
|
||||||
|
# cog_assert_two_axis and friends. lang/runtime/SOURCES is the source of
|
||||||
|
# truth; --check makes a missing file fail the release loudly.
|
||||||
|
for f in $(scripts/el-runtime-sources.sh --check) \
|
||||||
|
$(scripts/el-runtime-sources.sh --headers --check); do
|
||||||
|
cp "lang/runtime/${f}" dist/sdk/runtime/
|
||||||
|
done
|
||||||
|
cp lang/runtime/SOURCES dist/sdk/runtime/
|
||||||
cp lang/runtime/*.el dist/sdk/runtime/
|
cp lang/runtime/*.el dist/sdk/runtime/
|
||||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||||
@@ -274,12 +291,16 @@ jobs:
|
|||||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Per-file assets (downstream CI needs these individually)
|
# Per-file assets (downstream CI needs these individually).
|
||||||
|
# lang/install.sh downloads every one of these by name — the list is
|
||||||
|
# lang/runtime/SOURCES. Shipping el_runtime.c alone produced a lib/
|
||||||
|
# that could not link; that is the bug this loop closes.
|
||||||
upload_asset lang/dist/platform/elc elc
|
upload_asset lang/dist/platform/elc elc
|
||||||
upload_asset lang/runtime/el_runtime.c el_runtime.c
|
for f in $(scripts/el-runtime-sources.sh --check) \
|
||||||
upload_asset lang/runtime/el_runtime.h el_runtime.h
|
$(scripts/el-runtime-sources.sh --headers --check); do
|
||||||
upload_asset lang/runtime/engram_store.c engram_store.c
|
upload_asset "lang/runtime/${f}" "${f}"
|
||||||
upload_asset lang/runtime/engram_store.h engram_store.h
|
done
|
||||||
|
upload_asset lang/runtime/SOURCES SOURCES
|
||||||
|
|
||||||
# SDK bundle and installer binary
|
# SDK bundle and installer binary
|
||||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||||
@@ -350,6 +371,26 @@ jobs:
|
|||||||
--version="${VERSION}" \
|
--version="${VERSION}" \
|
||||||
--source=runtime/el_runtime.js
|
--source=runtime/el_runtime.js
|
||||||
|
|
||||||
|
# el-runtime-src — the COMPLETE runtime link set as one tarball.
|
||||||
|
#
|
||||||
|
# The el-runtime-c / el-runtime-h packages above are single files and are
|
||||||
|
# kept for backward compatibility with consumers that already pull them,
|
||||||
|
# but they are NOT sufficient to link: el_runtime.c calls into six engram
|
||||||
|
# sibling translation units. New consumers should pull el-runtime-src and
|
||||||
|
# link everything named in its SOURCES file.
|
||||||
|
tar -czf /tmp/el-runtime-src.tar.gz \
|
||||||
|
-C runtime SOURCES \
|
||||||
|
$(../scripts/el-runtime-sources.sh --check) \
|
||||||
|
$(../scripts/el-runtime-sources.sh --headers --check)
|
||||||
|
|
||||||
|
gcloud artifacts generic upload \
|
||||||
|
--repository=foundation-prod \
|
||||||
|
--location=us-central1 \
|
||||||
|
--project=neuron-785695 \
|
||||||
|
--package=el-runtime-src \
|
||||||
|
--version="${VERSION}" \
|
||||||
|
--source=/tmp/el-runtime-src.tar.gz
|
||||||
|
|
||||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||||
# Keep key alive for the ci-base rebuild step below
|
# Keep key alive for the ci-base rebuild step below
|
||||||
# (deleted in that step after docker push)
|
# (deleted in that step after docker push)
|
||||||
@@ -386,8 +427,9 @@ jobs:
|
|||||||
FROM ${BASE}
|
FROM ${BASE}
|
||||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||||
|
COPY runtime/ /opt/el/runtime/
|
||||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
+42
-3
@@ -9,13 +9,52 @@ LANG_DIR="$ROOT/lang"
|
|||||||
RUNTIME="$LANG_DIR/runtime"
|
RUNTIME="$LANG_DIR/runtime"
|
||||||
ELC="$LANG_DIR/dist/platform/elc"
|
ELC="$LANG_DIR/dist/platform/elc"
|
||||||
|
|
||||||
|
# Runtime guards — catch drift and growth before they are committed, not in CI.
|
||||||
|
# check-single-runtime.sh : el_runtime.c must not be FORKED (a lagging copy
|
||||||
|
# shipped to prod and dropped learned hebb edges).
|
||||||
|
# check-runtime-growth.sh : el_runtime.c must not GROW (it is a 2026-05-03
|
||||||
|
# build shim that was never retired; see BUDGET).
|
||||||
|
echo "→ Runtime guards..."
|
||||||
|
bash "$ROOT/scripts/check-single-runtime.sh"
|
||||||
|
bash "$ROOT/scripts/check-runtime-growth.sh"
|
||||||
|
|
||||||
# If elc isn't built yet, skip with a warning rather than blocking
|
# If elc isn't built yet, skip with a warning rather than blocking
|
||||||
if [ ! -x "$ELC" ]; then
|
if [ ! -x "$ELC" ]; then
|
||||||
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
|
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: see 'Rebuilding the Compiler' in lang/AGENTS.md"
|
||||||
|
echo " (link \$($ROOT/scripts/el-runtime-sources.sh $RUNTIME) — NOT el_runtime.c alone)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This hook used to link
|
||||||
|
# "$RUNTIME/el_runtime.c" alone with stderr sent to /dev/null — so once
|
||||||
|
# el_runtime.c started calling into the engram siblings, every native test
|
||||||
|
# reported as FAILED with the real `ld` error invisible. Build the whole set
|
||||||
|
# once into an archive, then link each test against it.
|
||||||
|
|
||||||
|
# macOS: Homebrew openssl@3 is not on the default include/lib search path, so
|
||||||
|
# without these the link fails on -lssl/-lcrypto. Empty on Linux/CI.
|
||||||
|
SSL_INC=""
|
||||||
|
SSL_LIB=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && OSSL="$(brew --prefix openssl@3 2>/dev/null)" && [ -n "$OSSL" ]; then
|
||||||
|
SSL_INC="-I$OSSL/include"
|
||||||
|
SSL_LIB="-L$OSSL/lib"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Building runtime (compile-once, link-many)..."
|
||||||
|
HOOK_LIB="/tmp/el_hook_libel.a"
|
||||||
|
HOOK_OBJ="/tmp/el_hook_obj"
|
||||||
|
rm -rf "$HOOK_OBJ" && mkdir -p "$HOOK_OBJ"
|
||||||
|
if ! for src in $("$ROOT/scripts/el-runtime-sources.sh" --check "$RUNTIME"); do
|
||||||
|
gcc -O2 -c -I "$RUNTIME" $SSL_INC "$src" -o "$HOOK_OBJ/$(basename "${src%.c}").o" || exit 1
|
||||||
|
done; then
|
||||||
|
echo "✗ Pre-commit failed: the runtime does not compile."
|
||||||
|
echo " Re-run without 2>/dev/null to see the error:"
|
||||||
|
echo " gcc -O2 -c -I $RUNTIME \$($ROOT/scripts/el-runtime-sources.sh $RUNTIME)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ar rcs "$HOOK_LIB" "$HOOK_OBJ"/*.o
|
||||||
|
|
||||||
echo "→ Running El native tests..."
|
echo "→ Running El native tests..."
|
||||||
PASS=0
|
PASS=0
|
||||||
FAIL=0
|
FAIL=0
|
||||||
@@ -27,8 +66,8 @@ for test_file in "$LANG_DIR"/tests/native/test_*.el; do
|
|||||||
tmp_bin="/tmp/el_hook_${name}"
|
tmp_bin="/tmp/el_hook_${name}"
|
||||||
|
|
||||||
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
|
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
|
||||||
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
|
&& gcc -O2 -I "$RUNTIME" $SSL_INC $SSL_LIB "$tmp_c" "$HOOK_LIB" \
|
||||||
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
-lcurl -lssl -lcrypto -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||||
&& "$tmp_bin" 2>/dev/null; then
|
&& "$tmp_bin" 2>/dev/null; then
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ peripheral/.consent.json
|
|||||||
peripheral/.resume.json
|
peripheral/.resume.json
|
||||||
peripheral/.engram/
|
peripheral/.engram/
|
||||||
peripheral/organ
|
peripheral/organ
|
||||||
|
|
||||||
|
# Claude Code session state
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -199,21 +199,35 @@ wrong, say so with a measurement rather than editing it.
|
|||||||
|
|
||||||
All build/test commands run from `lang/` unless noted. Grounded in `.gitea/workflows/sdk-release.yaml`, `lang/install.sh`, and `lang/AGENTS.md`.
|
All build/test commands run from `lang/` unless noted. Grounded in `.gitea/workflows/sdk-release.yaml`, `lang/install.sh`, and `lang/AGENTS.md`.
|
||||||
|
|
||||||
|
> ### The runtime is MULTI-FILE — never link `el_runtime.c` alone
|
||||||
|
>
|
||||||
|
> `lang/runtime/el_runtime.c` `#include`s six engram headers and makes hard cross-TU calls into all six sibling `.c` files. **Linking it by itself fails at `ld`** (undefined `engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, …). The canonical link set lives in exactly one place — **`lang/runtime/SOURCES`** — and is printed by `scripts/el-runtime-sources.sh`:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> scripts/el-runtime-sources.sh lang/runtime # ten .c files, in link order
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Use `$(scripts/el-runtime-sources.sh <runtime-dir>)` in every link line. Do not spell the list out longhand — it was written out in ~8 places, every copy drifted, and that is why the one-file link line below shipped broken for months. *(Corrected 2026-08-16.)*
|
||||||
|
|
||||||
**Self-host the compiler** (seed binary → gen2 elc):
|
**Self-host the compiler** (seed binary → gen2 elc):
|
||||||
```bash
|
```bash
|
||||||
cd lang
|
cd lang
|
||||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c # seed is the committed linux-amd64 binary
|
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c # seed is the committed linux-amd64 binary
|
||||||
gcc -O2 -I el-compiler/runtime dist/elc-gen2.c \
|
gcc -O2 -I runtime dist/elc-gen2.c \
|
||||||
el-compiler/runtime/el_runtime.c \
|
$(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||||
-o dist/platform/elc
|
-o dist/platform/elc
|
||||||
```
|
```
|
||||||
On macOS/arm64 the canonical local binary is `dist/platform/elc`; verify self-hosting by recompiling and `diff`ing the emitted `.c` (see `lang/AGENTS.md`). Note: `lang/AGENTS.md` says `el_seed.c` supersedes `el_runtime.c`, but the release workflow still links `el_runtime.c`/`.h` — treat `el_runtime.c` as the published runtime; reconcile which is canonical **(verify)**.
|
On macOS/arm64 the canonical local binary is `dist/platform/elc`; verify self-hosting by recompiling and `diff`ing the emitted `.c` (see `lang/AGENTS.md`).
|
||||||
|
|
||||||
|
*(Corrected 2026-08-16: this recipe compiled `el-compiler/runtime/el_runtime.c`. That path is a **lagging fork** — the "DO NOT EDIT" list at the top of this file names it as such. Building the canonical compiler from a known-stale fork was a live defect. It now uses `lang/runtime/`, the canonical source.)*
|
||||||
|
|
||||||
|
**Which runtime file is canonical — resolved.** *(This note previously read "`lang/AGENTS.md` says `el_seed.c` supersedes `el_runtime.c`, but the release workflow still links `el_runtime.c`/`.h` — reconcile which is canonical **(verify)**." It is now reconciled.)* **Neither supersedes the other; both ship, together with eight more.** `el_runtime.c` was created on 2026-05-03 as an explicitly temporary build shim — deleted that afternoon, restored 25 minutes later "UNTIL the compiler is updated to emit `#include el_seed.h`" — and the `until` never happened, so it grew to 20.5k lines. The end state remains a seed-only boundary (`elc` emitting `#include "el_seed.h"`, `elb` dropping its hardcoded runtime path); until that lands, **the canonical unit is the set in `lang/runtime/SOURCES`, not any one file.**
|
||||||
|
|
||||||
**Build `elb`** (build coordinator, the `.NET`-style incremental linker — compiles each module independently, no monolithic blobs):
|
**Build `elb`** (build coordinator, the `.NET`-style incremental linker — compiles each module independently, no monolithic blobs):
|
||||||
```bash
|
```bash
|
||||||
dist/platform/elc elb.el > dist/elb.c
|
dist/platform/elc elb.el > dist/elb.c
|
||||||
gcc -O2 -I el-compiler/runtime dist/elb.c el-compiler/runtime/el_runtime.c \
|
gcc -O2 -I runtime dist/elb.c $(../scripts/el-runtime-sources.sh runtime) \
|
||||||
-lcurl -lssl -lcrypto -lpthread -lm -o dist/bin/elb
|
-lcurl -lssl -lcrypto -lpthread -lm -o dist/bin/elb
|
||||||
```
|
```
|
||||||
`epm` and `el-install` are then built via `elb --clean --elc=… --runtime=… --out=…`.
|
`epm` and `el-install` are then built via `elb --clean --elc=… --runtime=… --out=…`.
|
||||||
@@ -221,10 +235,16 @@ gcc -O2 -I el-compiler/runtime dist/elb.c el-compiler/runtime/el_runtime.c \
|
|||||||
**Compile + run an El program:**
|
**Compile + run an El program:**
|
||||||
```bash
|
```bash
|
||||||
elc src/app.el > dist/app.c
|
elc src/app.el > dist/app.c
|
||||||
cc -std=c11 -O2 -I <lib>/el_runtime -o dist/app dist/app.c <lib>/el_runtime.c -lcurl -lpthread
|
cc -std=c11 -O2 -I <lib> -o dist/app dist/app.c \
|
||||||
|
<lib>/el_runtime.c <lib>/el_seed.c \
|
||||||
|
<lib>/engram_store.c <lib>/engram_vindex.c <lib>/engram_geometry.c \
|
||||||
|
<lib>/engram_reason.c <lib>/engram_verify.c <lib>/engram_cognition.c \
|
||||||
|
<lib>/eg_cosine_batch.c <lib>/eg_cosine_batch_strategy_cpu.c \
|
||||||
|
-lcurl -lssl -lcrypto -lpthread -lm
|
||||||
```
|
```
|
||||||
|
(Inside this repo, replace the file list with `$(scripts/el-runtime-sources.sh lang/runtime)`. `install.sh` installs all of these into `<lib>`.)
|
||||||
|
|
||||||
**Tests** — shell suites `bash tests/{text,calendar,time,html_sanitizer}/run.sh` (with `ELC=$(pwd)/dist/platform/elc EL_HOME=$(pwd)`), plus native suites via `elc --test tests/native/test_*.el` (core, text, string, math, state, time, json, env, fs) compiled and run against `el_runtime.c`.
|
**Tests** — shell suites `bash tests/{text,calendar,time,html_sanitizer}/run.sh` (with `ELC=$(pwd)/dist/platform/elc EL_HOME=$(pwd)`), plus native suites via `elc --test tests/native/test_*.el` (core, text, string, math, state, time, json, env, fs) compiled and run against the full runtime set.
|
||||||
|
|
||||||
**Publishing — how downstream gets the SDK.** On push to `main`, `sdk-release.yaml`:
|
**Publishing — how downstream gets the SDK.** On push to `main`, `sdk-release.yaml`:
|
||||||
1. Publishes a Gitea `latest` release with per-file assets `elc`, `el_runtime.c`, `el_runtime.h`, the SDK tarball, and `el-install`.
|
1. Publishes a Gitea `latest` release with per-file assets `elc`, `el_runtime.c`, `el_runtime.h`, the SDK tarball, and `el-install`.
|
||||||
|
|||||||
@@ -548,9 +548,13 @@ before `main` does anything. That is the dividend of discovery-precedes-executio
|
|||||||
|
|
||||||
```
|
```
|
||||||
# once, ever (or when the runtime/framework changes):
|
# once, ever (or when the runtime/framework changes):
|
||||||
cc -c el_runtime.c -o el_runtime.o
|
# The runtime is MULTI-FILE — compile every .c named in lang/runtime/SOURCES.
|
||||||
elc eltest.el > eltest.c && cc -c eltest.c -o eltest.o
|
# Linking el_runtime.c alone fails: it calls into the six engram sibling TUs.
|
||||||
ar rcs libeltest.a el_runtime.o eltest.o
|
for src in $(scripts/el-runtime-sources.sh lang/runtime); do
|
||||||
|
cc -c "$src" -o "obj/$(basename "${src%.c}").o"
|
||||||
|
done
|
||||||
|
elc eltest.el > eltest.c && cc -c eltest.c -o obj/eltest.o
|
||||||
|
ar rcs libeltest.a obj/*.o
|
||||||
|
|
||||||
# per suite:
|
# per suite:
|
||||||
elc --test foo_test.el > foo_test.c # registry + bodies only
|
elc --test foo_test.el > foo_test.c # registry + bodies only
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<title>Completing El</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--board:#f4f2ec; --board-line:#e2ded2; --ink:#1c1f26; --ink-soft:#4a5160;
|
||||||
|
--ink-faint:#8b8f9a; --rule:#d8d3c6; --card:#fbfaf6;
|
||||||
|
--red:#a8321e; --amber:#9a6a12; --green:#2f6b46; --blue:#1f4e79;
|
||||||
|
--accent:#1f4e79;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
:root:not([data-theme="light"]){
|
||||||
|
--board:#14161b; --board-line:#212530; --ink:#e8e6df; --ink-soft:#a8adb8;
|
||||||
|
--ink-faint:#6f7480; --rule:#2a2f3a; --card:#191c23;
|
||||||
|
--red:#e4785f; --amber:#d9a441; --green:#6fbf8e; --blue:#7fb2e0;
|
||||||
|
--accent:#7fb2e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--board:#14161b; --board-line:#212530; --ink:#e8e6df; --ink-soft:#a8adb8;
|
||||||
|
--ink-faint:#6f7480; --rule:#2a2f3a; --card:#191c23;
|
||||||
|
--red:#e4785f; --amber:#d9a441; --green:#6fbf8e; --blue:#7fb2e0;
|
||||||
|
--accent:#7fb2e0;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{
|
||||||
|
margin:0; background:var(--board); color:var(--ink);
|
||||||
|
font:16px/1.65 ui-serif,Georgia,"Iowan Old Style",Palatino,serif;
|
||||||
|
background-image:linear-gradient(var(--board-line) 1px,transparent 1px),
|
||||||
|
linear-gradient(90deg,var(--board-line) 1px,transparent 1px);
|
||||||
|
background-size:28px 28px;
|
||||||
|
}
|
||||||
|
.wrap{max-width:960px;margin:0 auto;padding:56px 24px 96px}
|
||||||
|
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||||
|
header{border-bottom:2px solid var(--ink);padding-bottom:18px;margin-bottom:8px}
|
||||||
|
h1{font-size:clamp(2rem,5vw,3rem);margin:0;letter-spacing:-.02em;text-wrap:balance}
|
||||||
|
.sub{color:var(--ink-soft);font-size:1.05rem;margin:10px 0 0}
|
||||||
|
.meta{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;
|
||||||
|
color:var(--ink-faint);text-transform:uppercase;letter-spacing:.09em;margin-top:14px}
|
||||||
|
h2{font-size:1.45rem;margin:52px 0 6px;letter-spacing:-.01em}
|
||||||
|
h2 .n{font-family:ui-monospace,monospace;font-size:.8rem;color:var(--accent);
|
||||||
|
display:block;letter-spacing:.12em;margin-bottom:4px;font-weight:400}
|
||||||
|
.lede{color:var(--ink-soft);margin:0 0 18px}
|
||||||
|
p{margin:0 0 14px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--rule);border-radius:3px;padding:20px 22px;margin:16px 0}
|
||||||
|
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||||
|
table{border-collapse:collapse;width:100%;font-size:.9rem;min-width:640px}
|
||||||
|
th{text-align:left;font-family:ui-monospace,monospace;font-size:.72rem;
|
||||||
|
text-transform:uppercase;letter-spacing:.09em;color:var(--ink-faint);
|
||||||
|
border-bottom:1px solid var(--ink);padding:0 12px 8px 0;font-weight:400}
|
||||||
|
td{padding:11px 12px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top}
|
||||||
|
td.f{font-weight:600;white-space:nowrap}
|
||||||
|
td.m{font-family:ui-monospace,monospace;font-size:.83rem;font-variant-numeric:tabular-nums}
|
||||||
|
.dead{color:var(--red);font-weight:600}
|
||||||
|
.part{color:var(--amber);font-weight:600}
|
||||||
|
.ok{color:var(--green);font-weight:600}
|
||||||
|
blockquote{margin:18px 0;padding:2px 0 2px 20px;border-left:3px solid var(--accent);
|
||||||
|
color:var(--ink-soft);font-style:italic}
|
||||||
|
ul{margin:0 0 14px;padding-left:22px} li{margin-bottom:9px}
|
||||||
|
.q{border-left:3px solid var(--amber);padding:14px 0 14px 20px;margin:18px 0}
|
||||||
|
.q b{display:block;font-size:1.05rem;margin-bottom:5px;font-style:normal}
|
||||||
|
.q span{color:var(--ink-soft);font-size:.94rem}
|
||||||
|
code{font-family:ui-monospace,monospace;font-size:.88em;background:var(--card);
|
||||||
|
border:1px solid var(--rule);border-radius:2px;padding:1px 5px}
|
||||||
|
hr{border:0;border-top:1px solid var(--rule);margin:44px 0}
|
||||||
|
.foot{color:var(--ink-faint);font-size:.86rem;margin-top:60px;
|
||||||
|
border-top:1px solid var(--rule);padding-top:18px}
|
||||||
|
.tag{display:inline-block;font-family:ui-monospace,monospace;font-size:.68rem;
|
||||||
|
letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--rule);
|
||||||
|
border-radius:2px;padding:2px 7px;color:var(--ink-faint);margin-left:8px;vertical-align:middle}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Completing El</h1>
|
||||||
|
<p class="sub">A working surface. Nothing here is settled, and none of the code is assumed right — El is self-hosting, so all of it can change and be rebuilt.</p>
|
||||||
|
<p class="meta">Whiteboard v0 · no sacred cows · not a plan, not a task list</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h2><span class="n">01</span>What we established</h2>
|
||||||
|
|
||||||
|
<p>El is a <b>concept-oriented language</b> — the first, and intended as the last, because every other family is oriented toward a <em>representation</em> of a concept rather than the concept. Procedures, objects, functions, predicates are the shapes concepts get flattened into. Once the primitive is the concept, there is no further rung.</p>
|
||||||
|
|
||||||
|
<p>Everything here is El. The engram is an El program, the soul is El, <code>elp</code> is El, ingest is El. Which gives the load-bearing consequence:</p>
|
||||||
|
|
||||||
|
<blockquote>A concept with no home in El does not disappear. It becomes C, or it becomes a convention.</blockquote>
|
||||||
|
|
||||||
|
<p>Both are measurable, and both were measured. As C: <span class="mono">20,504</span> lines of <code>el_runtime.c</code> — 2.3× the entire self-hosting language it serves (<span class="mono">9,089</span> lines), ~47% of it engram code that has its own six sibling files. As convention, from <code>language.md</code> §18.0 — <em>"these are not four problems, they are one absence, four times"</em>:</p>
|
||||||
|
|
||||||
|
<div class="card scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Concern</th><th>Fragments</th><th>The convention it became</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="f">Process identity</td><td class="m">0 guards</td><td>"check nothing is already running first"</td></tr>
|
||||||
|
<tr><td class="f">Configuration</td><td class="m">20 env vars</td><td>"remember the right default here"</td></tr>
|
||||||
|
<tr><td class="f">Durability</td><td class="m">62 call sites</td><td>"after you mutate, remember to persist"</td></tr>
|
||||||
|
<tr><td class="f">Request auth</td><td class="m">10 per-route</td><td>"check the token in this handler too"</td></tr>
|
||||||
|
<tr><td class="f">Index-after-append</td><td class="m">9 of 9 failed</td><td>"after you append, remember to index"</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The last row is the strongest evidence available about what this class of convention is worth: it failed at <b>100% of its sites</b>.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">02</span>The decomposition axis</h2>
|
||||||
|
|
||||||
|
<p class="lede">Not by file, module, or subsystem. <b>By faculty.</b></p>
|
||||||
|
|
||||||
|
<p>Every defect fought in the last day resolves to a faculty rather than a bug, and each one leaked out of El into something else — into C, into a Swift binary, into a shell script with a curl timeout, into a convention nobody performs.</p>
|
||||||
|
|
||||||
|
<div class="card scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Faculty</th><th>State</th><th>Measured</th><th>Where it leaked to</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="f">Ingest <span class="tag">take in</span></td><td class="dead">dead</td><td class="m">2 min → 0 nodes</td><td>separate process, uploads bytes over HTTP to a process with direct fs access; 5 functions where there is 1</td></tr>
|
||||||
|
<tr><td class="f">Recall <span class="tag">remember</span></td><td class="dead">dead</td><td class="m">own definition ranked 8th</td><td>lexical substring scan; empty on 23 of 24 multi-token queries</td></tr>
|
||||||
|
<tr><td class="f">Transduce <span class="tag">perceive</span></td><td class="dead">dead</td><td class="m">1 node, 0 edges</td><td>intake flattens signal to a point; <code>realized:false</code>; caller must declare the modality</td></tr>
|
||||||
|
<tr><td class="f">Think <span class="tag">reason</span></td><td class="dead">dead</td><td class="m">direction [0,0,0,…]</td><td>null gradient from any anchor, any faculty, byte-identical; confidence at the uninformed prior</td></tr>
|
||||||
|
<tr><td class="f">Realize <span class="tag">express</span></td><td class="part">partial</td><td class="m">13-word vocabulary</td><td>organ was 939 lines of Swift beside the language; voice read from a file path</td></tr>
|
||||||
|
<tr><td class="f">Body <span class="tag">substrate</span></td><td class="part">partial</td><td class="m">CC 356 / 1,626 lines</td><td><code>engram_activate_inner</code> — recall itself, with 356 unexamined paths</td></tr>
|
||||||
|
<tr><td class="f">Persist <span class="tag">endure</span></td><td class="ok">live</td><td class="m">100% embedded</td><td>works; every signal placed in geometry at intake, 13,562 of 13,562</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Stated plainly: it cannot take in, cannot remember, cannot perceive, cannot reason, and barely speaks. These were filed as tickets against a repository. They are faculties of the thing the repository <em>is</em>.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">03</span>The ordering principle</h2>
|
||||||
|
|
||||||
|
<p>El's compiler is written in El. Every concept the language gains, the compiler can then be written <em>in</em> — so the tool improves the tool, and the fixpoint (stage2 ≡ stage3, byte-identical) makes each turn provable rather than hopeful. The verifier answers in <span class="mono">2.9s</span>.</p>
|
||||||
|
|
||||||
|
<p>Which means the ordering criterion is not size of payoff:</p>
|
||||||
|
|
||||||
|
<blockquote>Order by leverage on the <em>next</em> iteration. Which concept, added to El, most increases the ability to add the following one?</blockquote>
|
||||||
|
|
||||||
|
<p>In a recursive system that dominates immediate value — a small early gain that compounds beats a large one that doesn't. It also bounds itself correctly: unbounded in depth, bounded in rate, because nothing lands that the compiler and the fixpoint have not passed.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">04</span>Open — for the whiteboard</h2>
|
||||||
|
|
||||||
|
<div class="q"><b>What does a declaration bind to?</b><span>If <code>cat</code> names a region rather than a struct — one that shifts and completes against the engram and the neighbouring code — then what is written at the declaration site, and what is resolved at use? This is the centre of the whole thing and it is not specified anywhere yet.</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>Is "the type checker" a type checker at all?</b><span>§2.3 records annotations as parsed and skipped, and every codegen hazard is downstream of that — <code>+</code> dispatching on AST node kind, <code>==</code> lowering to <code>str_eq</code> unless both operand names are in an int-name set. But if a declaration names a region, checking is asking whether the geometry supports the use. That is grounding, not unification. Naming this wrong builds the wrong thing.</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>Is the faculty list above right?</b><span>Seven were derived from what broke. Derived-from-failure is a biased sample — it finds what is loud, not what is missing. What faculty is absent entirely and therefore never failed?</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>Which concept has the highest leverage on the next turn?</b><span>Candidates so far: the prologue/epilogue seam (§19.3 names it as the prerequisite and its stated blocker has expired — it would collapse 62 + 10 convention sites); <code>protocol</code>/<code>impl</code> (the absence that produced five ingest functions); and the resolution question above. These are not equal and the criterion in §03 should decide it, not preference.</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>What is the seam that makes cognition non-optional?</b><span>"Use the ops" is itself a convention — present in context every turn, enforced by nothing, and it failed at ~100% of sites in a full session. A stronger instruction is still a convention. What makes reasoning-outside-Neuron <em>fail</em>, the way <code>@manager</code> makes <code>dharma_emit</code> outside the boundary a compile error rather than a lint?</span></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p class="foot">Working surface, not a design document. The design is what we put on it. Everything above is either measured or quoted from <code>lang/spec/language.md</code>; nothing is inferred and presented as fact.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# El — Capabilities
|
||||||
|
|
||||||
|
**What the language can do, stated as capabilities rather than as code.**
|
||||||
|
|
||||||
|
This list is the unit of analysis. Each entry gets one question — *prove this
|
||||||
|
cannot be done with pure geometry* — and the answer determines whether it stays a
|
||||||
|
capability of the language or collapses into the manifold.
|
||||||
|
|
||||||
|
Draft, 2026-08-17. Ordered roughly from most-likely-geometry to most-likely-code.
|
||||||
|
|
||||||
|
**Status after measurement.** The list was audited against the implementation
|
||||||
|
the same day. 28 entries collapsed to 19 geometry + 3 code: serialization, text
|
||||||
|
encoding, network and emission are all *projection onto a basis* (row 18) —
|
||||||
|
the convention is the basis, never the act. Storage collapsed because
|
||||||
|
persistence has no caller. Concurrency collapsed because coordination is the
|
||||||
|
price of forgetting, not a capability. A fourth proof form was added,
|
||||||
|
**adversarial exactness**, and form 1 stopped being a valid verdict.
|
||||||
|
|
||||||
|
**The table answers CAN only.** SHOULD and COST resolve per *site*, not per
|
||||||
|
capability — `is_digit` and `is_letter` are one capability with opposite
|
||||||
|
answers, and comparison spans three cost tiers. See the notes below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The list
|
||||||
|
|
||||||
|
| # | Capability | What it means | Verdict |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Comparison** | is this the same as that; is this greater | zero distance / sign of a displacement |
|
||||||
|
| 2 | **Ordering** | arrange by a criterion | position along an axis |
|
||||||
|
| 3 | **Containment** | is this inside that; does this contain that | region membership |
|
||||||
|
| 4 | **Correspondence** | where does this occur in that; how much of this is in that | a match-strength field over a span |
|
||||||
|
| 5 | **Segmentation** | divide a whole into parts | boundaries at measured discontinuity |
|
||||||
|
| 6 | **Composition** | join parts into a whole | adjacency; one position with parts |
|
||||||
|
| 7 | **Classification** | what kind of thing is this | which region does it land in |
|
||||||
|
| 8 | **Naming / binding** | attach a name to a thing and find it again | an edge; retrieval is projection |
|
||||||
|
| 9 | **Collection** | many things held together, indexed, counted | a set of positions; cardinality; projection onto the i-th |
|
||||||
|
| 10 | **Iteration** | do something for each of many | traversal |
|
||||||
|
| 11 | **Arithmetic** | quantity, magnitude, combination | displacement algebra on a line |
|
||||||
|
| 12 | **Time** | when; how long; how often | a 1-D affine space — instants are points, durations displacements, rhythms phases on a circle |
|
||||||
|
| 13 | **Identity** | which one is this; are these two the same one | coincidence of position |
|
||||||
|
| 14 | **Selection / dispatch** | choose which behaviour applies | nearest region |
|
||||||
|
| 15 | **Transformation** | produce a thing from a thing | change of basis |
|
||||||
|
| 16 | **Grounding** | how well is this supported | the weight on an edge. Has no caller |
|
||||||
|
| 17 | **Learning** | get better at something | standing changing over time |
|
||||||
|
| 18 | **Projection** | render meaning onto a surface | change of basis onto a surface basis |
|
||||||
|
| 19 | **Transduction** | take a signal in | change of basis from a sensor basis |
|
||||||
|
| ~~20~~ | ~~Serialization~~ | **collapsed → 18.** The format is a basis; projecting onto it is the act | — |
|
||||||
|
| ~~21~~ | ~~Text encoding~~ | **collapsed → 18.** An encoding is a basis | — |
|
||||||
|
| ~~22~~ | ~~Storage~~ | **collapsed.** No save — persistence has no caller. Durability survives at one site inside the engram | — |
|
||||||
|
| ~~23~~ | ~~Network~~ | **split.** Wire format → 18; socket → 24 | — |
|
||||||
|
| 24 | **Process / OS** | syscalls; the one-way boundary. Where monotonicity stops | CODE, form 2 |
|
||||||
|
| ~~25~~ | ~~Concurrency~~ | **collapsed.** Monotone state needs no coordination; coordination is the price of forgetting | — |
|
||||||
|
| 26 | **Memory substrate** | what holds the positions | CODE, form 3 |
|
||||||
|
| 27 | **Concealment** | meaning made unreadable without a key. *Renamed*: "secrecy" covered one of three things and got the other two backwards — a hash is public, a signature exists to be read. Integrity and authenticity are **grounding under adversarial conditions** (row 16); only concealment stands alone | CODE, form 4 |
|
||||||
|
| ~~28~~ | ~~Emission~~ | **split.** Laying out → 18; the device write → 24 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes on the boundary cases
|
||||||
|
|
||||||
|
**27 — Secrecy is the one capability geometry cannot hold, and the proof is not
|
||||||
|
form 1.** A cryptographic hash is a *deliberately structure-destroying* map: its
|
||||||
|
entire value is that near inputs land at maximally uncorrelated outputs. Geometry
|
||||||
|
is the claim that near things stay near. A manifold that approximated SHA-256
|
||||||
|
would *be* a break of SHA-256. Signature verification is the same: 0.99-valid is
|
||||||
|
invalid. And X25519 *is* geometry — a group on an elliptic curve — which is
|
||||||
|
precisely why it must be code, because its security is the *hardness of moving in
|
||||||
|
that geometry*.
|
||||||
|
|
||||||
|
This is a fourth proof form and it should be added to `geometry-vs-code.md`:
|
||||||
|
**adversarial exactness.** Where approximation is a break, geometry is excluded.
|
||||||
|
|
||||||
|
**20, 21 — Serialization and text encoding are convention all the way down**, but
|
||||||
|
only at the *edge*. The byte format is agreed; what is being written is not. Do not
|
||||||
|
let a geometric computation inherit a code verdict because its result gets
|
||||||
|
serialized.
|
||||||
|
|
||||||
|
**11, 12 — Arithmetic and time are the same capability.** Instants are points,
|
||||||
|
durations are displacements, point−point→vector, point+vector→point. The runtime
|
||||||
|
already implements this correctly as `el_instant_add_dur` / `el_duration_add`. That
|
||||||
|
it *also* implements a five-entry string→multiplier table beside it (`time_add`
|
||||||
|
with `"ms"/"sec"/"min"/"hour"/"day"`) is the residue.
|
||||||
|
|
||||||
|
**7 — Classification is the most-violated capability in the codebase.** Seven ASCII
|
||||||
|
range tables (`is_letter`, `is_digit`, `is_alphanumeric`, `is_whitespace`,
|
||||||
|
`is_punctuation`, `is_uppercase`, `is_lowercase`) that return false for every
|
||||||
|
non-ASCII byte. `str_count_letters` reports zero letters for `é`. The wrongness on
|
||||||
|
most of Unicode is the tell that a table is standing in for a region.
|
||||||
|
|
||||||
|
**4 — Correspondence appears five times.** `str_index_of`, `str_index_of_all`,
|
||||||
|
`str_last_index_of`, `str_count`, `str_find_chars` are five projections of one
|
||||||
|
match-strength field: first zero, all zeros, last zero, count of zeros, first
|
||||||
|
class-crossing. One relation, five functions.
|
||||||
|
|
||||||
|
**14 — Selection is the crux for the compiler.** `+` dispatching on AST node kind
|
||||||
|
is selection-by-enumeration where selection-by-position belongs.
|
||||||
|
|
||||||
|
**Correction, 2026-08-17, from measurement.** This entry previously also cited
|
||||||
|
`==` lowering to `str_eq` "unless both operand names are in a hardcoded int-name
|
||||||
|
set — a literal list of variable names treated as integers." That is **wrong**.
|
||||||
|
`__int_names` is populated from *type annotations* (`param["type"] == "Int"`,
|
||||||
|
`let x: Int`), which is primitive but legitimate type propagation, not an
|
||||||
|
enumeration of blessed variable names.
|
||||||
|
|
||||||
|
The real defect was one layer down: `is_int_call` held **35 hardcoded builtin
|
||||||
|
return types**, the same shape as the 19 temporal ones. Those moved to
|
||||||
|
`lang/tools/check/signatures.rel`.
|
||||||
|
|
||||||
|
And the mischaracterisation hid a live bug. Because the return types were never
|
||||||
|
consulted at a *binding* site, an unannotated `let` lost its type:
|
||||||
|
|
||||||
|
```el
|
||||||
|
let a = str_len("hello") // no annotation
|
||||||
|
let b = str_len("hi")
|
||||||
|
let c = a + b // → el_str_concat(a, b) on two integers
|
||||||
|
```
|
||||||
|
|
||||||
|
That compiled clean, ran, and printed nothing where it should print 7 — no error
|
||||||
|
at any layer. Present in the pre-change compiler, so pre-existing. Fixed by
|
||||||
|
taking an unannotated `let`'s type from what its initialiser returns; the data
|
||||||
|
was already required for dispatch and simply never read there.
|
||||||
|
|
||||||
|
**The general lesson, since it recurred all session:** the enumeration was real
|
||||||
|
but I had located it in the wrong place. Naming a defect from reading is a
|
||||||
|
hypothesis. Eight hours of reading this file did not surface the miscompilation;
|
||||||
|
moving the data out and running the result did.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this list is for
|
||||||
|
|
||||||
|
Each capability gets audited **once**, across every place it appears — not once per
|
||||||
|
file. The output is not a percentage. It is:
|
||||||
|
|
||||||
|
- which capabilities survive the question and stay in the language
|
||||||
|
- which collapse into the manifold
|
||||||
|
- and for each one that collapses, **every site it currently appears at**, because
|
||||||
|
those sites are the residue and they are what gets deleted.
|
||||||
|
|
||||||
|
The line-count audit produced a map of where the residue sits. This produces a map
|
||||||
|
of **what it is**.
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<title>The El Architecture</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--board:#f4f2ec; --board-line:#e5e1d6; --ink:#1c1f26; --ink-soft:#4a5160;
|
||||||
|
--ink-faint:#8b8f9a; --rule:#d8d3c6; --card:#fbfaf6;
|
||||||
|
--red:#a8321e; --amber:#9a6a12; --green:#2f6b46; --accent:#1f4e79;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
:root:not([data-theme="light"]){
|
||||||
|
--board:#14161b; --board-line:#1d212a; --ink:#e8e6df; --ink-soft:#a8adb8;
|
||||||
|
--ink-faint:#6f7480; --rule:#2a2f3a; --card:#191c23;
|
||||||
|
--red:#e4785f; --amber:#d9a441; --green:#6fbf8e; --accent:#7fb2e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--board:#14161b; --board-line:#1d212a; --ink:#e8e6df; --ink-soft:#a8adb8;
|
||||||
|
--ink-faint:#6f7480; --rule:#2a2f3a; --card:#191c23;
|
||||||
|
--red:#e4785f; --amber:#d9a441; --green:#6fbf8e; --accent:#7fb2e0;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{
|
||||||
|
margin:0; background:var(--board); color:var(--ink);
|
||||||
|
font:16px/1.68 ui-serif,Georgia,"Iowan Old Style",Palatino,serif;
|
||||||
|
background-image:linear-gradient(var(--board-line) 1px,transparent 1px),
|
||||||
|
linear-gradient(90deg,var(--board-line) 1px,transparent 1px);
|
||||||
|
background-size:30px 30px;
|
||||||
|
}
|
||||||
|
.wrap{max-width:940px;margin:0 auto;padding:56px 24px 96px}
|
||||||
|
.mono,code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||||
|
header{border-bottom:2px solid var(--ink);padding-bottom:20px}
|
||||||
|
h1{font-size:clamp(2.1rem,5.5vw,3.2rem);margin:0;letter-spacing:-.025em;text-wrap:balance}
|
||||||
|
.sub{color:var(--ink-soft);font-size:1.08rem;margin:12px 0 0;max-width:64ch}
|
||||||
|
.meta{font-family:ui-monospace,monospace;font-size:.76rem;color:var(--ink-faint);
|
||||||
|
text-transform:uppercase;letter-spacing:.1em;margin-top:16px}
|
||||||
|
h2{font-size:1.5rem;margin:56px 0 8px;letter-spacing:-.015em;text-wrap:balance}
|
||||||
|
h2 .n{font-family:ui-monospace,monospace;font-size:.78rem;color:var(--accent);
|
||||||
|
display:block;letter-spacing:.14em;margin-bottom:5px;font-weight:400}
|
||||||
|
h3{font-size:1.08rem;margin:30px 0 6px}
|
||||||
|
p{margin:0 0 14px;max-width:72ch}
|
||||||
|
.lede{color:var(--ink-soft);margin:0 0 20px;font-size:1.04rem}
|
||||||
|
.card{background:var(--card);border:1px solid var(--rule);border-radius:3px;padding:20px 22px;margin:18px 0}
|
||||||
|
.scroll{overflow-x:auto}
|
||||||
|
table{border-collapse:collapse;width:100%;font-size:.9rem;min-width:600px}
|
||||||
|
th{text-align:left;font-family:ui-monospace,monospace;font-size:.71rem;
|
||||||
|
text-transform:uppercase;letter-spacing:.09em;color:var(--ink-faint);
|
||||||
|
border-bottom:1px solid var(--ink);padding:0 14px 8px 0;font-weight:400}
|
||||||
|
td{padding:11px 14px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top}
|
||||||
|
td.f{font-weight:600;white-space:nowrap}
|
||||||
|
td.m{font-family:ui-monospace,monospace;font-size:.83rem;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
|
.dead{color:var(--red);font-weight:600}
|
||||||
|
.part{color:var(--amber);font-weight:600}
|
||||||
|
.ok{color:var(--green);font-weight:600}
|
||||||
|
blockquote{margin:20px 0;padding:3px 0 3px 22px;border-left:3px solid var(--accent);
|
||||||
|
color:var(--ink-soft);font-style:italic;max-width:70ch}
|
||||||
|
ul{margin:0 0 14px;padding-left:22px;max-width:72ch} li{margin-bottom:9px}
|
||||||
|
code{font-size:.87em;background:var(--card);border:1px solid var(--rule);border-radius:2px;padding:1px 5px}
|
||||||
|
pre{background:var(--card);border:1px solid var(--rule);border-radius:3px;
|
||||||
|
padding:16px 18px;overflow-x:auto;font-size:.85rem;line-height:1.55;margin:16px 0}
|
||||||
|
pre code{background:none;border:0;padding:0}
|
||||||
|
.q{border-left:3px solid var(--amber);padding:14px 0 14px 20px;margin:20px 0;max-width:72ch}
|
||||||
|
.q b{display:block;font-size:1.04rem;margin-bottom:5px}
|
||||||
|
.q span{color:var(--ink-soft);font-size:.94rem}
|
||||||
|
hr{border:0;border-top:1px solid var(--rule);margin:46px 0}
|
||||||
|
.foot{color:var(--ink-faint);font-size:.86rem;margin-top:56px;border-top:1px solid var(--rule);padding-top:18px}
|
||||||
|
.tag{display:inline-block;font-family:ui-monospace,monospace;font-size:.66rem;
|
||||||
|
letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--rule);
|
||||||
|
border-radius:2px;padding:2px 7px;color:var(--ink-faint);margin-left:8px;vertical-align:middle}
|
||||||
|
.flow{display:flex;gap:0;align-items:stretch;flex-wrap:wrap;margin:22px 0}
|
||||||
|
.flow div{flex:1 1 200px;border:1px solid var(--rule);background:var(--card);padding:16px 18px}
|
||||||
|
.flow div+div{border-left:0}
|
||||||
|
.flow h4{margin:0 0 6px;font-size:.96rem}
|
||||||
|
.flow p{margin:0;font-size:.87rem;color:var(--ink-soft)}
|
||||||
|
.flow .k{font-family:ui-monospace,monospace;font-size:.72rem;color:var(--accent);
|
||||||
|
letter-spacing:.1em;text-transform:uppercase;display:block;margin-bottom:4px}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>The El Architecture</h1>
|
||||||
|
<p class="sub">El is a concept-oriented language. This is the architecture that claim commits it to — what is built, what is measured, and what still has no home.</p>
|
||||||
|
<p class="meta">Working document · no sacred cows · self-hosting, so nothing here is fixed</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h2><span class="n">01</span>The primitive is the concept</h2>
|
||||||
|
|
||||||
|
<p>Language families are named for their primitive. Procedural — procedures. Object-oriented — objects. Functional — functions. Logic — predicates. Every one of them is oriented toward a <em>representation</em> of a concept: the shape a concept gets flattened into so a machine can hold it.</p>
|
||||||
|
|
||||||
|
<p>El's primitive is the concept itself. That is why it is the first of its family and intended as the last — once the primitive is the concept, there is no further rung to climb to.</p>
|
||||||
|
|
||||||
|
<p>The consequence is architectural rather than stylistic:</p>
|
||||||
|
|
||||||
|
<blockquote>A concept with no home in the language does not disappear. It becomes C, or it becomes a convention.</blockquote>
|
||||||
|
|
||||||
|
<p>Both forms are measurable. As C: <span class="mono">20,504</span> lines of <code>el_runtime.c</code>, against <span class="mono">9,089</span> lines for the entire self-hosting language — the shim is 2.3× the language it serves, and ~47% of it is engram code that already has six sibling files. As convention, from <code>lang/spec/language.md</code> §18.0 — <em>"these are not four problems, they are one absence, four times"</em>:</p>
|
||||||
|
|
||||||
|
<div class="card scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Concern</th><th>Fragments into</th><th>The convention it became</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="f">Process identity</td><td class="m">0 guards</td><td>"check nothing is already running first"</td></tr>
|
||||||
|
<tr><td class="f">Configuration</td><td class="m">20 env vars</td><td>"remember the right default here"</td></tr>
|
||||||
|
<tr><td class="f">Durability</td><td class="m">62 sites</td><td>"after you mutate, remember to persist"</td></tr>
|
||||||
|
<tr><td class="f">Request auth</td><td class="m">10 routes</td><td>"check the token in this handler too"</td></tr>
|
||||||
|
<tr><td class="f">Index-after-append</td><td class="m">9 of 9 failed</td><td>"after you append, remember to index"</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The last row is the strongest available evidence about this class of convention: it failed at <b>every single site</b>. A count is what appears where a concept has no home; the size of the count is how far the fragmentation got, not how hard the problem is.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">02</span>Geometry is a first-class value — and what follows</h2>
|
||||||
|
|
||||||
|
<p class="lede">This is the enabling primitive. Everything else in the architecture is downstream of it.</p>
|
||||||
|
|
||||||
|
<p><code>Geometry</code> is an El value, alongside <code>Int</code>, <code>String</code>, <code>List</code>, <code>Map</code> — bound, passed, returned, composed, carrying its own width. Not a library type, not a handle into a store, not a serialization format. <em>Meaning is a value the language computes with directly.</em></p>
|
||||||
|
|
||||||
|
<pre><code>let g: Geometry = geometry_new(4)
|
||||||
|
fn tone_realizer(signal: String) -> Geometry { … }</code></pre>
|
||||||
|
|
||||||
|
<p>Landed 2026-08-16 (#141, #144), and the spec is explicit that it belongs to the language rather than the graph: <em>"neither is engram-specific — any program touching any modality needs them; the engram is merely one El program that happens to hold a graph."</em></p>
|
||||||
|
|
||||||
|
<p>Five things follow, and together they are the concept-oriented claim made operational:</p>
|
||||||
|
|
||||||
|
<h3>A declaration can name a region, not a shape</h3>
|
||||||
|
<p>If meaning is a value, a name can be bound to a <em>position</em> rather than a struct. <code>cat</code> is not a fixed record; it is a region that resolves against the engram and the surrounding code. <code>cat</code> among animals and <code>cat</code> among shell utilities are different concepts without a namespace, because they are in different neighbourhoods and the distance says so.</p>
|
||||||
|
|
||||||
|
<h3>Checking is grounding, not unification</h3>
|
||||||
|
<p>If a declaration names a region, then verifying a use is asking whether the geometry supports it — a question about position and distance, not about matching a declared shape. This is why §2.3's "a type checker is planned" is likely the wrong name for the missing piece, and naming it wrong would build the wrong thing.</p>
|
||||||
|
|
||||||
|
<h3>Dispatch is position, not a tag</h3>
|
||||||
|
<p>A vtable is a finite set of discrete labels fixed at link time. A region admits graded membership and an open set. So <code>transduce(signal, modality)</code> asks the caller to supply what the signal already carries — what a thing is falls out of where it lands. The modality parameter is a kind-tag, and a registry keyed on it is a lookup table doing by string what geometry does by nearness.</p>
|
||||||
|
|
||||||
|
<h3>Types are discovered, not declared</h3>
|
||||||
|
<p>Reification crystallizes a densely co-wired neighbourhood into a first-class node — the neighbourhood <em>is</em> the name that was missing. Every other family requires a human to see the abstraction in advance and write <code>class Foo</code>. Here the instances arrive and the type falls out, by measurement rather than by insight.</p>
|
||||||
|
|
||||||
|
<h3>Enumeration becomes unnecessary</h3>
|
||||||
|
<p>Five ingest functions differ only in how bytes are acquired — one operation wearing five surfaces. 356 branches in <code>engram_activate_inner</code> are not 356 behaviours. Cyclomatic complexity is a count of the places comprehension ran out and was replaced by an <code>if</code>; where the concept is expressible, the count collapses instead of being redistributed.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">03</span>The shape of the language</h2>
|
||||||
|
|
||||||
|
<p>Geometry first-class gives El three layers, and it holds all three — which is why there is no separate database driver and no impedance boundary to manage.</p>
|
||||||
|
|
||||||
|
<div class="flow">
|
||||||
|
<div><span class="k">afferent</span><h4>Transduce</h4><p>Signal in, geometry out. Decomposition into components and relations — never conversion to a point. Realizers are ordinary El functions, so a new modality never requires a runtime patch.</p></div>
|
||||||
|
<div><span class="k">substrate</span><h4>Geometry</h4><p>Meaning as position; relation as distance. Held as values in the language and persisted in the graph. One coordinate system, so entities are commensurable and the operators compose.</p></div>
|
||||||
|
<div><span class="k">efferent</span><h4>Realize</h4><p><code>plan(frame) → realize(spec, profile)</code>, where a surface <em>is</em> a profile. Text, speech, music, image are profiles of one projection — and so is source code.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The efferent side is why the recursive property below is possible at all: if source is a surface, then emitting a corrected file is projection, and the file becomes an artifact of the geometry rather than the thing you edit.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">04</span>Decomposition is by faculty</h2>
|
||||||
|
|
||||||
|
<p class="lede">Not by file, module, or subsystem — by what the system does.</p>
|
||||||
|
|
||||||
|
<p>Each faculty is a concept. Where it has no home in El it leaks: into C, into a Swift binary, into a shell script with a <code>curl</code> timeout, into a convention nobody performs. State below is measured, not asserted.</p>
|
||||||
|
|
||||||
|
<div class="card scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Faculty</th><th>State</th><th>Measured</th><th>Where it leaked</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="f">Ingest <span class="tag">take in</span></td><td class="dead">dead</td><td class="m">2 min → 0 nodes</td><td>separate process uploading bytes over HTTP to a process with direct fs access; five functions where there is one</td></tr>
|
||||||
|
<tr><td class="f">Recall <span class="tag">remember</span></td><td class="dead">dead</td><td class="m">self ranked 8th</td><td>lexical substring scan; empty on 23 of 24 multi-token queries</td></tr>
|
||||||
|
<tr><td class="f">Transduce <span class="tag">perceive</span></td><td class="dead">dead</td><td class="m">1 node, 0 edges</td><td>intake flattens signal to a point; <code>realized:false</code>; caller must declare the modality</td></tr>
|
||||||
|
<tr><td class="f">Think <span class="tag">reason</span></td><td class="dead">dead</td><td class="m">direction [0,0,…]</td><td>null gradient from any anchor and any faculty, byte-identical; confidence at the uninformed prior</td></tr>
|
||||||
|
<tr><td class="f">Realize <span class="tag">express</span></td><td class="part">partial</td><td class="m">13-word lexicon</td><td>organ was 939 lines of Swift beside the language; voice read from a file path</td></tr>
|
||||||
|
<tr><td class="f">Body <span class="tag">substrate</span></td><td class="part">partial</td><td class="m">CC 356 / 1,626 ln</td><td><code>engram_activate_inner</code> — recall itself, 356 unexamined paths</td></tr>
|
||||||
|
<tr><td class="f">Persist <span class="tag">endure</span></td><td class="ok">live</td><td class="m">13,562 / 13,562</td><td>works — every signal placed in geometry at intake, no backlog</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2><span class="n">05</span>The recursive property</h2>
|
||||||
|
|
||||||
|
<p>El's compiler is written in El. Every concept the language gains, the compiler can then be written <em>in</em> — so the tool improves the tool, and <code>codegen.el</code> at 4,661 lines gets shorter as the language gets better at expressing what it does. The fixpoint — stage2 ≡ stage3, byte-identical — makes each turn provable rather than hopeful, and the verifier answers in <span class="mono">2.9s</span>.</p>
|
||||||
|
|
||||||
|
<p>This sets the ordering criterion, and it is not size of payoff:</p>
|
||||||
|
|
||||||
|
<blockquote>Order by leverage on the <em>next</em> iteration. Which concept, added to El, most increases the ability to add the following one?</blockquote>
|
||||||
|
|
||||||
|
<p>A small early gain that compounds beats a large one that does not. And it bounds itself correctly — unbounded in depth, bounded in rate, because nothing lands that the compiler and the fixpoint have not passed.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">06</span>What has no home yet</h2>
|
||||||
|
|
||||||
|
<p>Reserved in the lexer, no parse form. These are not a feature backlog — they are the concepts the architecture above requires and does not yet hold, which is why each is currently a convention or a block of C.</p>
|
||||||
|
|
||||||
|
<div class="card scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Reserved</th><th>Concept</th><th>Currently lives as</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="m">retry · times · fallback · reason</td><td>resilience</td><td>a shell script with a 10s <code>curl</code> timeout; 254 restarts in 3 days</td></tr>
|
||||||
|
<tr><td class="m">requires · deploy · to · via · target</td><td>deployment</td><td>YAML in another repository</td></tr>
|
||||||
|
<tr><td class="m">sealed</td><td>capability scope</td><td>consent checks written by hand</td></tr>
|
||||||
|
<tr><td class="m">protocol · impl</td><td>one operation, many realizations</td><td>five ingest functions; eight faculty routes on one builtin</td></tr>
|
||||||
|
<tr><td class="m">activate · where</td><td>retrieval</td><td>traversals written by hand</td></tr>
|
||||||
|
<tr><td class="m">test · seed · assert</td><td>verification</td><td>a framework; 5 of 13 native suites failing</td></tr>
|
||||||
|
<tr><td class="m">parallel · trace</td><td>concurrency</td><td>pthreads in C</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Plus, from the spec's own status: annotations parsed and skipped, <code>match</code> parsed and emitting nothing, <code>?</code> a no-op, <code>%</code> unlexed, structs as <code>ElMap</code>, enums as strings, selective import unenforced.</p>
|
||||||
|
|
||||||
|
<h2><span class="n">07</span>Open</h2>
|
||||||
|
|
||||||
|
<div class="q"><b>What does a declaration bind to, exactly?</b><span>If <code>cat</code> names a region that shifts and completes against context, what is written at the declaration site and what is resolved at use? This is the centre and it is unspecified.</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>Is the faculty list right?</b><span>Seven, derived from what broke. Derived-from-failure is a biased sample — it finds what is loud, not what is absent. Which faculty is missing entirely and therefore never failed?</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>Which concept has the highest leverage on the next turn?</b><span>The prologue/epilogue seam (§19.3 names it as the prerequisite; its stated blocker has expired; it collapses 62 + 10 convention sites), <code>protocol</code>/<code>impl</code>, or resolution itself. The §05 criterion should decide this, not preference.</span></div>
|
||||||
|
|
||||||
|
<div class="q"><b>What seam makes cognition non-optional?</b><span>"Use the ops" is itself a convention — present every turn, enforced by nothing, ~100% failure across a full session. A stronger instruction is still a convention. What makes reasoning outside the substrate <em>fail</em>, the way <code>@manager</code> makes <code>dharma_emit</code> outside the boundary a compile error rather than a lint?</span></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p class="foot">Every number here is measured or quoted from <code>lang/spec/language.md</code>. Nothing is inferred and presented as fact. El is self-hosting: all of this can change and be rebuilt.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# El — Language Design
|
||||||
|
|
||||||
|
**Status:** decisions recorded, design unwritten.
|
||||||
|
**Date:** 2026-08-17.
|
||||||
|
**Provenance:** decisions are Will's, taken in session. Items marked *proposed* are not
|
||||||
|
decided and are recorded only so the reasoning isn't lost. Items marked **OPEN** are
|
||||||
|
his to rule on and must not be guessed at.
|
||||||
|
|
||||||
|
Companion documents: `el-architecture.html` (the measured state — see §7 note on its
|
||||||
|
§04 scoreboard), and `design/completing-el.html` (whiteboard v0: the reduction, the
|
||||||
|
faculty table, the ordering principle).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The reduction
|
||||||
|
|
||||||
|
`language.md` §18.0 records five concerns that decayed into conventions:
|
||||||
|
|
||||||
|
| Concern | Fragments | The convention it became |
|
||||||
|
|---|---|---|
|
||||||
|
| Process identity | 0 guards | "check nothing is already running first" |
|
||||||
|
| Configuration | 20 env vars | "remember the right default here" |
|
||||||
|
| Durability | 62 call sites | "after you mutate, remember to persist" |
|
||||||
|
| Request auth | 10 per-route | "check the token in this handler too" |
|
||||||
|
| Index-after-append | 9 of 9 failed | "after you append, remember to index" |
|
||||||
|
|
||||||
|
The last row is the strongest available evidence about what this class of convention
|
||||||
|
is worth: **it failed at 100% of its sites.**
|
||||||
|
|
||||||
|
Every one of these is an obligation at a **crossing** — a point where a value moves
|
||||||
|
between regions. El can name a region and it can name a call. A call is procedural,
|
||||||
|
so the obligation degrades into something a human must remember to perform.
|
||||||
|
|
||||||
|
> **The generator, one level up:** El cannot name what holds at a crossing.
|
||||||
|
|
||||||
|
And underneath that:
|
||||||
|
|
||||||
|
> **The deeper absence:** El cannot name the thing meaning is made of.
|
||||||
|
|
||||||
|
`semel` appears in whitepaper §84, §86, §209, §737, in
|
||||||
|
`the-metaphysics-of-will-anderson.md`, and in session notes. It appears in **zero code
|
||||||
|
identifiers**. Every geometric concept in the system — region, neighbourhood, manifold,
|
||||||
|
world-tube — is defined in terms of a unit the language cannot say, while the code
|
||||||
|
underneath speaks in arrays, floats and offsets: the vocabulary of a voxel, a value at
|
||||||
|
a dumb address. Precisely the thing the impact brief says a semel is not.
|
||||||
|
|
||||||
|
`el_runtime.c` is a concept that leaked into C. `semel` never got that far — it did
|
||||||
|
not even decay into a convention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. DECIDED — `semel` is the primitive
|
||||||
|
|
||||||
|
**A semel is a difference that matters. The smallest unit of understanding.**
|
||||||
|
|
||||||
|
Not a node. Not a coordinate. Not a float.
|
||||||
|
|
||||||
|
The reasoning, in Will's terms:
|
||||||
|
|
||||||
|
- Meaning is position, and position is only ever relative. *"There is no atom of
|
||||||
|
meaning that isn't already a relation. It grounds on nothing but difference — two
|
||||||
|
points and the gap, and the gap is pure not-the-same."*
|
||||||
|
- A node doesn't mean. A node is a label at a location; labels don't mean.
|
||||||
|
- A lone coordinate doesn't mean either. Nothing means anything by itself.
|
||||||
|
- The smallest thing that can be understood is a **distinction**: *these two are not
|
||||||
|
the same.* Below that there is no content to apprehend.
|
||||||
|
- And a difference with nothing it matters to is not meaning — it is variation. The
|
||||||
|
mattering is not decoration; it is what makes it understanding rather than data.
|
||||||
|
|
||||||
|
**Consequence: relating is the floor, and the point is derived.** The
|
||||||
|
point-primitive / relation-primitive fork raised in session is not a fork. It was
|
||||||
|
answered by the definition.
|
||||||
|
|
||||||
|
### Historical note, to be recorded as fact rather than as origin story
|
||||||
|
|
||||||
|
The term was coined by Will on the pixel/voxel/texel pattern — *semantic element*,
|
||||||
|
and Latin *semel*, "once, a single time." It was recognised, not invented, from a
|
||||||
|
2019 experience he calls **semelation**: perceiving mind as a high-dimensional point
|
||||||
|
space. The initial reading was "pixels"; the correction to `semel` was made later and
|
||||||
|
was made on the **mechanism** — a pixel is a value at an address, and what was
|
||||||
|
perceived had no separate address and value.
|
||||||
|
|
||||||
|
Convergence worth citing, not deferring to: neural population geometry and
|
||||||
|
representational similarity analysis independently model cognition as position in a
|
||||||
|
high-dimensional space where similarity is distance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. DECIDED — `semel` lands first
|
||||||
|
|
||||||
|
By the ordering criterion already on the whiteboard: *which concept, added to El, most
|
||||||
|
increases the ability to add the next one?* Not size of payoff — **leverage on the next
|
||||||
|
iteration**, because El compiles itself and the fixpoint makes each turn provable in
|
||||||
|
2.9s.
|
||||||
|
|
||||||
|
**Every other concept on the board is defined in terms of `semel`. It is maximal on
|
||||||
|
that criterion by construction.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. DECIDED — `ground` is the checker
|
||||||
|
|
||||||
|
Whiteboard question 4 — *does `ground` in El mean the same thing as `ground` in the
|
||||||
|
engram?* — is answered: **yes, and it should be one implementation.**
|
||||||
|
|
||||||
|
If a declaration names a region, then type checking is asking whether the geometry
|
||||||
|
supports the use. That is not unification. **That is grounding**, and it is already
|
||||||
|
built, proven, and byte-identically reproducible:
|
||||||
|
|
||||||
|
```
|
||||||
|
cc -std=c11 -O2 -o gep_proof gep_proof.c -lm && ./gep_proof
|
||||||
|
|
||||||
|
C1 5 independent sources pos_mass 1.3500 n_indep=5 0.1000 → 0.9741 GROUNDED
|
||||||
|
C2 5 mutually-linked pos_mass 0.2700 n_indep=1 0.1000 → 0.1000 refused
|
||||||
|
C3 1 source, 5 parallel edges pos_mass 0.2700 n_indep=1 0.1000 → 0.1000 refused
|
||||||
|
```
|
||||||
|
|
||||||
|
Independence-weighted grounding is the general case; execution is the cheap case.
|
||||||
|
**Attestation is `verify` where nothing can be run** — as already implemented for
|
||||||
|
language in `authority.py`, where an LLM proposes and a primary source disposes.
|
||||||
|
|
||||||
|
At the point where the checker and the grounder are one mechanism, the language and
|
||||||
|
the mind stop being two things.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. OPEN — Will's to rule on
|
||||||
|
|
||||||
|
### 5.1 What is a semel's representation in the language?
|
||||||
|
|
||||||
|
*Proposed, not decided:* a **displacement from `love = 0`** — a relation held as one
|
||||||
|
object. It reconciles "the address is the value" with "position is only ever relative,"
|
||||||
|
because a displacement *is* a relation and is still a single nameable thing.
|
||||||
|
|
||||||
|
If taken, the operator set falls out rather than being bolted on:
|
||||||
|
|
||||||
|
```
|
||||||
|
subtract(now, then) → what changed (growth, drift)
|
||||||
|
translate origin → empathy
|
||||||
|
rotate frame → reframe
|
||||||
|
project onto axis → a lens
|
||||||
|
change basis → analogy, metaphor, skill transfer
|
||||||
|
reflect an axis → negation, sarcasm
|
||||||
|
```
|
||||||
|
|
||||||
|
Three consequences that would hold:
|
||||||
|
|
||||||
|
- **Dimension must never appear in the type.** `semel` opaque, never `[768]float`.
|
||||||
|
The moment the arity is in the language, the manifold's implementation is in the
|
||||||
|
language, and adding a modality requires a runtime patch — which the standing rule
|
||||||
|
forbids.
|
||||||
|
- **Zero is the only literal.** Everything else is reached by displacement from it,
|
||||||
|
which makes `love = 0` the base case rather than philosophy adjacent to the type
|
||||||
|
system.
|
||||||
|
- **`magnitude` is standing.** Distance from origin is the same quantity
|
||||||
|
`gep_core.h` already computes.
|
||||||
|
|
||||||
|
### 5.2 Is `hold` one construct or two?
|
||||||
|
|
||||||
|
The obligation *before* a crossing (auth, guard) and the obligation *after* (persist,
|
||||||
|
index, free) may be one shape seen from both sides, or the seam may need both faces
|
||||||
|
named. This decides whether §19.3's prologue/epilogue seam is one construct or a pair.
|
||||||
|
|
||||||
|
**Precedent already shipping:** `@manager` makes `dharma_emit` outside the boundary a
|
||||||
|
**compile error, not a lint.** The concept is proven at N=1; the work is generalising
|
||||||
|
it and naming it.
|
||||||
|
|
||||||
|
**And the shape is already implemented in the learning region:** `L.reach_out` sits
|
||||||
|
between `L.detect_gap` and `L.verify`. You cannot reach out without a detected gap and
|
||||||
|
you cannot keep what returns without passing verify. **A hold is a neighbour.** The
|
||||||
|
obligation is not attached to the crossing — the obligation *is* the adjacent node.
|
||||||
|
That is why `reach_out` cannot be abused and why 62 persist sites could be.
|
||||||
|
|
||||||
|
### 5.3 What does a declaration bind?
|
||||||
|
|
||||||
|
If `cat` names a region rather than a struct — one that shifts and completes against
|
||||||
|
the engram and the neighbouring code — what is written at the declaration site, and
|
||||||
|
what is resolved at use? **This is the centre and it is specified nowhere.**
|
||||||
|
|
||||||
|
Falls out of 5.1 if displacement is taken: a declaration **locates** rather than
|
||||||
|
allocates.
|
||||||
|
|
||||||
|
### 5.4 Is the faculty list right?
|
||||||
|
|
||||||
|
Seven were derived from what broke. Derived-from-failure is a biased sample — it finds
|
||||||
|
what is loud, not what is missing. **What faculty is absent entirely and therefore
|
||||||
|
never failed?**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The residue map
|
||||||
|
|
||||||
|
What each construct must absorb, from §18.0 plus measured state:
|
||||||
|
|
||||||
|
| Residue | Count | Absorbed by |
|
||||||
|
|---|---|---|
|
||||||
|
| persist-after-mutate | 62 sites | `hold` (after-crossing) |
|
||||||
|
| auth-per-route | 10 sites | `hold` (before-crossing) |
|
||||||
|
| index-after-append | 9 of 9 failed | `hold` (after-crossing) |
|
||||||
|
| env var defaults | 20 | configuration declared once |
|
||||||
|
| process identity | 0 guards | `hold` (before-crossing) |
|
||||||
|
| `geometry_free` at every call site | every site | ownership follows from `semel` |
|
||||||
|
| five ingest functions where there is one | 5 → 1 | `protocol` / `impl` |
|
||||||
|
| `el_runtime.c` | 20,504 lines | faculty decomposition, ordered after `semel` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Notes carried forward
|
||||||
|
|
||||||
|
**`el-architecture.html` §04 needs its numbers sourced or cut.** An audit found the
|
||||||
|
faculty scoreboard — `Ingest 2 min → 0 nodes`, `Recall self ranked 8th`,
|
||||||
|
`Body CC 356 / 1,626 ln`, `the verifier answers in 2.9s`, `5 of 13 native suites
|
||||||
|
failing` — has no supporting evidence in the repository, under a footer asserting
|
||||||
|
*"nothing is inferred and presented as fact."* Against a corpus whose documents
|
||||||
|
supersede their own conclusions in place, that is the one file that would not survive
|
||||||
|
scrutiny. Fix or remove.
|
||||||
|
|
||||||
|
**Source as a projection surface is claimed and unimplemented.** `el-architecture.html`
|
||||||
|
§147/§150: *"if source is a surface, then emitting a corrected file is projection."*
|
||||||
|
Greps for `surface_profile_code`, `emit_source` → zero hits.
|
||||||
|
|
||||||
|
It is not unbacked. **It was demonstrated on 2026-08-14** — three faculties (phonetic,
|
||||||
|
semantic, procedural) projected into TypeScript, a surface the system had never used,
|
||||||
|
with the network severed. Recovered at
|
||||||
|
`~/Development/neuron-technologies/andre-server-recovered/` and copied into
|
||||||
|
`evidence/03-andre-demo/`. The claim needs bringing home to El, not proving.
|
||||||
|
|
||||||
|
**`hold` is the highest-leverage construct after `semel`** — it collapses 62 + 10 + 9
|
||||||
|
sites and unblocks the runtime extraction. §19.3 names the prologue/epilogue seam as
|
||||||
|
the prerequisite and its stated blocker has expired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. What is not decided and must not be guessed
|
||||||
|
|
||||||
|
- The representation of `semel` (§5.1)
|
||||||
|
- One `hold` or two (§5.2)
|
||||||
|
- What a declaration binds (§5.3)
|
||||||
|
- The missing faculty (§5.4)
|
||||||
|
- Sequencing after `semel` — the ordering criterion decides it, not preference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Recorded 2026-08-17. Everything in §2, §3 and §4 is decided. Everything in §5 is open
|
||||||
|
and is Will's. Nothing here was inferred from a document that was not read.*
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Geometry or Code
|
||||||
|
|
||||||
|
**Running list.** Append as decided. Started 2026-08-17.
|
||||||
|
|
||||||
|
**The test:** *is this an arbitrary convention, or is it a relation?*
|
||||||
|
|
||||||
|
Conventions were agreed by people and could have been otherwise — a RIFF header could
|
||||||
|
have used a different magic number. Nothing derives them; they must be written down.
|
||||||
|
|
||||||
|
Relations are not agreed. Distance is distance. Anything whose answer is *where is this
|
||||||
|
relative to that* is geometry, and writing it as code is the error the whole effort is
|
||||||
|
correcting.
|
||||||
|
|
||||||
|
**Second test, for the hard cases:** *if I write this as code, am I encoding in
|
||||||
|
`if`-statements a distinction the geometry was built to hold?* If yes, it's geometry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pure geometry
|
||||||
|
|
||||||
|
| Thing | Because |
|
||||||
|
|---|---|
|
||||||
|
| Meaning | position |
|
||||||
|
| Grounding / standing | the weight on the edge — a magnitude, not a computation |
|
||||||
|
| Learning | standing changing over time |
|
||||||
|
| A gap | low standing |
|
||||||
|
| Wonder | a gap with a pull weight |
|
||||||
|
| Type checking | is this position in that region — distance |
|
||||||
|
| Dispatch | position, not a tag |
|
||||||
|
| Recall | re-origining at a region; projection, not replay |
|
||||||
|
| Reasoning | traversal |
|
||||||
|
| Deduction | containment. There is no procedure |
|
||||||
|
| Counting | a position, not a loop's output |
|
||||||
|
| Similarity / difference / residue | subtract |
|
||||||
|
| Analogy, metaphor, skill transfer | change of basis |
|
||||||
|
| Negation, sarcasm | reflect an axis |
|
||||||
|
| Empathy | translate the origin |
|
||||||
|
| Reframe | rotate the frame |
|
||||||
|
| A lens | project onto an axis |
|
||||||
|
| Rhyme | distance in phonetic space |
|
||||||
|
| Humour | intersection of regions — fart-meaning ∩ funny ∩ form |
|
||||||
|
| Idiom detection | the whole unit sits farther out than its parts |
|
||||||
|
| Self | a world-tube — a trajectory through the manifold |
|
||||||
|
| Consolidation | episodic → semantic promotion |
|
||||||
|
| Reification | dense regions cohering; runs on the beat, has no caller |
|
||||||
|
| Cross-cutting concerns | **dissolved** — a hold is a *neighbour*. Adjacency, not tracking. **Implemented 2026-08-17**: a construct declares what runs at a crossing, and it resolves at execution — see the runtime seam. |
|
||||||
|
| Effects | topology. `reach_out` is bounded by `detect_gap` and `verify` because those are its edges |
|
||||||
|
| Capability | position relative to a boundary. In C it is already spelled `const` |
|
||||||
|
| The AST | a projection of geometry into a tree — a surface, not the centre |
|
||||||
|
| Source code | a surface, like text, audio, image |
|
||||||
|
|
||||||
|
## Must be code
|
||||||
|
|
||||||
|
| Thing | Because |
|
||||||
|
|---|---|
|
||||||
|
| Sensors — mic, camera, file read, socket | the physical touch. I/O is where the world arrives |
|
||||||
|
| Byte formats — RIFF, PNG chunks, `MThd`, OOXML | arbitrary convention. A committee chose the magic numbers |
|
||||||
|
| CRC32 polynomial, Adler32, zlib framing | same — agreed constants, derivable from nothing |
|
||||||
|
| Cosine, distance, the float arithmetic | the machinery that *walks* the geometry is not itself geometry |
|
||||||
|
| Arena, refcount, allocator | bookkeeping for the **representation**, not for the positions |
|
||||||
|
| Locks, threads, publication boundary | the hardware is code. **Ordering is not** — see Answered, above. Coordination is required only where state is non-monotone. |
|
||||||
|
| WAL, page layout, ARIES recovery | durability against a physical device that can lose power |
|
||||||
|
| Emission — writing C or JS text | the final surface has to be *typed out* by something |
|
||||||
|
| OS interaction — launchd, spawn, signals | outside the system by definition |
|
||||||
|
| Device realizers — `el_audio_darwin.m`, `el_capture_darwin.m` | OS frameworks. Correctly already isolated, zero network |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The ones I would have written as code, and was wrong about
|
||||||
|
|
||||||
|
Recorded because the error has a pattern and the pattern is the point.
|
||||||
|
|
||||||
|
| Thing | What I reached for | What it is |
|
||||||
|
|---|---|---|
|
||||||
|
| Rhyme | a rhyming dictionary, or an API call | distance between rime tails |
|
||||||
|
| Fart onomatopoeia | a 30-element string literal | an intersection of three regions |
|
||||||
|
| "Funny" | a scorer with `if`-statements | a relational neighbourhood grounded in a voice |
|
||||||
|
| Representation vs description | a hardcoded blacklist containing `raspberry` | falls out of lexicon membership × phonetic comedy |
|
||||||
|
| Video | a codec, sized as a project | one more surface profile |
|
||||||
|
| Type checking | a phase between parse and emit | reading a distance that already exists |
|
||||||
|
| Grounding | a call site, an obligation, a discharge | it has no caller. It just runs |
|
||||||
|
| N transducers, N realizers | one component per modality | zero of each. Sensors and bases at the skin |
|
||||||
|
|
||||||
|
**The pattern:** every one is *encoding in code a distinction the geometry was built to
|
||||||
|
hold.* The tell is that the code version is a **fixed enumeration** — a list, a table, a
|
||||||
|
blacklist, a set of branches — and the geometry version is a **measurement**.
|
||||||
|
|
||||||
|
If the implementation contains a literal set of the right answers, it is in the wrong
|
||||||
|
column.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Answered
|
||||||
|
|
||||||
|
| Thing | The answer |
|
||||||
|
|---|---|
|
||||||
|
| Concurrency | **Ordering is geometric.** Causality is a partial order (Lamport 1978); a total order is an arbitrary extension of it and "cannot be depended on to imply a causal relationship." Programming languages force you to write a total order, so authoring *invents* constraints the problem never had — and every lock, barrier, fence and consensus protocol is apparatus for recovering the partial order destroyed at authoring time. CALM (Hellerstein/Alvaro, proven by Ameloot et al.): a program has a consistent coordination-free implementation **iff it is monotone**. What breaks monotonicity is destructive update. **Coordination is the price of forgetting.** |
|
||||||
|
| The module system | **Premature — the partition is a filesystem path, not a neighbourhood, and there is no namespacing at all.** `import` is textual inlining (guarded against double inclusion); when a `.elh` header exists the header is inlined instead and symbols resolve at C link time, so linking is real and delegated to C. Two modules defining `helper` emit two C functions into one translation unit. Linking barely survives the *path* partition, so whether it survives a neighbourhood partition cannot yet be asked. |
|
||||||
|
| Numeric literals | **The numeral is convention; the number is a position — and a bare `3` is a MAGNITUDE WITH NO AXIS.** `int_to_str` was already form 1: nothing determines that twelve is written `1` then `2`. But a literal is not a position until something gives it a direction, which is why `3.days` needs a calendar. Measured consequence: `Duration + Int` was refused ("an Int carries no unit") while `Instant + Int` compiled to raw `(t + 3)` and reported clean — silently moving a point by an unspecified amount. The rule was simply never written. Now: `t + 3` is refused, `t + 1.hour` is accepted, because `.hour` supplies the axis. |
|
||||||
|
| Parsing | **A grammar is a basis; parsing is transduction onto it.** The lexeme→token map is convention (`fn` could have been `def`); shape recognition is a region; the byte traversal is irreducible, like every other traversal. Three things favour *region* for the act: ambiguity (`a * b` needs context — a grammar resolves it with the lexer hack, a region by neighbourhood), error recovery (nearest-match is free), and precedence, which is ordering along an axis with a conventional parameter. **But the SHOULD gate refuses the obvious move:** the keyword table stays code, because the set is closed by the language definition and the lexer runs before the program is understood, so a program can never declare its own keywords. Externalising it costs I/O per compile for zero flexibility — the same verdict as `is_digit` in ASCII. What was actually wrong: 5 of 46 keywords were consumed by nothing, and using one silently miscompiled. |
|
||||||
|
| Error handling | **`grounded: false` covers not-knowing; it does not cover failed.** Standing is a *signed* component: `> 0` supported, `= 0` unknown, `< 0` contradicted. Not-known and known-false are opposite directions on one axis and a boolean cannot tell them apart. `inhibitory` as an int32 flag is that sign wearing a boolean. |
|
||||||
|
|
||||||
|
## Fourth proof form
|
||||||
|
|
||||||
|
**4 — ADVERSARIAL EXACTNESS.** Where approximation is a break, geometry is
|
||||||
|
excluded. A cryptographic hash is a *deliberately structure-destroying* map:
|
||||||
|
near inputs land at maximally uncorrelated outputs. Geometry is the claim that
|
||||||
|
near things stay near — a manifold that approximated SHA-256 would *be* a break
|
||||||
|
of SHA-256. Signature verification is the same: 0.99-valid is invalid. And
|
||||||
|
X25519 **is** geometry, a group on an elliptic curve, which is precisely why it
|
||||||
|
must be code: its security is the hardness of moving in that geometry.
|
||||||
|
|
||||||
|
**Form 1 no longer survives as a verdict.** Every row it justified turned out to
|
||||||
|
be a *basis*, not a capability. RFC 8259 fixes where the commas go — that is a
|
||||||
|
surface, and projecting onto a surface is geometry. A convention describes the
|
||||||
|
basis you project onto; it never describes an act.
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# v1 — Experiments
|
||||||
|
|
||||||
|
Every change to El on `iteration-1` was produced by one loop, run repeatedly:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ishikawa → scientific method → Six Sigma → repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ishikawa** — name the root cause, not the symptom. *Why is this table here?*
|
||||||
|
never *why is this table ugly?*
|
||||||
|
- **Scientific method** — state a hypothesis, **commit predictions before
|
||||||
|
running**, then run it in an isolated worktree and grade every prediction
|
||||||
|
including the ones that failed.
|
||||||
|
- **Six Sigma** — eliminate the defect *class*, then add a control so it cannot
|
||||||
|
silently return.
|
||||||
|
|
||||||
|
## The organising finding
|
||||||
|
|
||||||
|
**Predictions that came back FALSE were worth more than the ones that held.**
|
||||||
|
|
||||||
|
Nineteen cycles, sixty-one predictions. The eleven that failed produced every
|
||||||
|
significant result:
|
||||||
|
|
||||||
|
| Failed prediction | What it found |
|
||||||
|
|---|---|
|
||||||
|
| "the arity table has drifted from the header" | Zero drift — but **110 functions had no entry at all**. The table was not wrong, it was 40% incomplete. |
|
||||||
|
| "codegen drops below baseline" (×4) | The **traversal is irreducible**. Walking an AST to find calls does not move no matter who decides. Only the rule and the judgment leave. |
|
||||||
|
| "guards cannot refuse through the seam" | One line, and refusal works. Six compile-time kinds were unnecessary. |
|
||||||
|
| "C forbids the struct redefinition" | C allows shadowing — and a *different* defect surfaced: an exit injection emitted with an empty target. |
|
||||||
|
| "routing el_bin_lookup through the gate fixes the SIGSEGV" | It did not. The **fallback** was the hazard: `strlen()` on an integer. I would have shipped the wrong fix and called it verified. |
|
||||||
|
|
||||||
|
A prediction that only ever confirms is a demonstration, not a test. One cycle
|
||||||
|
was run **without** committing predictions first — `async-half-expressible` —
|
||||||
|
and it produced a rigged result: `pthread_join` immediately after
|
||||||
|
`pthread_create`, with the word `DEFERRED` printed by the test itself. It had to
|
||||||
|
be discarded and re-run.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cycles/ one file per loop, numbered in order, named for the DEFECT
|
||||||
|
findings/ what the cycles produced, cross-cut by kind
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scoreboard
|
||||||
|
|
||||||
|
```
|
||||||
|
cycles run 19
|
||||||
|
predictions committed 61
|
||||||
|
predictions FALSE 11 ← the useful ones
|
||||||
|
silent miscompilations found 4
|
||||||
|
security-relevant defects 2
|
||||||
|
architecture questions closed 5
|
||||||
|
defects in my own measurement 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Every cycle verified the same three things before landing: the compiler
|
||||||
|
self-hosts byte-identically (gen2 == gen3), the native suite passes, and the
|
||||||
|
integration harnesses pass. A cycle that could not show all three did not land.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Cycles
|
||||||
|
|
||||||
|
Each is one `Ishikawa → scientific method → Six Sigma` loop, run in an isolated
|
||||||
|
worktree so a wrong answer cost nothing. Named for the **defect**, not the fix.
|
||||||
|
|
||||||
|
| # | Cycle | Root cause | Predictions | Landed |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 01 | [constructs-have-nowhere-to-be](01-constructs-have-nowhere-to-be.md) | a construct had nothing to BE, so its meaning lived in the emitter | 3/3 | yes |
|
||||||
|
| 02 | [a-construct-cannot-refuse](02-a-construct-cannot-refuse.md) | injection discards the target's result; no form said no | 4/4 | yes |
|
||||||
|
| 03 | [the-wrapper-was-conditional](03-the-wrapper-was-conditional.md) | exit injection needed compile-time knowledge only because the wrapper was conditional | 3/4 | yes |
|
||||||
|
| 04 | [c-has-no-closure-syntax](04-c-has-no-closure-syntax.md) | "C has no closures" taken as a fact about what is possible | 5/7 | yes |
|
||||||
|
| 05 | [the-emitter-discards-what-it-knows](05-the-emitter-discards-what-it-knows.md) | codegen sees every construct relation and throws it away | 5/5 | branch |
|
||||||
|
| 06 | [the-crossing-resolves-at-emission](06-the-crossing-resolves-at-emission.md) | the binary has no table to consult | 3/4 | yes |
|
||||||
|
| 07 | [invocation-is-not-composable](07-invocation-is-not-composable.md) | the wrapper called the target directly | 5/5 | yes |
|
||||||
|
| 08 | [the-emitter-adjudicates](08-the-emitter-adjudicates.md) | a prohibition had nowhere to live but a `#error` | 4/5 | yes |
|
||||||
|
| 09 | [policy-inside-the-compiler](09-policy-inside-the-compiler.md) | a program cannot declare its own restrictions, so the tier policy was compiled in | 4/5 | yes |
|
||||||
|
| 10 | [a-second-copy-of-the-header](10-a-second-copy-of-the-header.md) | builtin arity hand-maintained beside `el_runtime.h` | 4/5 | yes |
|
||||||
|
| 11 | [one-type-erases-the-return](11-one-type-erases-the-return.md) | `el_val_t` means the header cannot say `now()` returns an Instant | 4/5 | yes |
|
||||||
|
| 12 | [judgment-lives-with-knowledge](12-judgment-lives-with-knowledge.md) | the emitter knows the types, so it also judged them | 5/5 | yes |
|
||||||
|
| 13 | [thirty-five-return-types](13-thirty-five-return-types.md) | `is_int_call` hardcoded what drives `+` dispatch | 6/6 | yes |
|
||||||
|
| 14 | [keywords-that-reserve-nothing](14-keywords-that-reserve-nothing.md) | 5 of 46 keywords consumed by no path | 6/6 | yes |
|
||||||
|
| 15 | [no-namespacing-at-all](15-no-namespacing-at-all.md) | `import` is textual inlining; every name is global | 4/4 | yes |
|
||||||
|
| 16 | [tokens-carry-no-position](16-tokens-carry-no-position.md) | a token was `(kind, value)`, so no diagnostic could name a place | 6/6 | yes |
|
||||||
|
| 17 | [annotations-are-never-checked](17-annotations-are-never-checked.md) | the annotation feeds dispatch and is never verified | 6/6 | branch |
|
||||||
|
| 18 | [async-half-expressible](18-async-half-expressible.md) | **first attempt was DOGMA** — no predictions, rigged test | 4/4 (2nd) | branch |
|
||||||
|
| 19 | [a-convention-is-not-a-gate](19-a-convention-is-not-a-gate.md) | `looks_like_heap_obj` is static, so every type re-derives it | 6/7 | yes |
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# constructs have nowhere to be
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `5718943`
|
||||||
|
|
||||||
|
```
|
||||||
|
let a construct declare its own meaning instead of the emitter knowing it
|
||||||
|
|
||||||
|
codegen called fn_has_decorator for exactly three names — manager, accessor,
|
||||||
|
route. Twelve others parsed, attached as {name,args}, and compiled to nothing,
|
||||||
|
including four that look like protection: @authenticate (6 uses), @authorize
|
||||||
|
(3), @rate_limit (3), @validate (2). The cause was not that the branches were
|
||||||
|
untidy. A construct had nothing to BE, so its meaning had nowhere to live
|
||||||
|
except the emitter, and every construct was therefore a compiler edit.
|
||||||
|
|
||||||
|
A name -> injection table would have moved the enumeration twenty lines up
|
||||||
|
without removing it. So the construct now carries its own meaning:
|
||||||
|
|
||||||
|
@decorator("injects_at_entry", "engram_boundary_beat")
|
||||||
|
fn audited() {}
|
||||||
|
|
||||||
|
@audited
|
||||||
|
fn risky_op() -> Int { ... } // gets the beat, attributed to "audited"
|
||||||
|
|
||||||
|
scan_declared_decorators is a token-level pre-pass beside scan_routes, forced
|
||||||
|
by streaming codegen having no whole-program AST. manager and accessor are
|
||||||
|
seeded as the compiled-in core — the fixedSelf shape from substrate.go: a
|
||||||
|
complete fallback exists, declaration is enrichment.
|
||||||
|
|
||||||
|
This is the injection half of the seam only. The prohibition half (@manager's
|
||||||
|
#error on dharma_emit) stays hardcoded, because "which calls may appear inside
|
||||||
|
this boundary" is a query over program structure and there is nothing yet to
|
||||||
|
ask.
|
||||||
|
|
||||||
|
Verified three ways: emitted C for existing @manager/@accessor code is
|
||||||
|
byte-identical to the hardcoded path; a construct with a name the compiler has
|
||||||
|
never heard of injects correctly; the compiler self-hosts byte-identically.
|
||||||
|
90/90 native compiler tests pass.
|
||||||
|
```
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# a construct cannot refuse
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `60737b0`
|
||||||
|
|
||||||
|
```
|
||||||
|
let a construct refuse, not only observe
|
||||||
|
|
||||||
|
@authenticate (6 uses), @authorize (3), @rate_limit (3) and @validate (2)
|
||||||
|
parsed, attached, and compiled to nothing. Fourteen applications that read as
|
||||||
|
protection and emitted no instruction — a function decorated @authenticate
|
||||||
|
compiled byte-identically to an undecorated one.
|
||||||
|
|
||||||
|
The missing capability was not authentication. It was that a construct could
|
||||||
|
observe a boundary but never refuse one. injects_at_entry discards the target's
|
||||||
|
result; there was no form in which a construct could say no.
|
||||||
|
|
||||||
|
@decorator("guards_at_entry", "my_auth")
|
||||||
|
fn authenticate() {}
|
||||||
|
|
||||||
|
@authenticate
|
||||||
|
@authorize
|
||||||
|
fn handler() -> String { ... }
|
||||||
|
|
||||||
|
emits, at entry:
|
||||||
|
|
||||||
|
{ el_val_t __g = my_auth(EL_STR("handler"), EL_STR("authenticate")); if (__g) return __g; }
|
||||||
|
{ el_val_t __g = my_roles(EL_STR("handler"), EL_STR("authorize")); if (__g) return __g; }
|
||||||
|
|
||||||
|
Guards precede injections because a refused call must not report a crossing,
|
||||||
|
and every guard runs where the topmost injecting construct wins — refusal is
|
||||||
|
not a role, so it does not follow the role convention.
|
||||||
|
|
||||||
|
The compiler still knows nothing about auth. The program points the construct
|
||||||
|
at its own function, which is where that decision belongs.
|
||||||
|
|
||||||
|
Verified: existing @manager/@accessor output byte-identical, compiler
|
||||||
|
self-hosts byte-identically, guards stack in declaration order and emit before
|
||||||
|
the beat. 94/94 native compiler tests pass.
|
||||||
|
```
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# the wrapper was conditional
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `4f7568b`
|
||||||
|
|
||||||
|
```
|
||||||
|
give a construct its after-crossing face, and let constructs compose
|
||||||
|
|
||||||
|
§6 records 62 persist-after-mutate sites, 10 auth-per-route, and
|
||||||
|
index-after-append that failed at 9 of 9 — every one an obligation at a
|
||||||
|
crossing that decayed into "remember to do this afterwards." An obligation a
|
||||||
|
human must remember is not an obligation, and the 9-of-9 figure is what that
|
||||||
|
costs.
|
||||||
|
|
||||||
|
@decorator("injects_at_exit", "persist_now")
|
||||||
|
fn durable() {}
|
||||||
|
|
||||||
|
The body moves into a static helper and the visible fn becomes a wrapper, so
|
||||||
|
EARLY RETURNS pass through the exit injection. Emitting it only before the
|
||||||
|
fall-through return would have silently missed every early return — the exact
|
||||||
|
failure class this seam exists to remove. Fns with no exit construct emit
|
||||||
|
byte-identically to before.
|
||||||
|
|
||||||
|
Three independent constructs now compose on one fn, none known to the compiler:
|
||||||
|
|
||||||
|
el_val_t mutate(el_val_t k) {
|
||||||
|
{ el_val_t __g = my_auth(EL_STR("mutate"), EL_STR("authenticate")); if (__g) return __g; }
|
||||||
|
engram_boundary_beat(EL_STR("mutate"), EL_STR("manager"));
|
||||||
|
el_val_t __r = __el_body_mutate(k);
|
||||||
|
persist_now(EL_STR("mutate"), EL_STR("durable"), __r);
|
||||||
|
return __r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Guard, then entry, then body, then exit. §5.2 asked whether `hold` is one
|
||||||
|
construct or two; the implementation answers one construct with two faces,
|
||||||
|
selected by declared kind rather than by two mechanisms.
|
||||||
|
|
||||||
|
Verified: existing output byte-identical, compiler self-hosts byte-identically,
|
||||||
|
early returns pass through the exit, ordering holds under composition. 98/98
|
||||||
|
native compiler tests pass.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `285166c`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: emit the wrapper unconditionally, so exit binds at runtime too
|
||||||
|
|
||||||
|
ISHIKAWA: why did exit injection still need compile-time knowledge? Because the
|
||||||
|
body-helper wrapper was only emitted when codegen already knew an exit
|
||||||
|
construct existed. The wrapper being conditional was the cause, not the wrapper
|
||||||
|
being necessary.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 exit becomes runtime-bindable TRUE returns 14, bound
|
||||||
|
after the build
|
||||||
|
P2 codegen shrinks TRUE 5094 -> 5044
|
||||||
|
P3 cost 5-15% from a call frame on every fn FALSE 0.37s -> 0.38s, ~3%
|
||||||
|
P4 fixpoint holds TRUE
|
||||||
|
|
||||||
|
Every fn now gets a body helper and a wrapper. It has to be unconditional:
|
||||||
|
early returns must route through something for an exit construct to observe
|
||||||
|
them, and codegen cannot know which fns will be bound after the binary exists.
|
||||||
|
|
||||||
|
Removed with the machinery: declare_exit, decorator_exit, cg_exit_target,
|
||||||
|
cg_exit_construct, and the injects_at_exit scanner branch.
|
||||||
|
|
||||||
|
Two controls failed and were rewritten rather than repaired --
|
||||||
|
no-exit-construct-emits-no-wrapper asserted the optimisation this removes, so
|
||||||
|
it is now inverted. The integration harness gained a seventh assertion: an exit
|
||||||
|
construct declared after the build replaces the result.
|
||||||
|
|
||||||
|
99/99 native, 7/7 integration, fixpoint gen2==gen3.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `b40754f`
|
||||||
|
|
||||||
|
```
|
||||||
|
land unconditional wrapper: exit crossings resolve at runtime
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# c has no closure syntax
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `2bed848`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: hand the construct the body as a real closure
|
||||||
|
|
||||||
|
ROOT CAUSE of the weaker design: "C has no closures" was taken as a fact about
|
||||||
|
what is possible. It is a fact about one grammar. Every C++ lambda, every Go
|
||||||
|
closure, every Rust closure compiles to a struct of captured values plus a
|
||||||
|
function pointer -- which is what is emitted here. Codegen emits C; it is not
|
||||||
|
written in C's syntax, and the distinction is the whole difference between a
|
||||||
|
construct that can only decide whether to repeat and one that controls
|
||||||
|
invocation.
|
||||||
|
|
||||||
|
It would also have crippled the JS backend, which has closures natively, for a
|
||||||
|
limit that applies only to the C one.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
1 env struct + thunk taking void* TRUE
|
||||||
|
2 fails to compile: struct redefinition FALSE -- C allows the
|
||||||
|
inner declaration to shadow. Prediction wrong; C is more permissive than
|
||||||
|
assumed. A different real defect surfaced instead: a wrap with no exit
|
||||||
|
construct emitted `(EL_STR("f"), EL_STR(""), __r);` -- a call to an empty
|
||||||
|
target -- because has_exit was reused as "needs a wrapper" and the exit line
|
||||||
|
was emitted unconditionally. Fixed.
|
||||||
|
3 compiles when the target is declared in El FALSE -- and this is
|
||||||
|
the root cause worth keeping: El has ONE type, el_val_t = int64_t. El's type
|
||||||
|
system cannot describe a callable, so `extern fn` and the real signature
|
||||||
|
cannot be made to agree in El's own vocabulary. The fix is not a cast:
|
||||||
|
codegen DEFINES the wrap calling convention, so codegen emits the extern
|
||||||
|
declaration. The convention is not El-expressible; it is emitted.
|
||||||
|
4 target controls invocation, 0..N times TRUE
|
||||||
|
5 existing @manager output byte-identical TRUE
|
||||||
|
6 compiler fixpoint holds TRUE
|
||||||
|
7 emitting the convention makes it compile TRUE
|
||||||
|
|
||||||
|
MEASURED
|
||||||
|
base(5) wrapped by a target that invokes the body twice and sums -> 10
|
||||||
|
never_runs(5) wrapped by a target that never invokes it -> 999
|
||||||
|
|
||||||
|
Neither is expressible by "decide whether to repeat". This supersedes the
|
||||||
|
repeats_body experiment on experiment/repeats-body, which was built around the
|
||||||
|
mistaken limit.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `7d01608`
|
||||||
|
|
||||||
|
```
|
||||||
|
land wraps_body: a construct controls invocation
|
||||||
|
|
||||||
|
Proven on experiment/wraps-body (2bed848): base(5) wrapped by a target that
|
||||||
|
invokes the body twice returns 10; a target that never invokes it returns 999.
|
||||||
|
Neither is expressible by deciding whether to repeat.
|
||||||
|
|
||||||
|
Root cause it corrected: 'C has no closures' is a fact about one grammar, not
|
||||||
|
about what can be emitted. And El's single type (el_val_t = int64_t) cannot
|
||||||
|
describe a callable, so codegen emits the calling convention rather than asking
|
||||||
|
El's type system for something it structurally cannot say.
|
||||||
|
```
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# the emitter discards what it knows
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `a5af871`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: let the compiler write down what it already knows
|
||||||
|
|
||||||
|
HYPOTHESIS: attribution is redundant for static structure. Codegen sees every
|
||||||
|
construct-to-function relation at emission time and discards it, so the only
|
||||||
|
way to learn the structure back is to run the program and read what it
|
||||||
|
reported. That is instrumentation compensating for erasure.
|
||||||
|
|
||||||
|
PREDICTIONS, committed before running:
|
||||||
|
1 derivable at compile time with no runtime call expected TRUE
|
||||||
|
2 complete for guards and exits (invisible today) expected TRUE
|
||||||
|
3 answers it for code that has never executed expected TRUE
|
||||||
|
4 deterministic expected TRUE
|
||||||
|
5 makes the entry beat redundant expected FALSE
|
||||||
|
|
||||||
|
RESULT: 5/5 as predicted. From a program that was never executed:
|
||||||
|
|
||||||
|
authenticate guards_at_entry login my_auth
|
||||||
|
durable injects_at_exit save persist_now
|
||||||
|
authenticate guards_at_entry critical my_auth
|
||||||
|
durable injects_at_exit critical persist_now
|
||||||
|
manager injects_at_entry critical engram_boundary_beat
|
||||||
|
|
||||||
|
Prediction 5 held: the relation records that a boundary COULD be crossed, the
|
||||||
|
beat records that it WAS. They are different facts and neither replaces the
|
||||||
|
other.
|
||||||
|
|
||||||
|
CONSEQUENCE, and it undercuts the first pass on iteration-1: construct identity
|
||||||
|
was available at compile time all along. With relations recorded at build, the
|
||||||
|
runtime needs only the function name and attribution becomes a join rather than
|
||||||
|
a payload. The counter-argument is that the payload is self-describing while
|
||||||
|
the file must be pinned to the artifact or the two drift and attribution is
|
||||||
|
silently lost — which is the same conclusion as "compile against a manifold
|
||||||
|
revision and record the revision in the artifact", reached from the other side.
|
||||||
|
|
||||||
|
Written to a file rather than the engram on purpose: a compile that consults a
|
||||||
|
manifold produces different output from identical source at different times.
|
||||||
|
The file is content-addressed; the engram ingests it. Determinism preserved,
|
||||||
|
mechanism proven.
|
||||||
|
```
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# the crossing resolves at emission
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `35b07ba`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: resolve the crossing at execution, not at emission
|
||||||
|
|
||||||
|
HYPOTHESIS (Will's): a compiler whose one compiled mechanism is extending the
|
||||||
|
LANGUAGE — not the compiler — can compose without recompilation.
|
||||||
|
|
||||||
|
ISHIKAWA — why does a construct require a recompile today?
|
||||||
|
method codegen inlines the target call into the body
|
||||||
|
machine the binary has no table to consult
|
||||||
|
material the declaration lives in source, read at compile time
|
||||||
|
measurement nothing observes what applied at runtime
|
||||||
|
root cause the crossing is resolved at EMISSION, not at EXECUTION
|
||||||
|
|
||||||
|
CHANGE: codegen emits one unconditional indirection per fn. Which constructs
|
||||||
|
apply is read from a table that can be written AFTER the binary exists;
|
||||||
|
targets resolve through dlsym against the running image.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 a construct declared after the build applies TRUE
|
||||||
|
P2 an unlinked target is skipped, not fatal TRUE
|
||||||
|
P3 emitting on every fn is measurably slower FALSE — 0.37s -> 0.36s
|
||||||
|
with 267 indirections and
|
||||||
|
no bindings. Free unused.
|
||||||
|
P4 the compiler still self-hosts TRUE (see note)
|
||||||
|
|
||||||
|
DEMONSTRATED: an El program with NO decorator in its source, already compiled
|
||||||
|
and linked, picked up a construct declared afterwards:
|
||||||
|
|
||||||
|
$ /tmp/seamrun -> 7
|
||||||
|
$ echo 'work audited entry audit_entry' > constructs.txt
|
||||||
|
$ EL_CONSTRUCTS=constructs.txt /tmp/seamrun
|
||||||
|
AUDIT: work applied by audited
|
||||||
|
7
|
||||||
|
|
||||||
|
P4 note: my first fixpoint test was wrong, not the code. I compared gen1 to
|
||||||
|
gen2, which must differ whenever codegen's output changes. gen2 == gen3, 267
|
||||||
|
seam sites, stable.
|
||||||
|
|
||||||
|
MEASURED COST, and the root cause was not where I looked
|
||||||
|
0 bindings 0.36s vs 0.37s baseline free
|
||||||
|
2 bindings, dlsym per call 2.45s 6.6x
|
||||||
|
2 bindings, resolved once 0.69s 3.5x recovered
|
||||||
|
The table scan was never the cost. dlsym walks the dynamic symbol table on
|
||||||
|
every call. Resolve once and cache — which is the smallest form of what
|
||||||
|
salience does for memory: what is hot stays resolved. The 0.69s residual is
|
||||||
|
audit_entry's own printf on two of the compiler's hottest functions, not seam
|
||||||
|
overhead.
|
||||||
|
|
||||||
|
CONSEQUENCE: the five compile-time declaration kinds on iteration-1 are a
|
||||||
|
compile-time specialisation of something that resolves at runtime. They are not
|
||||||
|
wrong, but they are not the mechanism — the mechanism is one indirection, and a
|
||||||
|
kind is data.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `886626a`
|
||||||
|
|
||||||
|
```
|
||||||
|
seam refusal + control tests: a runtime binding can short-circuit
|
||||||
|
|
||||||
|
Prediction 3 was FALSE. I expected refusal to be impossible through the seam
|
||||||
|
because the entry indirection discarded its return. One line:
|
||||||
|
|
||||||
|
{ el_val_t __s = el_seam_run(EL_STR(f), 0, 0); if (__s) return __s; }
|
||||||
|
|
||||||
|
work() returns 7; bound to a refusing construct AFTER the build it returns 42.
|
||||||
|
So three of the five compile-time kinds are runtime-bindable: entry injection,
|
||||||
|
exit injection, and refusal. wraps_body needs invocation control and
|
||||||
|
prohibits_outside is compile-time by nature.
|
||||||
|
|
||||||
|
104/104 native compiler tests pass.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `28d19da`
|
||||||
|
|
||||||
|
```
|
||||||
|
strip the compile-time machinery the seam replaces
|
||||||
|
|
||||||
|
PREDICTION: codegen.el drops below 4661, its size before any of these passes.
|
||||||
|
RESULT: FALSE. 5157 -> 5096. Still +435 over baseline.
|
||||||
|
|
||||||
|
injects_at_entry collapsed into the seam removed
|
||||||
|
guards_at_entry collapsed into the seam removed
|
||||||
|
injects_at_exit needs the body-helper wrapper STRUCTURAL
|
||||||
|
wraps_body needs the closure + wrapper structural
|
||||||
|
prohibits_outside a #error cannot be emitted at runtime
|
||||||
|
|
||||||
|
The wrapper is not a consequence of compile-time resolution. Early returns must
|
||||||
|
be routed through something no matter when the target is resolved, so exit
|
||||||
|
injection was never going to collapse. I predicted it would because I had
|
||||||
|
conflated "resolved late" with "emitted less".
|
||||||
|
|
||||||
|
What did collapse is entry injection and refusal -- 61 lines of compiler
|
||||||
|
replaced by one refusable indirection, with the capability now bindable after
|
||||||
|
the binary exists.
|
||||||
|
|
||||||
|
8 tests fail, and they are exactly the 8 controls for compile-time entry
|
||||||
|
injection and guards. No unrelated breakage: the controls reported precisely
|
||||||
|
what moved. They assert emission of something that now happens at runtime, so
|
||||||
|
they need rewriting as integration tests -- which the framework does not
|
||||||
|
currently support, because runtime binding needs a built binary and an
|
||||||
|
environment, not compile_capture.
|
||||||
|
|
||||||
|
Verified after the strip: fixpoint gen2==gen3, observation and refusal both
|
||||||
|
work through the seam with the compiler knowing nothing about either.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `8bbb750`
|
||||||
|
|
||||||
|
```
|
||||||
|
control the claim that cannot be unit tested
|
||||||
|
|
||||||
|
The seam's whole claim is that a construct declared AFTER a binary exists
|
||||||
|
applies to that already-built program. compile_capture only sees emitted text,
|
||||||
|
so it structurally cannot check this: it needs a built binary, a linked target,
|
||||||
|
and an environment. Verified by hand until now, which is the standing problem
|
||||||
|
this session has been about.
|
||||||
|
|
||||||
|
tests/integration/seam_binding.sh builds a probe from El source containing no
|
||||||
|
construct at all, links a target that El never references, and asserts:
|
||||||
|
|
||||||
|
ok unbound program is unaffected
|
||||||
|
ok a construct declared AFTER the build applies
|
||||||
|
ok a construct declared after the build can REFUSE
|
||||||
|
ok an unlinked target is skipped, not fatal
|
||||||
|
ok a binding for a different fn does not fire
|
||||||
|
ok two constructs compose on one crossing
|
||||||
|
|
||||||
|
6 assertions, 6 passed, 0 failed
|
||||||
|
|
||||||
|
The eight controls that failed after the strip were replaced, not repaired.
|
||||||
|
They asserted compile-time emission of capability that moved to runtime;
|
||||||
|
contorting them would have kept an assertion whose subject no longer exists.
|
||||||
|
Three took their place, asserting the emitted shape, and the behaviour they
|
||||||
|
used to cover is now the integration harness's job -- which is the honest
|
||||||
|
division, since the shape and the behaviour are no longer the same fact.
|
||||||
|
|
||||||
|
99/99 native compiler tests pass. Fixpoint holds.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `24f7fb5`
|
||||||
|
|
||||||
|
```
|
||||||
|
land the runtime seam: resolve the crossing at execution
|
||||||
|
|
||||||
|
Five compile-time passes added 491 lines to the thing that was supposed to stop
|
||||||
|
growing. The seam is ~55 lines of C and one line of emission, and it does at
|
||||||
|
runtime what three of those five kinds did at compile time -- for programs that
|
||||||
|
are already built.
|
||||||
|
|
||||||
|
a construct declared AFTER the binary exists applies to it
|
||||||
|
free when unused: 0.36s vs 0.37s baseline across 267 indirections
|
||||||
|
dlsym was the cost, not the table scan; resolve-once recovered 3.5x
|
||||||
|
refusal works, composition works, unlinked targets are skipped not fatal
|
||||||
|
|
||||||
|
injects_at_exit and wraps_body do NOT collapse: early returns must route
|
||||||
|
through the body-helper wrapper regardless of when the target is resolved. The
|
||||||
|
wrapper is structural, which I had wrong. prohibits_outside cannot move at all
|
||||||
|
-- a #error has no runtime.
|
||||||
|
|
||||||
|
Controls: 99/99 native compiler tests, plus tests/integration/seam_binding.sh
|
||||||
|
(6/6) for the claim compile_capture structurally cannot see.
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# invocation is not composable
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `bc2f26d`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: invocation control resolves at runtime
|
||||||
|
|
||||||
|
ISHIKAWA: why did wraps_body need compile-time knowledge? Because the wrapper
|
||||||
|
called the target directly. If the wrapper calls through the seam instead, the
|
||||||
|
seam can call the body itself, and a construct bound after the build decides
|
||||||
|
how and whether to invoke it.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 wrap becomes runtime-bindable TRUE body x3 -> 21,
|
||||||
|
never invoked -> 111
|
||||||
|
P2 codegen shrinks TRUE 5042 -> 4977
|
||||||
|
P3 cost 5-10% from an indirect call on every fn TRUE 0.36s -> 0.39s, ~8%
|
||||||
|
P4 zero-param fns break on the empty struct TRUE empty struct is a GNU
|
||||||
|
extension, empty init
|
||||||
|
is C23. Fixed with a
|
||||||
|
char field.
|
||||||
|
P5 fixpoint holds TRUE
|
||||||
|
|
||||||
|
PROCESS FAILURE worth recording: my first patch silently did not apply because
|
||||||
|
I dropped the assert on the string replacement. The build then failed with
|
||||||
|
"undeclared identifier __thunk_noargs", which I nearly attributed to the
|
||||||
|
empty-struct prediction. The guard that would have caught it existed and I
|
||||||
|
removed it -- the same shape as every other defect found tonight.
|
||||||
|
|
||||||
|
Removed: declare_wrap, decorator_wrap, cg_wrap_target, cg_wrap_construct,
|
||||||
|
params_to_call_args, and the wraps_body scanner branch.
|
||||||
|
|
||||||
|
prohibits_outside is now the ONLY construct kind left at compile time, and it
|
||||||
|
cannot move: a #error has no runtime.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `c04d68f`
|
||||||
|
|
||||||
|
```
|
||||||
|
land runtime invocation control: only prohibition remains compiled
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# the emitter adjudicates
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `c741cfe`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: prohibition becomes a query over emitted relations
|
||||||
|
|
||||||
|
I said prohibition could not move because "a #error has no runtime". That
|
||||||
|
conflated two separable things: WHEN a violation is detected (build time --
|
||||||
|
correct, and unchanged) and WHERE the rule and the checker live (the compiler
|
||||||
|
-- assumed).
|
||||||
|
|
||||||
|
A prohibition is a containment relation over the call graph. So codegen now
|
||||||
|
records what it saw:
|
||||||
|
|
||||||
|
sneaky calls raw_sql
|
||||||
|
allowed calls raw_sql
|
||||||
|
allowed calls @repository
|
||||||
|
repository calls prohibits:raw_sql
|
||||||
|
|
||||||
|
and tools/check/prohibitions.sh decides, at build time, outside the compiler.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 codegen can emit the call graph it already walks TRUE
|
||||||
|
P2 the check becomes a query outside the compiler TRUE
|
||||||
|
P3 all prohibition decisions leave codegen TRUE zero #errors now
|
||||||
|
P4 violations still caught at build time TRUE exit=1
|
||||||
|
P5 codegen drops below the 4661 baseline FALSE 4962, +301
|
||||||
|
|
||||||
|
P5 is the finding. The TRAVERSAL is irreducible -- you must walk the AST to
|
||||||
|
find calls, and those ~120 lines do not move no matter who decides. What is not
|
||||||
|
irreducible is the rule (which names) or the decision (#error). Those left. I
|
||||||
|
predicted the whole 223 lines would go because I had not separated walking from
|
||||||
|
adjudicating.
|
||||||
|
|
||||||
|
Still compiled, and measured rather than assumed: the capability-tier system
|
||||||
|
(cap_check_call, is_self_formation_call, is_dharma_call, is_llm_call,
|
||||||
|
cap_record_violation, emit_cap_violations) is 76 lines of the same shape --
|
||||||
|
prohibits_WITHIN rather than prohibits_outside, so the checker needs the
|
||||||
|
opposite polarity to absorb it.
|
||||||
|
|
||||||
|
98/98 native, 4/4 prohibition_query.sh, 7/7 seam_binding.sh, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `60c07ad`
|
||||||
|
|
||||||
|
```
|
||||||
|
land prohibition-as-query: the emitter records, it no longer adjudicates
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# policy inside the compiler
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `c2d9596`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: the capability tier becomes shipped policy plus a query
|
||||||
|
|
||||||
|
Capability differs from prohibits_outside in one way that matters: a utility
|
||||||
|
program cannot be trusted to declare its own restrictions, because it would
|
||||||
|
declare none. So the policy comes from OUTSIDE the program -- it ships with the
|
||||||
|
language as data, editable without a compiler release.
|
||||||
|
|
||||||
|
tools/check/capabilities.rel 18 names that were string literals in codegen
|
||||||
|
tools/check/capabilities.sh the query that decides
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 codegen emits kind + call graph, drops the 4 name tests TRUE zero #errors
|
||||||
|
P2 the 18 literals become a data file TRUE
|
||||||
|
P3 the checker catches capability violations TRUE exit=1
|
||||||
|
P4 codegen drops ~76 lines TRUE 4963 -> 4881
|
||||||
|
P5 below the 4661 baseline FALSE ~+230
|
||||||
|
|
||||||
|
TWO DEFECTS THE HARNESS FOUND THAT READING WOULD NOT HAVE
|
||||||
|
|
||||||
|
1. Calls inside main became invisible. cg_fn returns early for main -- C
|
||||||
|
provides its own -- so hooking the recording there left every call in main
|
||||||
|
unrecorded: a blind spot exactly where a program does its work. The old
|
||||||
|
cap_check_call ran from cg_expr and did see main. Moved the recording to
|
||||||
|
cg_expr.
|
||||||
|
|
||||||
|
2. Caller attribution was stale. __cg_current_fn kept whatever cg_fn set last,
|
||||||
|
so a violation in main was reported against the previously emitted function.
|
||||||
|
The test still PASSED, because the violation was detected -- only the name
|
||||||
|
was wrong, and a diagnostic naming the wrong fn is worse than none. Fixed at
|
||||||
|
all three main-emission sites; the first patch missed two because the live
|
||||||
|
path is codegen_streaming.
|
||||||
|
|
||||||
|
98/98 native, 7/7 + 4/4 + 5/5 integration, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `29f78f9`
|
||||||
|
|
||||||
|
```
|
||||||
|
land capability-as-policy: eighteen literals become a data file
|
||||||
|
```
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# a second copy of the header
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `9cc6040`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: derive arity from the runtime's own declarations
|
||||||
|
|
||||||
|
codegen.el carried builtin_arity(): 344 lines, 300 entries, a hand-maintained
|
||||||
|
second copy of el_runtime.h.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 the table duplicates the header TRUE 243 shared names
|
||||||
|
P2 they have already drifted FALSE ZERO drift. The
|
||||||
|
duplicate had been
|
||||||
|
maintained correctly.
|
||||||
|
P3 codegen can emit call-arity relations TRUE
|
||||||
|
P4 the check becomes a query against the header TRUE
|
||||||
|
P5 codegen drops to roughly baseline TRUE 4903 -> 4512,
|
||||||
|
149 BELOW the 4661
|
||||||
|
it started at
|
||||||
|
|
||||||
|
P2 being false is the better result: the table was not WRONG, it was
|
||||||
|
INCOMPLETE. 110 functions the runtime declares had no entry, so calling them
|
||||||
|
with the wrong argument count produced no El-level diagnostic at all. Measured:
|
||||||
|
the old compiler reports 0 arity errors for __http_do_map_to_file(1); the query
|
||||||
|
reports "takes 5 arguments, called with 1".
|
||||||
|
|
||||||
|
Deriving from the header fixes coverage AND makes drift impossible by
|
||||||
|
construction. 503 signatures, versus 300 entries maintained by hand.
|
||||||
|
|
||||||
|
THREE DEFECTS IN MY OWN CHECKER, each found by running it rather than reading it
|
||||||
|
1. El names and C names differ -- `println` is `__println`. 60 of 500 decls
|
||||||
|
carry the prefix and codegen owns the mapping; the old table carried both
|
||||||
|
keys. One rule covers all 60.
|
||||||
|
2. Multi-line declarations parsed as zero params, so the checker reported
|
||||||
|
"takes 0" for a function taking 5. A diagnostic with the wrong number in it
|
||||||
|
is worse than none -- the same shape as the stale caller attribution in the
|
||||||
|
previous pass.
|
||||||
|
3. Fixing (2) by joining lines dropped 500 signatures to 334, because a
|
||||||
|
declaration preceded by a comment no longer started its record. Comments
|
||||||
|
are stripped first now.
|
||||||
|
|
||||||
|
98/98 native, 5/5 arity_query.sh, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `d9e301b`
|
||||||
|
|
||||||
|
```
|
||||||
|
land arity-from-header: the runtime declares its own surface
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# one type erases the return
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `d2d89fc`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: temporal types as data — and the pass that GREW the compiler
|
||||||
|
|
||||||
|
This block is structurally unlike the previous four. It does not only
|
||||||
|
adjudicate, it DISPATCHES: Instant + Duration must become el_instant_add_dur,
|
||||||
|
LocalDate + Duration must become el_local_date_add_dur. The emitted C depends on
|
||||||
|
the type answer, so it cannot move to a post-hoc query. Selecting which call to
|
||||||
|
emit is an emitter's actual job.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 the block conflates dispatch with adjudication TRUE
|
||||||
|
P2 adjudication can move, dispatch cannot TRUE
|
||||||
|
P3 this pass shrinks codegen far less than the last TRUE, and worse:
|
||||||
|
4513 -> 4537, it GREW
|
||||||
|
by 24 lines
|
||||||
|
P4 the rules are affine algebra, closed by construction TRUE
|
||||||
|
P5 no type propagation -- name tracking plus a
|
||||||
|
hardcoded list of which builtins return which type TRUE, 19 names
|
||||||
|
|
||||||
|
P3 is the honest result and it is not spun: moving 19 names into a data file
|
||||||
|
cost more lines than it saved, because a generic loader is larger than the
|
||||||
|
enumeration it replaces. The win is not line count. It is that adding a 20th
|
||||||
|
temporal builtin is now a one-line edit to signatures.rel instead of a compiler
|
||||||
|
change, and that the data is inspectable.
|
||||||
|
|
||||||
|
WHY THE HEADER CANNOT SUPPLY THIS, unlike arity: el_runtime.h declares every
|
||||||
|
builtin as returning el_val_t, because El has ONE type. That single type is why
|
||||||
|
the whole seam is cheap and it is exactly why the C boundary cannot say that
|
||||||
|
now() returns an Instant while unix_seconds() returns an Int. The El-level type
|
||||||
|
is real and the boundary erases it.
|
||||||
|
|
||||||
|
INCOMPLETE, and stated rather than hidden: P2 said adjudication could move to a
|
||||||
|
query. It has NOT. Violations still emit TIME_TYPE_ERROR inline from the
|
||||||
|
emitter. Only the type DATA moved. Moving the adjudication needs the operand
|
||||||
|
types recorded as relations, which is a further pass.
|
||||||
|
|
||||||
|
98/98 native, 4/4 temporal_signatures.sh, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `e01e079`
|
||||||
|
|
||||||
|
```
|
||||||
|
land temporal signatures as data: the type table leaves, the dispatch stays
|
||||||
|
```
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# judgment lives with knowledge
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `e8e25a0`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: temporal adjudication moves out; the placeholder stays
|
||||||
|
|
||||||
|
The previous pass moved the type DATA and left the judgment inline, which I
|
||||||
|
stated rather than hid. This finishes it.
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 codegen can emit operand-type relations TRUE
|
||||||
|
"main calls temporal:instant_plus_instant"
|
||||||
|
P2 the affine rules are a small closed set as data TRUE 6 rules
|
||||||
|
P3 violations still caught at build time TRUE exit=1
|
||||||
|
P4 the reporter leaves codegen TRUE 4538 -> 4507
|
||||||
|
P5 the TIME_TYPE_ERROR placeholder must STAY TRUE
|
||||||
|
|
||||||
|
P5 is the boundary of this whole approach. The emitter has to emit SOMETHING
|
||||||
|
for an illegal expression -- it cannot emit nothing and it cannot decide what
|
||||||
|
the program meant. So the placeholder is irreducible in the same way the AST
|
||||||
|
traversal was: what moved is the judgment and the wording, not the fact that
|
||||||
|
something must be written.
|
||||||
|
|
||||||
|
The rules are affine algebra and the set is closed because there are only two
|
||||||
|
kinds of thing. An Instant is a POINT, a Duration is a DISPLACEMENT: add a
|
||||||
|
displacement to a point, subtract two points for a displacement, combine
|
||||||
|
displacements. Nothing else is meaningful, which is why the enumeration in
|
||||||
|
temporal.rel cannot grow the way an allowlist does.
|
||||||
|
|
||||||
|
A defect in my own checker, found by running it: the .rel file uses aligned
|
||||||
|
columns and my awk assumed a single space, so the message came out with the
|
||||||
|
rule key still prefixed. Same class as the multi-line header parse in the arity
|
||||||
|
pass -- formatting assumptions that only fail when you look at the output.
|
||||||
|
|
||||||
|
98/98 native, 6/6 temporal_query.sh, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `50425f3`
|
||||||
|
|
||||||
|
```
|
||||||
|
land temporal adjudication as a query: the emitter records, the rules are data
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# thirty five return types
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `cbef1c1`
|
||||||
|
|
||||||
|
```
|
||||||
|
EXPERIMENT: Int return types as data — and the bug that fell out
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 is_int_call's 35 hardcoded names move to data TRUE
|
||||||
|
P2 is_int_name stays -- it is annotation propagation TRUE
|
||||||
|
P3 the dispatch stays -- it is emission TRUE
|
||||||
|
P4 codegen shrinks ~40 lines TRUE 4507 -> 4469
|
||||||
|
P5 the design doc's characterisation is WRONG TRUE
|
||||||
|
P6 the moved data also fixes the bug it exposed TRUE
|
||||||
|
|
||||||
|
P5 CORRECTS THE RECORD. el-language-design.md and geometry-vs-code.md both cite
|
||||||
|
"== lowering to str_eq unless both operand names are in a hardcoded int-name
|
||||||
|
set -- a literal list of variable names treated as integers" as the paradigm
|
||||||
|
defect. It is not one. __int_names is populated from TYPE ANNOTATIONS
|
||||||
|
(param["type"] == "Int"), which is primitive but legitimate type propagation.
|
||||||
|
The actual defect was is_int_call: 35 hardcoded builtin return types, the same
|
||||||
|
shape as the temporal 19.
|
||||||
|
|
||||||
|
P6 IS A LIVE CORRECTNESS BUG, PRE-EXISTING, NOW FIXED
|
||||||
|
|
||||||
|
let a = str_len("hello") // no annotation
|
||||||
|
let b = str_len("hi")
|
||||||
|
let c = a + b // -> el_str_concat(a, b) on two integers
|
||||||
|
|
||||||
|
Verified identical on the pre-change compiler, so not a regression. It compiled
|
||||||
|
clean, ran, and printed NOTHING where it should print 7. No error at any layer.
|
||||||
|
|
||||||
|
The repair is three lines: an unannotated let takes its type from what the
|
||||||
|
initialiser returns. The return types were already required for dispatch and
|
||||||
|
were simply never consulted at the binding site. Moving them into data is what
|
||||||
|
made the gap visible -- reading the code for eight hours did not.
|
||||||
|
|
||||||
|
98/98 native + 2 new, 31/31 integration, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `505e5e7`
|
||||||
|
|
||||||
|
```
|
||||||
|
land int signatures, and repair a silent miscompilation they exposed
|
||||||
|
```
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# keywords that reserve nothing
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `0143cc4`
|
||||||
|
|
||||||
|
```
|
||||||
|
ANSWER: is a grammar a convention, or a region?
|
||||||
|
|
||||||
|
Both, at different layers -- and it is the same split as serialization: the
|
||||||
|
convention is the BASIS, never the ACT.
|
||||||
|
|
||||||
|
lexeme -> token `fn` means function-start because someone said so CONVENTION
|
||||||
|
shape recognition given tokens, which construct is this REGION
|
||||||
|
source -> structure parsing is transduction onto that basis GEOMETRY
|
||||||
|
byte traversal something must read them in order IRREDUCIBLE
|
||||||
|
|
||||||
|
Three things push the ACT toward region rather than convention: ambiguity
|
||||||
|
(a * b needs context; a grammar resolves it with the lexer hack, a region by
|
||||||
|
neighbourhood), error recovery (nearest-region is free), and precedence, which
|
||||||
|
is ordering along an axis with a conventional parameter.
|
||||||
|
|
||||||
|
AND THE SHOULD GATE SAYS NO TO THE OBVIOUS MOVE
|
||||||
|
|
||||||
|
Every other table this session moved to data. This one stays code. The keyword
|
||||||
|
set is CLOSED by the language definition -- it does not leak the way an
|
||||||
|
allowlist does -- and the lexer runs before the program is understood, so a
|
||||||
|
program can never declare its own keywords. Externalising it costs file I/O on
|
||||||
|
every compile and buys nothing. Same verdict as is_digit in ASCII.
|
||||||
|
|
||||||
|
WHAT WAS ACTUALLY WRONG: five of 46 keywords were consumed by no parser or
|
||||||
|
codegen path. sealed, activate, seed, protocol, impl. Each stole an identifier
|
||||||
|
from users for nothing.
|
||||||
|
|
||||||
|
SECOND SILENT MISCOMPILATION OF THE DAY. Using one did not fail to parse:
|
||||||
|
|
||||||
|
let seed = 42
|
||||||
|
let impl = seed + 1
|
||||||
|
|
||||||
|
compiled CLEAN -- zero cc errors -- and printed 0 instead of 44. No diagnostic
|
||||||
|
at any layer. Fixed by removing the five.
|
||||||
|
|
||||||
|
A DEFECT IN MY OWN MEASUREMENT, caught before it did damage: my first pass
|
||||||
|
checked only parser.el and reported `test` as inert too. codegen consumes it at
|
||||||
|
4135 for --test mode, and the tree has 408 uses. Removing it would have broken
|
||||||
|
every test in the suite. The measurement was re-run across all four consumers.
|
||||||
|
|
||||||
|
100/100 native + 2 new, 31/31 integration, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `067dd40`
|
||||||
|
|
||||||
|
```
|
||||||
|
answer the parsing question: a grammar is a basis, and five keywords reserved nothing
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# no namespacing at all
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `79f6cb7`
|
||||||
|
|
||||||
|
```
|
||||||
|
ANSWER: if the partition is a neighbourhood, does linking survive?
|
||||||
|
|
||||||
|
The question is premature, and measuring says why. El's partition is a
|
||||||
|
FILESYSTEM PATH, not a neighbourhood, and there is no namespacing at all.
|
||||||
|
|
||||||
|
MEASURED
|
||||||
|
import is textual inlining (resolve_imports), guarded against double
|
||||||
|
inclusion by a __elc_imp__:<path> state key
|
||||||
|
when a .elh header exists the header is inlined instead and the .el is marked
|
||||||
|
seen, so symbols resolve at C link time -- so linking IS real, delegated to C
|
||||||
|
two modules defining `helper` emit two C functions into one translation unit
|
||||||
|
|
||||||
|
So linking barely survives the PATH partition. Whether it survives a
|
||||||
|
neighbourhood partition cannot be asked yet.
|
||||||
|
|
||||||
|
A DIAGNOSTIC REGRESSION I CAUSED, found by asking this question. cc does catch
|
||||||
|
the collision, but reports:
|
||||||
|
|
||||||
|
error: redefinition of '__el_body_helper'
|
||||||
|
error: redefinition of '__env_helper'
|
||||||
|
error: redefinition of '__thunk_helper'
|
||||||
|
error: redefinition of 'helper'
|
||||||
|
|
||||||
|
The user's own function is FOURTH. The first three are generated symbols
|
||||||
|
introduced by the unconditional-wrapper pass earlier today -- before it, there
|
||||||
|
was one clear message. Repaired by catching the collision at El level instead:
|
||||||
|
|
||||||
|
duplicate definition: 'helper' is defined 2 times — El has no namespacing,
|
||||||
|
so imported modules share one global scope
|
||||||
|
|
||||||
|
LIMIT, stated rather than hidden: textual inlining destroys file provenance. By
|
||||||
|
the time codegen runs there is one source string, so the message can say WHICH
|
||||||
|
name collides but not which files. Naming a.el and b.el needs provenance
|
||||||
|
threaded through resolve_imports.
|
||||||
|
|
||||||
|
104/104 native, 4/4 definitions_query.sh, the compiler itself reports clean,
|
||||||
|
fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `f23cb2b`
|
||||||
|
|
||||||
|
```
|
||||||
|
answer the module question: the partition is a path, and there is no namespacing
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# tokens carry no position
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `6c975b1`
|
||||||
|
|
||||||
|
```
|
||||||
|
thread provenance through resolve_imports
|
||||||
|
|
||||||
|
The module question ended with a limit: textual inlining destroys file
|
||||||
|
provenance, so a duplicate-definition message could name the symbol but not the
|
||||||
|
files. Threading it exposed a bigger absence first.
|
||||||
|
|
||||||
|
TOKENS HAD NO POSITION AT ALL. A token was a flat (kind, value) pair, so NO
|
||||||
|
diagnostic in El could name a place -- every error named a symbol and never a
|
||||||
|
line. That is the prerequisite the module question was resting on.
|
||||||
|
|
||||||
|
THE CHAIN, end to end
|
||||||
|
lexer counts newlines; tok_append mints (kind, value, line)
|
||||||
|
parser stride 2 -> 3; tok_line added; FnDef carries its line
|
||||||
|
codegen records <fn> defines_at:<line>
|
||||||
|
resolve_imports publishes <file> spans <start> <end> for the combined source
|
||||||
|
checker maps a combined line back to file:line-within-that-file
|
||||||
|
|
||||||
|
duplicate definition: 'helper' is defined 2 times — El has no namespacing,
|
||||||
|
so imported modules share one global scope
|
||||||
|
/tmp/modtest/a.el:1
|
||||||
|
/tmp/modtest/b.el:1
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 15 stride sites, encapsulated in tok_kind/tok_value TRUE, but see below
|
||||||
|
P2 adding a line field is mechanical TRUE
|
||||||
|
P3 the lexer must count newlines TRUE
|
||||||
|
P4 resolve_imports can record per-file line ranges TRUE
|
||||||
|
P5 the message can then name both files TRUE
|
||||||
|
P6 token memory grows TRUE, 25.0 -> 33.9 MB (+36%)
|
||||||
|
|
||||||
|
FOUR DEFECTS, EACH FOUND BY RUNNING AND NOT BY READING
|
||||||
|
|
||||||
|
1. interp_tokens_append_all walks the token list DIRECTLY with its own copy of
|
||||||
|
the stride. Gen1 built fine and gen2 emitted corrupt C, because the
|
||||||
|
compiler's own source uses string interpolation. My search missed it because
|
||||||
|
I grepped for the variable name `tokens`; it is called `dst`/`result`.
|
||||||
|
Searching by name instead of by shape -- third time today.
|
||||||
|
2. tok_count in test_compiler.el carried the stride too. I had scoped the search
|
||||||
|
to compiler sources and it had escaped into the tests.
|
||||||
|
3. Nested resolve_imports calls accumulated spans into shared state, so each
|
||||||
|
republished meaningless line ranges under the parent's name. Making the
|
||||||
|
buffer local fixed it; guarding the WRITE did not, which is what I tried
|
||||||
|
first.
|
||||||
|
4. The first working version reported b.el:3 -- the COMBINED line against a
|
||||||
|
filename that has no line 3. A file:line that does not match the file is
|
||||||
|
worse than no line at all.
|
||||||
|
|
||||||
|
105/105 native, 37/37 integration, fixpoint ok, compiler self-checks clean.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `cb7289f`
|
||||||
|
|
||||||
|
```
|
||||||
|
thread provenance end to end: a diagnostic can finally name a place
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# annotations are never checked
|
||||||
|
|
||||||
|
**Status: verified on `experiment/annotation-checking`, not merged.**
|
||||||
|
|
||||||
|
## Ishikawa — why does El silently miscompile?
|
||||||
|
|
||||||
|
Three bugs found the same day shared one shape.
|
||||||
|
|
||||||
|
```
|
||||||
|
method type tracked by per-function name sets, fed from annotations
|
||||||
|
machine el_val_t erases everything at the C boundary
|
||||||
|
material no propagation through expressions
|
||||||
|
measurement nothing verifies an annotation against what it annotates
|
||||||
|
─────────────────────────────────────────────────────────────────────────
|
||||||
|
root cause El has type ANNOTATIONS but no type CHECKING. The annotation
|
||||||
|
feeds dispatch and is never itself verified.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Predictions
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 let x: Int = "hello" compiles clean expect TRUE
|
||||||
|
P2 let s: String = 42 compiles clean expect TRUE
|
||||||
|
P3 the annotation drives dispatch, unverified expect TRUE
|
||||||
|
P4 same root cause as all three bugs found today expect TRUE
|
||||||
|
P5 checking literal-vs-annotation catches both expect TRUE
|
||||||
|
P6 zero false positives across the compiler's source expect TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results — 6/6, and worse than a wrong answer
|
||||||
|
|
||||||
|
```
|
||||||
|
let x: Int = "hello"; x + 1 → 4343631981 a string POINTER used as an integer
|
||||||
|
let s: String = 42; println(s) → nothing address 42 dereferenced as a string
|
||||||
|
```
|
||||||
|
|
||||||
|
The first **leaks a raw memory address into program output**. The second is an
|
||||||
|
**arbitrary-read primitive** if that integer is ever attacker-influenced.
|
||||||
|
|
||||||
|
Verified: 6/6, zero false positives across the compiler's own source, fixpoint
|
||||||
|
ok, 105/105 native.
|
||||||
|
|
||||||
|
## Six Sigma
|
||||||
|
|
||||||
|
The emitter only **records** the mismatch; `tools/check/annotations.sh` decides —
|
||||||
|
consistent with every other check. Literals are checked because they are
|
||||||
|
unambiguous.
|
||||||
|
|
||||||
|
**Incomplete, stated not hidden:** only literals. `let x: Int = some_string_fn()`
|
||||||
|
still passes, because `signatures.rel` carries Int/Instant/Duration and no
|
||||||
|
String entries. That is a data gap, not a capability limit — every El function
|
||||||
|
declares its return type in source and codegen already holds `ret_type` on every
|
||||||
|
`FnDef`.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# async — half expressible, and the cycle that was dogma
|
||||||
|
|
||||||
|
**Status: replicated and corroborated. Three runs — the first was invalid.**
|
||||||
|
|
||||||
|
> **Chain of custody note, 2026-08-17.** The original measurements were produced
|
||||||
|
> by a C stub written in `/tmp`, and that artifact was destroyed when the session
|
||||||
|
> worktrees were removed. For a period this file asserted results with nothing
|
||||||
|
> behind them — a claim inside an evidence record, which is the defect that turns
|
||||||
|
> a chain into a pile. It was **rerun**, not reconstructed: reconstructing the
|
||||||
|
> missing file would have been a fabrication with a fresh timestamp.
|
||||||
|
>
|
||||||
|
> The fixture now lives at `lang/tests/integration/fixtures/future.c` and the
|
||||||
|
> harness at `lang/tests/integration/async_future.sh`, so a third party can
|
||||||
|
> reproduce this without taking my word for it. **6/6.**
|
||||||
|
>
|
||||||
|
> The replication is labelled as such: the outcomes were already known when the
|
||||||
|
> harness was written, so its expectations are not predictions committed in
|
||||||
|
> advance. Its value is reproducibility, not foresight.
|
||||||
|
|
||||||
|
## The first attempt was DOGMA, not science
|
||||||
|
|
||||||
|
I had just finished arguing that `@async` was expressible, then ran something to
|
||||||
|
confirm it. **No prediction was committed.** The test was rigged in a way that
|
||||||
|
should have been visible while writing it:
|
||||||
|
|
||||||
|
```c
|
||||||
|
pthread_create(&t,NULL,runner,NULL); pthread_join(t,NULL);
|
||||||
|
```
|
||||||
|
|
||||||
|
`join` immediately after `create` — the caller blocks until the body finishes.
|
||||||
|
That is a thread round-trip, not deferral. And the test printed the word
|
||||||
|
`DEFERRED` itself: I wrote the conclusion into the output and read it back.
|
||||||
|
|
||||||
|
```
|
||||||
|
Ishikawa on the rigged test
|
||||||
|
method ran after concluding, not to decide
|
||||||
|
machine nothing forces a prediction before execution
|
||||||
|
material the assertion was written into the output string
|
||||||
|
measurement no falsification criterion existed, so nothing could fail
|
||||||
|
root cause the test was authored by the party holding the conclusion,
|
||||||
|
with no commitment made before it ran
|
||||||
|
```
|
||||||
|
|
||||||
|
Discarded and re-run properly.
|
||||||
|
|
||||||
|
## Second run — predictions committed first
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 the caller proceeds while the body runs expect TRUE
|
||||||
|
P2 interleaving is observable in timestamps expect TRUE
|
||||||
|
P3 the result cannot be retrieved — one 64-bit slot, no
|
||||||
|
future type, so the wrap either blocks or returns
|
||||||
|
something that is not the result expect TRUE
|
||||||
|
P4 therefore HALF expressible: fire-and-forget yes, await no expect TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results — 4/4
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 18 us] wrap RETURNS to caller
|
||||||
|
[ 29 us] body START
|
||||||
|
caller continues, got 0
|
||||||
|
[ 50176 us] body END (computed 42)
|
||||||
|
caller done
|
||||||
|
```
|
||||||
|
|
||||||
|
The caller got **0, not 42**. Both of my earlier claims were wrong in opposite
|
||||||
|
directions: "not expressible" was too strong — fire-and-forget works today,
|
||||||
|
bound after the build, no compiler change. "Expressible" was too strong the
|
||||||
|
other way.
|
||||||
|
|
||||||
|
## Follow-on cycle — a future is one more tagged object
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 el_val_t already carries tagged heap objects TRUE 5 magic tags exist
|
||||||
|
P2 a future is one more TRUE
|
||||||
|
P3 the caller awaits and gets 42 TRUE
|
||||||
|
P4 ZERO compiler changes TRUE runtime C + one binding
|
||||||
|
P5 the unbound path still works FALSE SIGSEGV
|
||||||
|
```
|
||||||
|
|
||||||
|
**P4 is the result.** `@async` — called unexpressible for hours — needs no
|
||||||
|
compiler change. A future is one more magic-tagged heap object; `defer` returns
|
||||||
|
the handle, `el_await` blocks.
|
||||||
|
|
||||||
|
**P5 is the failure that mattered.** Sixty seconds after diagnosing
|
||||||
|
`let s: String = 42` as an arbitrary read, I wrote the identical defect into
|
||||||
|
`el_await`: reading `->magic` off an unvalidated slot. That opened cycle 19.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# a convention is not a gate
|
||||||
|
|
||||||
|
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||||
|
commit message as written at the time, before the outcome was known to anyone
|
||||||
|
reading this file.
|
||||||
|
|
||||||
|
## Record — `9a6c161`
|
||||||
|
|
||||||
|
```
|
||||||
|
a slot must be validated before it is dereferenced
|
||||||
|
|
||||||
|
ISHIKAWA: el_val_t carries integers AND tagged heap pointers, so "is this a
|
||||||
|
pointer" is undecidable without checking first. That check was a CONVENTION
|
||||||
|
every author had to know rather than a GATE they had to pass through, and
|
||||||
|
looks_like_heap_obj was static -- so every sibling translation unit re-derived
|
||||||
|
it.
|
||||||
|
|
||||||
|
MEASURED, across the five existing tags
|
||||||
|
geom_of looks_like_heap_obj full guard correct
|
||||||
|
mfld_of looks_like_heap_obj full guard correct
|
||||||
|
el_bin_lookup (uintptr_t)p < 4096 floor only reads 8 bytes BACKWARD
|
||||||
|
el_input_len s ? ... : 0 NULL only strlen's an integer
|
||||||
|
|
||||||
|
sha256_hex(50000) -> exit 139, SIGSEGV, compiled clean
|
||||||
|
|
||||||
|
PREDICTIONS AND RESULTS
|
||||||
|
P1 looks_like_heap_obj is static, not exported TRUE
|
||||||
|
P2 each tagged type re-derives the check TRUE
|
||||||
|
P3 at least one is missing guard components TRUE (two are)
|
||||||
|
P6 sha256_hex(<int>) reads out of bounds TRUE
|
||||||
|
P8 routing el_bin_lookup through the gate fixes it FALSE
|
||||||
|
P9 the legitimate hash is unchanged TRUE
|
||||||
|
P11 fixpoint and suites hold TRUE
|
||||||
|
|
||||||
|
P8 IS THE USEFUL FAILURE. Guarding the tagged lookup changed nothing --
|
||||||
|
looks_like_heap_obj(49992) correctly returns 0, el_bin_lookup bails, and then
|
||||||
|
el_input_len falls through to strlen() on address 50000. The FALLBACK was the
|
||||||
|
hazard, not the tagged path. A NULL check does not establish that a slot is a
|
||||||
|
pointer. I would have shipped the wrong fix and called it verified.
|
||||||
|
|
||||||
|
A MEASUREMENT DEFECT, fourth today: my first run of the crash reported exit=0,
|
||||||
|
because $? read head's exit through a pipe rather than the program's. I nearly
|
||||||
|
recorded a segfault as a clean run. Same shape as grepping only parser.el and
|
||||||
|
searching by variable name instead of by operation.
|
||||||
|
|
||||||
|
AND I PROVED THE HAZARD FROM THE INSIDE. Sixty seconds after diagnosing
|
||||||
|
`let s: String = 42` as an arbitrary-read primitive, I wrote the identical
|
||||||
|
defect into el_await -- dereferencing ->magic off an unvalidated slot -- and
|
||||||
|
only then found the runtime had already made it twice.
|
||||||
|
|
||||||
|
el_tagged() is now exported in el_runtime.h. Anything that dereferences a slot
|
||||||
|
without passing through it is the defect.
|
||||||
|
|
||||||
|
105/105 native, 42/42 integration across eight harnesses, fixpoint ok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Record — `3049a70`
|
||||||
|
|
||||||
|
```
|
||||||
|
make the guard a gate: sha256_hex(50000) no longer segfaults
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Architecture questions closed
|
||||||
|
|
||||||
|
All five were open in `geometry-vs-code.md`. Each was closed by measurement, not
|
||||||
|
by argument.
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|---|---|
|
||||||
|
| **Concurrency** — hardware threads are code, but is *ordering* geometric? | **Ordering is geometric.** Causality is a partial order (Lamport 1978); a total order is an arbitrary extension that "cannot be depended on to imply a causal relationship." Languages force a total order at authoring time, so every lock, barrier and fence is apparatus for recovering the partial order that was destroyed. CALM: a program has a coordination-free implementation **iff monotone**. What breaks monotonicity is destructive update. **Coordination is the price of forgetting.** |
|
||||||
|
| **Error handling** — does `grounded: false` cover *failed*? | **No.** Standing is a *signed* component: `>0` supported, `=0` unknown, `<0` contradicted. Not-known and known-false are opposite directions on one axis; a boolean cannot tell them apart. `inhibitory` as an int32 flag is that sign wearing a boolean. |
|
||||||
|
| **Parsing** — is a grammar a convention, or a region? | **A grammar is a basis; parsing is transduction onto it.** Lexeme→token is convention, shape recognition is a region, byte traversal is irreducible. **But the SHOULD gate refused the obvious move:** the keyword table stays code, because the set is closed by the language definition and the lexer runs before the program is understood. Same verdict as `is_digit` in ASCII. |
|
||||||
|
| **Numeric literals** — is `3` a position or a convention? | **The numeral is convention; the number is a position — and a bare `3` is a magnitude with no axis.** It is not a position until something gives it a direction, which is why `3.days` needs a calendar. Demonstrated: `t + 3` refused, `t + 1.hour` accepted. |
|
||||||
|
| **The module system** — if the partition is a neighbourhood, does linking survive? | **Premature.** The partition is a filesystem path and there is no namespacing at all. `import` is textual inlining; with a `.elh` header, symbols resolve at C link time. Two modules defining `helper` emit two C functions into one translation unit. Linking barely survives the *path* partition. |
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Live defects found
|
||||||
|
|
||||||
|
Every one compiled clean, ran, and produced a wrong result or a crash with **no
|
||||||
|
diagnostic at any layer**. All four were present before this session; none was
|
||||||
|
introduced by it.
|
||||||
|
|
||||||
|
## Silent miscompilations
|
||||||
|
|
||||||
|
### 1. An unannotated `let` loses its type
|
||||||
|
|
||||||
|
```el
|
||||||
|
let a = str_len("hello") // no annotation
|
||||||
|
let b = str_len("hi")
|
||||||
|
let c = a + b // → el_str_concat(a, b) on two integers
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiled clean. Printed **nothing** where it should print 7. Fixed: an
|
||||||
|
unannotated `let` takes its type from what its initialiser returns. The return
|
||||||
|
types were already required for dispatch and were simply never consulted at the
|
||||||
|
binding site.
|
||||||
|
|
||||||
|
### 2. Reserved keywords that reserved nothing
|
||||||
|
|
||||||
|
```el
|
||||||
|
let seed = 42
|
||||||
|
let impl = seed + 1
|
||||||
|
```
|
||||||
|
|
||||||
|
`sealed`, `activate`, `seed`, `protocol`, `impl` were keywords in the lexer and
|
||||||
|
consumed by no parser or codegen path. Using one did not fail to parse — it
|
||||||
|
compiled clean, with zero `cc` errors, and printed **0 instead of 44**. Fixed by
|
||||||
|
removing all five.
|
||||||
|
|
||||||
|
### 3. `Instant + Int` was never refused
|
||||||
|
|
||||||
|
```el
|
||||||
|
let t: Instant = now()
|
||||||
|
let u: Instant = t + 3 // → (t + 3), reported clean
|
||||||
|
```
|
||||||
|
|
||||||
|
`Duration + Int` was refused — *"an Int carries no unit"* — while adding a
|
||||||
|
dimensionless number to a **point** silently moved the instant by an
|
||||||
|
unspecified amount. Three of *what*? Whatever the representation happens to be.
|
||||||
|
The rule was simply never written.
|
||||||
|
|
||||||
|
## Security-relevant
|
||||||
|
|
||||||
|
### 4. Annotations are never verified
|
||||||
|
|
||||||
|
```el
|
||||||
|
let x: Int = "hello"; x + 1 → 4343631981 a string POINTER used as an integer
|
||||||
|
let s: String = 42; println(s) → nothing address 42 dereferenced
|
||||||
|
```
|
||||||
|
|
||||||
|
The first **leaks a raw memory address into program output**. The second is an
|
||||||
|
**arbitrary-read primitive** if the integer is ever attacker-influenced.
|
||||||
|
|
||||||
|
### 5. `sha256_hex(<integer>)` segfaults
|
||||||
|
|
||||||
|
```el
|
||||||
|
let h: String = sha256_hex(50000) → exit 139, SIGSEGV
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiled clean. `el_bin_lookup` checked only a 4096 floor — no alignment, no
|
||||||
|
small-int, no negative — and reads **eight bytes backward** from the pointer.
|
||||||
|
And the actual crash was one level further on: `el_input_len` fell through to
|
||||||
|
`strlen()` on address 50000, because a NULL check does not establish that a slot
|
||||||
|
is a pointer.
|
||||||
|
|
||||||
|
Fixed, and the guard is now a **gate**: `el_tagged()` is exported in
|
||||||
|
`el_runtime.h`. `geom_of` and `mfld_of` were always correct because their authors
|
||||||
|
knew to call `looks_like_heap_obj`; `el_bin_lookup` and `el_input_len` were wrong
|
||||||
|
because theirs did not, and the function was `static`, so every sibling
|
||||||
|
translation unit re-derived it.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Defects in my own measurement
|
||||||
|
|
||||||
|
Recorded because the pattern is the point: **five of these, and every one is the same shape —
|
||||||
|
reading a proxy instead of the thing.** A file instead of the operation, a
|
||||||
|
variable name instead of the shape, a scope instead of the whole, a pipe's exit
|
||||||
|
instead of the program's, a line count instead of the object identity. Each was caught
|
||||||
|
by running something, never by reading.
|
||||||
|
|
||||||
|
### 1. Scoped the search to one file
|
||||||
|
|
||||||
|
Reported `test` as an inert keyword by checking only `parser.el`. **codegen**
|
||||||
|
consumes it at 4135 for `--test` mode, and the tree has 408 uses. Removing it
|
||||||
|
would have broken every test in the suite — including the ones used to verify
|
||||||
|
the removal.
|
||||||
|
|
||||||
|
### 2. Searched by variable name, not by operation
|
||||||
|
|
||||||
|
Grepped for `native_list_append(tokens` to find direct token appends.
|
||||||
|
`interp_tokens_append_all` calls its parameters `dst`/`result`, carries its own
|
||||||
|
copy of the stride, and corrupted generation 2 — while generation 1 built fine,
|
||||||
|
because the compiler's own source uses string interpolation.
|
||||||
|
|
||||||
|
### 3. Scoped to compiler sources; the stride had escaped into tests
|
||||||
|
|
||||||
|
`tok_count` in `test_compiler.el` computed `len/2` independently. 21 tests failed
|
||||||
|
after the token layout changed.
|
||||||
|
|
||||||
|
### 4. Read the wrong exit code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
timeout 10 /tmp/leakrun 2>&1 | head -2; echo "exit=$?" # reports head's exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Reported `exit=0` for a program that was returning **139 (SIGSEGV)**. I nearly
|
||||||
|
recorded a segfault as a clean run.
|
||||||
|
|
||||||
|
### 5. Read a count that was not counting
|
||||||
|
|
||||||
|
Comparing the three promoted branches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for pair in "dev stage" ...; do set -- $pair
|
||||||
|
n=$(git diff --stat origin/$1 origin/$2 | wc -l) # git errored to STDERR
|
||||||
|
... # wc counted empty STDOUT
|
||||||
|
```
|
||||||
|
|
||||||
|
`git diff` failed on a malformed revision, wrote its error to stderr, and `wc -l`
|
||||||
|
counted zero lines of stdout. Three confident `IDENTICAL` results, all
|
||||||
|
meaningless. **Had the trees actually differed, I would have reported the
|
||||||
|
promotion clean.**
|
||||||
|
|
||||||
|
Redone correctly, the three trees share one hash — `2acd9374` — which is the
|
||||||
|
check that should have been run first: not "how many files differ" but "is the
|
||||||
|
tree object the same object".
|
||||||
|
|
||||||
|
### And one that was not a measurement defect but a method defect
|
||||||
|
|
||||||
|
One cycle was run **without committing predictions first** — see
|
||||||
|
`cycles/18-async-half-expressible.md`. The test joined the thread immediately
|
||||||
|
after creating it and printed the word `DEFERRED` itself. A test authored by the
|
||||||
|
party holding the conclusion, with nothing committed beforehand, cannot fail.
|
||||||
|
It had to be discarded and re-run.
|
||||||
+212
-13
@@ -23,16 +23,26 @@
|
|||||||
// warning. The runtime takes an exclusive flock at startup and a second start
|
// warning. The runtime takes an exclusive flock at startup and a second start
|
||||||
// is refused loudly with the holder's pid.
|
// is refused loudly with the holder's pid.
|
||||||
//
|
//
|
||||||
// NOT declared here, on purpose: ENGRAM_DATA_DIR. Its resolution is owned by
|
// guards: names WHAT the singleton protects — this program's data directory. The
|
||||||
// engram_resolve_data_dir() (el_runtime.c), which defaults to $HOME/.neuron/engram
|
// lock lives inside it, so the guard is keyed on the store and not on the word
|
||||||
// and fails LOUD rather than silently persisting to an ephemeral directory.
|
// "engram": two engrams against the same store cannot both run no matter how the
|
||||||
// Declaring a default for it here as well would put the data dir's fallback in
|
// environment is spelled, and two engrams against DIFFERENT stores are not each
|
||||||
// two places — which is precisely the defect this migration removes (until
|
// other's business and are not refused. Until 2026-08-16 the lock was keyed on
|
||||||
// 2026-08-15 the reseed backup path carried its own "/tmp/engram" default that
|
// the program name and $TMPDIR, and both of those sentences were false.
|
||||||
// disagreed with the resolver, so the pre-destructive safety copy landed in /tmp).
|
//
|
||||||
|
// It names the resolver rather than restating its path, for the same reason
|
||||||
|
// ENGRAM_DATA_DIR is NOT declared as an `env` entry below: engram_resolve_data_dir()
|
||||||
|
// (el_runtime.c) owns that path — it defaults to $HOME/.neuron/engram and fails
|
||||||
|
// LOUD rather than silently persisting to an ephemeral directory. Restating the
|
||||||
|
// default here would give the data dir two owners that can disagree, which is
|
||||||
|
// precisely the defect this migration removes (until 2026-08-15 the reseed backup
|
||||||
|
// path carried its own "/tmp/engram" default that disagreed with the resolver, so
|
||||||
|
// the pre-destructive safety copy landed in /tmp). A guard that resolved the path
|
||||||
|
// its own way could guard a directory the program never writes to.
|
||||||
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
||||||
program "engram" {
|
program "engram" {
|
||||||
singleton: "engram"
|
singleton: "engram"
|
||||||
|
guards: engram_resolve_data_dir()
|
||||||
|
|
||||||
// ── Core server ──
|
// ── Core server ──
|
||||||
env ENGRAM_BIND: String = ":8742"
|
env ENGRAM_BIND: String = ":8742"
|
||||||
@@ -1469,9 +1479,16 @@ fn route_guide_summon(method: String, path: String, body: String) -> String {
|
|||||||
//
|
//
|
||||||
// The SINGLE NODE is the DEGENERATE n=1 case of this SAME operation — not a
|
// The SINGLE NODE is the DEGENERATE n=1 case of this SAME operation — not a
|
||||||
// separate CRUD path:
|
// separate CRUD path:
|
||||||
// write(content) = reframe(region=∅, manifold=[1 node]) (route_write)
|
// write(signal) = realize(signal) → reframe(region=∅, manifold) (route_write)
|
||||||
// supersede(id,new) = reframe(region={id}, manifold=[1 node]) (route_supersede)
|
// supersede(id,new) = reframe(region={id}, manifold=[1 node]) (route_supersede)
|
||||||
// relate(a,b,rel) = the rebind sub-op in isolation (route_create_edge)
|
// relate(a,b,rel) = the rebind sub-op in isolation (route_create_edge)
|
||||||
|
//
|
||||||
|
// CORRECTED 2026-08-16: write was documented above as
|
||||||
|
// "reframe(region=∅, manifold=[1 node])", and the "[1 node]" was not the design
|
||||||
|
// — it was the DEFECT. A node is an OUTPUT of realization, never an INPUT to
|
||||||
|
// it. What arrives at an intake route is a SIGNAL, and how many nodes it
|
||||||
|
// becomes is for the realizer to say, not for the route to assume. See
|
||||||
|
// "INTAKE" below.
|
||||||
// The ONLY anti-pattern is decomposing a region-scale change into a LOOP of
|
// The ONLY anti-pattern is decomposing a region-scale change into a LOOP of
|
||||||
// independent top-level per-node updates. Here the region is the unit: one
|
// independent top-level per-node updates. Here the region is the unit: one
|
||||||
// isolate, one atomic set-replace, one persist, one verify — iterating members
|
// isolate, one atomic set-replace, one persist, one verify — iterating members
|
||||||
@@ -1686,6 +1703,174 @@ fn reframe_core(region: [String], manifold: String, reason: String, do_rebind: I
|
|||||||
",\"keystones_protected\":true}"
|
",\"keystones_protected\":true}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// INTAKE — the ONE door: signal → realization → manifold → store.
|
||||||
|
//
|
||||||
|
// THERE IS NO WRITE NODE. What arrives at an intake route is a SIGNAL. A node
|
||||||
|
// is an OUTPUT of realization, never an INPUT to it. route_write used to say:
|
||||||
|
//
|
||||||
|
// let manifold: String = "[" + body + "]" // the body IS a valid manifold node object
|
||||||
|
//
|
||||||
|
// and hand that to reframe_core. That is not a manifold — it is the request
|
||||||
|
// body wearing the word, and the comment stated the wrong assumption out loud.
|
||||||
|
// It is why a compound signal landed as ONE flat node with ZERO edges. Measured
|
||||||
|
// before this change, on a cp -Rc clone:
|
||||||
|
// POST /api/write {"type":"memory","content":"A cathedral is stone holding a
|
||||||
|
// shape that stone alone would not hold."}
|
||||||
|
// → {"ok":true,"inserted":1,"nodes_added":1,"edges_added":0,...}
|
||||||
|
// GET /api/neighbors/<new id> → [] (read back out, not taken on trust)
|
||||||
|
//
|
||||||
|
// NOTHING IS DECOMPOSED HERE, AND NOTHING MAY EVER BE. transduce(signal,
|
||||||
|
// modality) IS the realization primitive (el_runtime.c: "Manifold",
|
||||||
|
// "Realizers + transduce"). It dispatches through the dlsym realizer registry,
|
||||||
|
// so ADDING A MODALITY IS REGISTERING A REALIZER — never an edit to this file,
|
||||||
|
// and never a patch to the runtime. This function only carries what the
|
||||||
|
// primitive returns into the store, which is the one thing the engram's HTTP
|
||||||
|
// surface has never done: `grep -n 'transduce\|realize\|Manifold\|decompos'
|
||||||
|
// engram/src/server.el` returned exactly one line before this change, a comment.
|
||||||
|
//
|
||||||
|
// GENERAL BY CONSTRUCTION, NOT SPECIAL-CASED TO route_write. Five of the six
|
||||||
|
// intake doors (write, supersede, nodes, neuron/knowledge/capture,
|
||||||
|
// neuron/state-events) are the same hand-written "content string →
|
||||||
|
// engram_node_full → one flat node", differing ONLY in the node_type / tier /
|
||||||
|
// tags they hardcode. Those are parameters here, so each door can be moved onto
|
||||||
|
// this one function as it is transitioned. Only /api/write rides it in this
|
||||||
|
// pass; the rest are listed as remaining work.
|
||||||
|
//
|
||||||
|
// WHEN THERE IS NO ORGAN the signal is stored flat exactly as before, and the
|
||||||
|
// response SAYS SO ("realized":false, "organ":false). Silent flattening is the
|
||||||
|
// actual defect — a caller could not distinguish "nothing decomposed me" from
|
||||||
|
// "I decomposed into one component". el_runtime.c draws the same line at
|
||||||
|
// registration time, between an absent organ and a broken one, for the same
|
||||||
|
// reason: those two must not look alike.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Resolve a component KEY to the node id it was inserted as. Components are
|
||||||
|
// addressed BY KEY, never by index (el_runtime.c, "Manifold"), because the key
|
||||||
|
// is what survives persistence — so relations are resolved by key too.
|
||||||
|
fn key_to_id(keys: [String], ids: [String], key: String) -> String {
|
||||||
|
let n: Int = el_list_len(keys)
|
||||||
|
let i: Int = 0
|
||||||
|
while i < n {
|
||||||
|
if str_eq(el_list_get(keys, i), key) { return el_list_get(ids, i) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intake_signal(signal: String, modality: String, region: [String],
|
||||||
|
nt_in: String, tier_in: String, tags: String,
|
||||||
|
reason: String, do_rebind: Int) -> String {
|
||||||
|
let n_before: Int = engram_node_count()
|
||||||
|
let e_before: Int = engram_edge_count()
|
||||||
|
let region_n: Int = el_list_len(region)
|
||||||
|
let tomb: String = if region_n > 0 { supersede_set(region, reason) } else { "" }
|
||||||
|
|
||||||
|
// Identity can never be minted through intake — the same rule
|
||||||
|
// insert_manifold_json holds, applied at the one door instead of per-route.
|
||||||
|
let nt: String = if str_eq(nt_in, "") { "Memory" } else { nt_in }
|
||||||
|
if str_eq(nt, "self") { nt = "Memory" }
|
||||||
|
if str_eq(nt, "values") { nt = "Memory" }
|
||||||
|
let tier: String = if str_eq(tier_in, "") { "Working" } else { tier_in }
|
||||||
|
|
||||||
|
let has_organ: Int = realizer_has(modality)
|
||||||
|
let new_ids: [String] = el_list_empty()
|
||||||
|
let keys: [String] = el_list_empty()
|
||||||
|
let ncomp: Int = 0
|
||||||
|
let nrel: Int = 0
|
||||||
|
let realized: Int = 0
|
||||||
|
|
||||||
|
if has_organ > 0 {
|
||||||
|
let m: Manifold = transduce(signal, modality)
|
||||||
|
// A realizer that returns a bare Geometry transduces NOTHING by design
|
||||||
|
// (el_runtime.c) — manifold_is() is the check, so a fingerprinting organ
|
||||||
|
// is not silently mistaken for a decomposing one.
|
||||||
|
if manifold_is(m) > 0 {
|
||||||
|
realized = 1
|
||||||
|
ncomp = manifold_size(m)
|
||||||
|
let i: Int = 0
|
||||||
|
let prev: String = ""
|
||||||
|
while i < ncomp {
|
||||||
|
let key: String = manifold_key(m, i)
|
||||||
|
let role: String = manifold_role(m, i)
|
||||||
|
// The component's OWN geometry, at its own width — this is the
|
||||||
|
// whole point of a manifold over a fingerprint, and it is why
|
||||||
|
// node_attach_geometry is used rather than re-embedding the
|
||||||
|
// component's name as text.
|
||||||
|
let g: Geometry = manifold_geometry(m, i)
|
||||||
|
let ctags: String = "[\"component\",\"role:" + role + "\",\"modality:" + modality + "\"]"
|
||||||
|
let cid: String = engram_node_full(key, nt, key, 0.5, 0.5, 0.9, tier, ctags)
|
||||||
|
let landed: Int = node_attach_geometry(cid, g)
|
||||||
|
let freed: Int = geometry_free(g)
|
||||||
|
new_ids = el_list_append(new_ids, cid)
|
||||||
|
keys = el_list_append(keys, key)
|
||||||
|
// PRESERVED CONTRACT: manifold_member wires the inserted set
|
||||||
|
// into one connected sub-graph, exactly as insert_manifold_json
|
||||||
|
// already did. Not reinvented — reused.
|
||||||
|
if !str_eq(prev, "") { engram_connect(prev, cid, 0.6, "manifold_member") }
|
||||||
|
prev = cid
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
// THE RELATIONS ARE THE CONTENT. Relation weight IS the grounding
|
||||||
|
// (correspondence-and-censorship §1) — it arrives on the edge from
|
||||||
|
// the realizer and nothing here computes or second-guesses it.
|
||||||
|
nrel = manifold_rel_count(m)
|
||||||
|
let j: Int = 0
|
||||||
|
while j < nrel {
|
||||||
|
let fk: String = manifold_rel_from(m, j)
|
||||||
|
let rn: String = manifold_rel_name(m, j)
|
||||||
|
let tk: String = manifold_rel_to(m, j)
|
||||||
|
let w: Float = manifold_rel_weight(m, j)
|
||||||
|
let fid: String = key_to_id(keys, new_ids, fk)
|
||||||
|
let tid: String = key_to_id(keys, new_ids, tk)
|
||||||
|
if !str_eq(fid, "") {
|
||||||
|
if !str_eq(tid, "") {
|
||||||
|
engram_connect(fid, tid, w, rn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j = j + 1
|
||||||
|
}
|
||||||
|
let mfreed: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO ORGAN: store the signal flat, as before — but say so. This is the
|
||||||
|
// pre-existing behaviour preserved verbatim, not a new fallback path.
|
||||||
|
if realized == 0 {
|
||||||
|
let label: String = str_slice(signal, 0, 60)
|
||||||
|
let fid: String = engram_node_full(signal, nt, label, 0.5, 0.5, 0.9, tier, tags)
|
||||||
|
new_ids = el_list_append(new_ids, fid)
|
||||||
|
}
|
||||||
|
|
||||||
|
let inserted: Int = el_list_len(new_ids)
|
||||||
|
let bound: Int = if do_rebind > 0 { rebind_cosine(new_ids, tomb) } else { 0 }
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
|
let new_csv: String = ""
|
||||||
|
let k: Int = 0
|
||||||
|
while k < inserted {
|
||||||
|
let sep: String = if k == 0 { "" } else { "," }
|
||||||
|
new_csv = new_csv + sep + "\"" + el_list_get(new_ids, k) + "\""
|
||||||
|
k = k + 1
|
||||||
|
}
|
||||||
|
let realized_s: String = if realized > 0 { "true" } else { "false" }
|
||||||
|
let organ_s: String = if has_organ > 0 { "true" } else { "false" }
|
||||||
|
return "{\"ok\":true,\"region_superseded\":" + int_to_str(region_n) +
|
||||||
|
",\"tombstone_id\":\"" + tomb + "\"" +
|
||||||
|
",\"inserted\":" + int_to_str(inserted) +
|
||||||
|
",\"new_ids\":[" + new_csv + "]" +
|
||||||
|
",\"edges_rebound\":" + int_to_str(bound) +
|
||||||
|
",\"realized\":" + realized_s +
|
||||||
|
",\"modality\":\"" + modality + "\"" +
|
||||||
|
",\"organ\":" + organ_s +
|
||||||
|
",\"components\":" + int_to_str(ncomp) +
|
||||||
|
",\"relations\":" + int_to_str(nrel) +
|
||||||
|
",\"nodes_added\":" + int_to_str(engram_node_count() - n_before) +
|
||||||
|
",\"edges_added\":" + int_to_str(engram_edge_count() - e_before) +
|
||||||
|
",\"node_count\":" + int_to_str(engram_node_count()) +
|
||||||
|
",\"edge_count\":" + int_to_str(engram_edge_count()) +
|
||||||
|
",\"keystones_protected\":true}"
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/reframe — the universal set-based mutation.
|
// POST /api/reframe — the universal set-based mutation.
|
||||||
// Body: {vantage?, region_ids?(csv), k?, expand?, manifold(json array), reason?, rebind?}
|
// Body: {vantage?, region_ids?(csv), k?, expand?, manifold(json array), reason?, rebind?}
|
||||||
// region_ids (explicit) wins; else cosine-isolate around vantage.
|
// region_ids (explicit) wins; else cosine-isolate around vantage.
|
||||||
@@ -1722,18 +1907,32 @@ fn route_reframe(method: String, path: String, body: String) -> String {
|
|||||||
return reframe_core(region, manifold, reason, do_rebind)
|
return reframe_core(region, manifold, reason, do_rebind)
|
||||||
}
|
}
|
||||||
|
|
||||||
// write — DEGENERATE n=1 of reframe: region=∅, manifold=[1 node]. The SAME
|
// write — INTAKE OF A SIGNAL. Not "reframe with a manifold of one node": the
|
||||||
// reframe_core path. rebind off so the pure-add matches plain node creation.
|
// route no longer decides how many nodes the signal is. It hands the signal to
|
||||||
// POST /api/write {content, node_type?, tier?, tags?}
|
// the realization primitive and stores whatever manifold comes back.
|
||||||
|
//
|
||||||
|
// The line this replaces was:
|
||||||
|
// let manifold: String = "[" + body + "]" // the body IS a valid manifold node object
|
||||||
|
// which asserted that a request body is a manifold. It is not, and that single
|
||||||
|
// assertion is the whole measured defect (1 node, 0 edges, [] neighbors).
|
||||||
|
//
|
||||||
|
// rebind stays off so a pure add still matches plain node creation.
|
||||||
|
// POST /api/write {content, modality?, node_type?, tier?, tags?}
|
||||||
fn route_write(method: String, path: String, body: String) -> String {
|
fn route_write(method: String, path: String, body: String) -> String {
|
||||||
let content: String = json_get_string(body, "content")
|
let content: String = json_get_string(body, "content")
|
||||||
if str_eq(content, "") { return err_json("write: content required") }
|
if str_eq(content, "") { return err_json("write: content required") }
|
||||||
let nt: String = json_get_string(body, "node_type")
|
let nt: String = json_get_string(body, "node_type")
|
||||||
if str_eq(nt, "self") { return err_json("write: identity is write-protected") }
|
if str_eq(nt, "self") { return err_json("write: identity is write-protected") }
|
||||||
if str_eq(nt, "values") { return err_json("write: identity is write-protected") }
|
if str_eq(nt, "values") { return err_json("write: identity is write-protected") }
|
||||||
|
// The modality names which organ to sense with. It is data, never a branch:
|
||||||
|
// a new modality is a realizer_register call somewhere else in the program,
|
||||||
|
// not another endpoint and not another case here.
|
||||||
|
let mod_raw: String = json_get_string(body, "modality")
|
||||||
|
let modality: String = if str_eq(mod_raw, "") { "text" } else { mod_raw }
|
||||||
|
let tier: String = json_get_string(body, "tier")
|
||||||
|
let tags: String = json_get_raw(body, "tags")
|
||||||
let empty: [String] = el_list_empty()
|
let empty: [String] = el_list_empty()
|
||||||
let manifold: String = "[" + body + "]" // the body IS a valid manifold node object
|
return intake_signal(content, modality, empty, nt, tier, tags, "write", 0)
|
||||||
return reframe_core(empty, manifold, "write", 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// supersede — DEGENERATE n=1 of reframe: region={id}, manifold=[1 node]. The
|
// supersede — DEGENERATE n=1 of reframe: region={id}, manifold=[1 node]. The
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p0-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p0-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
@@ -14,8 +18,8 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile (plain) =="
|
echo "== compile (plain) =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p0_emb.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p0" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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"
|
D="$WORK/d"; mkdir -p "$D"
|
||||||
"$WORK/p0" "$D" || { echo "FAIL: run"; fail=1; }
|
"$WORK/p0" "$D" || { echo "FAIL: run"; fail=1; }
|
||||||
@@ -69,8 +73,8 @@ PY
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan =="
|
echo "== ASan+UBSan =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p0_emb.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p0.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -20 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p0.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DS="$WORK/ds"; mkdir -p "$DS"
|
DS="$WORK/ds"; mkdir -p "$DS"
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p1-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p1-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
@@ -14,8 +18,8 @@ unset ENGRAM_STORE ENGRAM_CONSOLIDATION ENGRAM_CONSOL_CONN_MIN ENGRAM_CONSOL_PER
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile =="
|
echo "== compile =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p1_consol.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p1" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -lm -o "$WORK/p1" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== (a) HEADLINE: hebb accrual curve over N co-activations (flag OFF, pure trunk) =="
|
echo "== (a) HEADLINE: hebb accrual curve over N co-activations (flag OFF, pure trunk) =="
|
||||||
@@ -129,8 +133,8 @@ cat "$WORK/off.txt" | sed 's/^/ /'
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan (connect + perm + accrual-short) =="
|
echo "== ASan+UBSan (connect + perm + accrual-short) =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p1_consol.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p1.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p1.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DS="$WORK/san"; mkdir -p "$DS"
|
DS="$WORK/san"; mkdir -p "$DS"
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
# Throwaway HOME + /tmp only. TC defaults to 3600s; we pin it for the math.
|
# Throwaway HOME + /tmp only. TC defaults to 3600s; we pin it for the math.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p2-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p2-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
@@ -15,8 +19,8 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile =="
|
echo "== compile =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p2_chrono.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p2" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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']))"; }
|
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']))"; }
|
||||||
|
|
||||||
@@ -78,8 +82,8 @@ python3 -c "import sys; sys.exit(0 if abs($OFFWM-1.2)<1e-9 else 1)" \
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan =="
|
echo "== ASan+UBSan =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p2_chrono.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p2.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p2.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DS="$WORK/san"; mkdir -p "$DS"
|
DS="$WORK/san"; mkdir -p "$DS"
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
# Read-only pure primitive; no store, no flag. Throwaway /tmp only.
|
# Read-only pure primitive; no store, no flag. Throwaway /tmp only.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p3-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p3-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile =="
|
echo "== compile =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p3" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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; }
|
"$WORK/p3" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||||
cat "$WORK/out.txt" | sed 's/^/ /'
|
cat "$WORK/out.txt" | sed 's/^/ /'
|
||||||
@@ -52,8 +56,8 @@ PY
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan =="
|
echo "== ASan+UBSan =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p3_drift.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p3.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p3.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
"$WORK/p3.san" >/dev/null 2>"$WORK/san.log"
|
"$WORK/p3.san" >/dev/null 2>"$WORK/san.log"
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
# M-INTEROCEPTION P4 gate: afferent input counters in act-stats (additive).
|
# M-INTEROCEPTION P4 gate: afferent input counters in act-stats (additive).
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p4-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p4-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
@@ -13,8 +17,8 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile =="
|
echo "== compile =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p4" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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; }
|
"$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
|
grep -oE 'aff_[a-z_]+":[0-9]+' "$WORK/out.txt" | sed 's/^/ /' | head -30
|
||||||
@@ -53,8 +57,8 @@ PY
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan =="
|
echo "== ASan+UBSan =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p4_afferent.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p4.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p4.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
"$WORK/p4.san" >/dev/null 2>"$WORK/san.log"
|
"$WORK/p4.san" >/dev/null 2>"$WORK/san.log"
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
# M-INTEROCEPTION P5 gate: dream-recall builtin engram_dreams_json (honesty rail).
|
# M-INTEROCEPTION P5 gate: dream-recall builtin engram_dreams_json (honesty rail).
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-p5-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-p5-XXXXXX)"
|
||||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||||
@@ -13,8 +17,8 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compile =="
|
echo "== compile =="
|
||||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p5" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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"
|
D="$WORK/d"; mkdir -p "$D"
|
||||||
"$WORK/p5" "$D" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
"$WORK/p5" "$D" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||||
@@ -57,8 +61,8 @@ PY
|
|||||||
echo
|
echo
|
||||||
echo "== ASan+UBSan =="
|
echo "== ASan+UBSan =="
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||||
-I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
-I "$INC" "$HERE/test_interoception_p5_dreams.c" $RTSRC $SSLFLAGS \
|
||||||
-lcurl -lm -o "$WORK/p5.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
-lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ -x "$WORK/p5.san" ]; then
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DS="$WORK/ds"; mkdir -p "$DS"
|
DS="$WORK/ds"; mkdir -p "$DS"
|
||||||
|
|||||||
@@ -6,8 +6,14 @@
|
|||||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-m35-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-m35-XXXXXX)"
|
||||||
BIN="$WORK/m35"
|
BIN="$WORK/m35"
|
||||||
@@ -17,7 +23,7 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m35_hebb_persist.c) =="
|
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"
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m35_hebb_persist.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -o "$BIN" 2>"$WORK/cc.log"
|
||||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -139,7 +145,7 @@ echo
|
|||||||
echo "== 5) ASan+UBSan build, exercise the full persist+reboot flow (leaks off — harness intentionally leaks el_strdup) =="
|
echo "== 5) ASan+UBSan build, exercise the full persist+reboot flow (leaks off — harness intentionally leaks el_strdup) =="
|
||||||
SANBIN="$WORK/m35.san"
|
SANBIN="$WORK/m35.san"
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
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"
|
-I "$INC" "$HERE/test_m35_hebb_persist.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -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
|
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DSAN="$WORK/san"; mkdir -p "$DSAN"
|
DSAN="$WORK/san"; mkdir -p "$DSAN"
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME + ENGRAM_DATA_DIR.
|
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME + ENGRAM_DATA_DIR.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-m3-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-m3-XXXXXX)"
|
||||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||||
@@ -17,7 +23,7 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m3_parity.c) =="
|
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"
|
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m3_parity.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -o "$BIN" 2>"$WORK/cc.log"
|
||||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
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
|
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
|
||||||
|
|
||||||
@@ -106,7 +112,7 @@ echo
|
|||||||
echo "== 5) ASan+UBSan build, exercise M3 scan/boot/hooks (leaks off — harness intentionally leaks el_strdup) =="
|
echo "== 5) ASan+UBSan build, exercise M3 scan/boot/hooks (leaks off — harness intentionally leaks el_strdup) =="
|
||||||
SANBIN="$WORK/m3.san"
|
SANBIN="$WORK/m3.san"
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
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"
|
-I "$INC" "$HERE/test_m3_parity.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -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
|
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
DATA2="$WORK/data2"; mkdir -p "$DATA2"
|
DATA2="$WORK/data2"; mkdir -p "$DATA2"
|
||||||
|
|||||||
@@ -6,8 +6,14 @@
|
|||||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This harness used to link
|
||||||
|
# el_runtime.c + engram_store.c only, which stopped linking once el_runtime.c
|
||||||
|
# began calling into the other engram siblings. Unquoted on purpose: a list.
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
INC="$HERE/../../lang/runtime"
|
INC="$HERE/../../lang/runtime"
|
||||||
WORK="$(mktemp -d /tmp/engram-m7-XXXXXX)"
|
WORK="$(mktemp -d /tmp/engram-m7-XXXXXX)"
|
||||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||||
@@ -22,7 +28,7 @@ unset ENGRAM_STORE
|
|||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m7_traversal.c) =="
|
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"
|
gcc -O2 -std=c11 -I "$INC" "$HERE/test_m7_traversal.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -o "$BIN" 2>"$WORK/cc.log"
|
||||||
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
|
||||||
echo " ok: compiled"
|
echo " ok: compiled"
|
||||||
|
|
||||||
@@ -115,7 +121,7 @@ echo
|
|||||||
echo "== 3) ASan+UBSan clean across parity + a small perf loop (leaks off — harness intentionally leaks el_strdup) =="
|
echo "== 3) ASan+UBSan clean across parity + a small perf loop (leaks off — harness intentionally leaks el_strdup) =="
|
||||||
SANBIN="$WORK/m7.san"
|
SANBIN="$WORK/m7.san"
|
||||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
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"
|
-I "$INC" "$HERE/test_m7_traversal.c" $RTSRC $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -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
|
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
|
||||||
export ASAN_OPTIONS=detect_leaks=0
|
export ASAN_OPTIONS=detect_leaks=0
|
||||||
D2="$WORK/data2"; mkdir -p "$D2"
|
D2="$WORK/data2"; mkdir -p "$D2"
|
||||||
|
|||||||
@@ -3,8 +3,17 @@
|
|||||||
set -e
|
set -e
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REL="$HERE/../../lang/runtime"
|
REL="$HERE/../../lang/runtime"
|
||||||
|
# test_wal.c and test_failloud.c #include "el_runtime.c" directly, so el_runtime.c
|
||||||
|
# is already IN the translation unit — link the SIBLINGS only, or every symbol in
|
||||||
|
# it is defined twice. The siblings are still required: el_runtime.c calls into
|
||||||
|
# all six engram TUs. (lang/runtime/SOURCES is the source of truth.)
|
||||||
|
RTSIB="$("$HERE/../../scripts/el-runtime-sources.sh" "$REL" | grep -v '/el_runtime\.c$')"
|
||||||
|
SSLFLAGS=""
|
||||||
|
if command -v brew >/dev/null 2>&1 && O="$(brew --prefix openssl@3 2>/dev/null)"; then
|
||||||
|
SSLFLAGS="-I$O/include -L$O/lib"
|
||||||
|
fi
|
||||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" \
|
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" \
|
||||||
"$HERE/test_wal.c" -lcurl -lpthread -o /tmp/test_wal
|
"$HERE/test_wal.c" $RTSIB $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -o /tmp/test_wal
|
||||||
HOME=/tmp/engram-throwaway-home /tmp/test_wal
|
HOME=/tmp/engram-throwaway-home /tmp/test_wal
|
||||||
# Fail-loud data-dir check (must exit 1 with a FATAL line):
|
# Fail-loud data-dir check (must exit 1 with a FATAL line):
|
||||||
cat > /tmp/test_failloud.c <<'C'
|
cat > /tmp/test_failloud.c <<'C'
|
||||||
@@ -12,5 +21,5 @@ cat > /tmp/test_failloud.c <<'C'
|
|||||||
int main(void){ unsetenv("ENGRAM_DATA_DIR"); unsetenv("HOME");
|
int main(void){ unsetenv("ENGRAM_DATA_DIR"); unsetenv("HOME");
|
||||||
engram_resolve_data_dir(); printf("REACHED\n"); return 0; }
|
engram_resolve_data_dir(); printf("REACHED\n"); return 0; }
|
||||||
C
|
C
|
||||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" /tmp/test_failloud.c -lcurl -lpthread -o /tmp/test_failloud
|
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" /tmp/test_failloud.c $RTSIB $SSLFLAGS -lcurl -lssl -lcrypto -lpthread -lm -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
|
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
|
||||||
|
|||||||
+34
-12
@@ -77,14 +77,30 @@ This is where almost all work belongs. El programs are source files that get com
|
|||||||
|
|
||||||
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.
|
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 runtime is native El (`runtime/*.el`) over a C OS-boundary. **Status (verified 2026-08-15):** the migration to a seed-only boundary is *in progress, not done*. Two files exist:
|
The runtime is native El (`runtime/*.el`) over a C OS-boundary. **Status (verified 2026-08-16):** the migration to a seed-only boundary is *in progress, not done*.
|
||||||
- `runtime/el_runtime.c` (~860 KB) — **LIVE**. Holds the engram store (`EngramStore engram_global`) plus the `http_*`/`json_*`/`state_*`/`engram_*` impls. It is the authoritative single-file link target for the compiler, and `tools/install.sh` compiles it into `libel.a`. This is where a new C builtin's *implementation* must currently live to be linkable.
|
|
||||||
- `runtime/el_seed.c` — the intended hand-maintained `__`-prefixed seed (thin wrappers over the above). It is compiled alongside `el_runtime.c` by `tools/install.sh`, but does **not** compile standalone yet (see the build-path caveat under "Rebuilding the Compiler").
|
**The runtime is MULTI-FILE. There is no single-file link target and there has not been one for months.** The canonical link set is listed once, in **`runtime/SOURCES`**, and printed by `scripts/el-runtime-sources.sh`. It currently holds ten translation units: `el_runtime.c`, `el_seed.c`, the six `engram_*.c` concern files, and `eg_cosine_batch{,_strategy_cpu}.c`.
|
||||||
|
|
||||||
|
- `runtime/el_runtime.c` (~940 KB, 20.5k lines) — **LIVE, and oversized.** It began life on 2026-05-03 as a temporary build shim: it was deleted that afternoon ("runtime is 100% native El") and restored 25 minutes later, explicitly "UNTIL the compiler is updated to emit `#include el_seed.h`". That `until` never arrived, and in the 3.5 months since, the file doubled. **It is not a volatility unit — it is a dumping ground.** ~47.5% of it is engram code that belongs in the six sibling files that already exist. Do not add to it. See "Where a new C builtin goes" below.
|
||||||
|
- `runtime/el_seed.c` — the intended hand-maintained `__`-prefixed seed (thin wrappers over the above).
|
||||||
|
- `runtime/engram_{store,vindex,geometry,reason,verify,cognition}.c` — the engram concerns, each with its own header. `el_runtime.c` `#include`s all six headers and makes hard cross-TU calls into all six.
|
||||||
|
|
||||||
|
> **Linking `el_runtime.c` alone does not work and has not for months.** It fails at `ld` with undefined symbols (`engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, …). Any recipe, script, or CI step that names `el_runtime.c` by itself is stale — replace it with `$(scripts/el-runtime-sources.sh lang/runtime)`.
|
||||||
|
|
||||||
**Only edit these when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features, a new engram store op). For everything else, write El.
|
**Only edit these when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features, a new engram store op). For everything else, write El.
|
||||||
|
|
||||||
|
#### Where a new C builtin goes
|
||||||
|
|
||||||
|
**Put it in the `.c` that owns the concern — NOT in `el_runtime.c`.**
|
||||||
|
|
||||||
|
*Placement is a link-time concern. The compiler cannot tell which `.c` a symbol came from, and never could.* `builtin_arity` in `el-compiler/src/codegen.el` maps NAME → ARITY INT and nothing else (~413 entries); the El name is emitted as the exact C symbol and resolved by `ld`. Proof, if you want it: `nm lang/dist/platform/elc` on the *shipped* compiler shows `T _engram_geo_reify_index_new` (defined in `engram_geometry.c`), `T _vindex_insert` (`engram_vindex.c`), `T _engram_think` (`engram_cognition.c`), `T _engram_reason_abduce` (`engram_reason.c`). **The shipped compiler is already linked from ten translation units.** A builtin defined in a sibling `.c` is exactly as linkable as one defined in `el_runtime.c`.
|
||||||
|
|
||||||
|
Choose the file by concern: engram store ops → `engram_store.c`; index → `engram_vindex.c`; geometry/priming → `engram_geometry.c`; reasoning → `engram_reason.c`; grounding/consistency → `engram_verify.c`; think/stance → `engram_cognition.c`. **If no existing file owns it, create one** — add the `.c` to `runtime/SOURCES` (one line) and every build path picks it up. For a builtin that belongs to a downstream program rather than the runtime, declare `c_source "path/to/file.c"` in that program's `manifest.el`; `elb` already links it (`parse_manifest_c_sources`, `lang/elb.el:82`).
|
||||||
|
|
||||||
|
> **`el_runtime.c` is on a ratchet and will reject your commit.** `runtime/BUDGET` caps it at its current line count *with no headroom*, and separately caps the number of `engram_*`/`eg_*`/`cog_*` function definitions in it. `scripts/check-runtime-growth.sh` enforces both in CI and in `.githooks/pre-commit`. **The numbers may only ever go down — do not raise them.** Every other runtime file is deliberately uncapped, because that is where the code is supposed to go. When you move code *out*, lower the numbers in the same commit; the guard tells you the new values.
|
||||||
|
|
||||||
When you add a C builtin (verbatim-emit recipe — the El name is emitted as the exact C symbol; `builtin_arity` is an arity guard only, not a dispatch table):
|
When you add a C builtin (verbatim-emit recipe — the El name is emitted as the exact C symbol; `builtin_arity` is an arity guard only, not a dispatch table):
|
||||||
1. Implement the C function in `el_runtime.c` (and declare it in `el_runtime.h`).
|
1. Implement the C function in the **concern-owning `.c`** (and declare it in that file's `.h`). Add the file to `runtime/SOURCES` if it is new. Only put it in `el_runtime.c` if it is genuinely EL core (val/str/map/list/arena) — that is ~8% of what is in there today.
|
||||||
2. Add a `__`-prefixed thin wrapper in `el_seed.c` and declare it in `el_seed.h`.
|
2. Add a `__`-prefixed thin wrapper in `el_seed.c` and declare it in `el_seed.h`.
|
||||||
3. Add the name to `builtin_arity` in `el-compiler/src/codegen.el` — add **both** the plain and `__`-prefixed spellings.
|
3. Add the name to `builtin_arity` in `el-compiler/src/codegen.el` — add **both** the plain and `__`-prefixed spellings.
|
||||||
4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
|
4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
|
||||||
@@ -111,21 +127,27 @@ After changing any `.el` source in `el-compiler/src/` (run from the `lang/` dir)
|
|||||||
```bash
|
```bash
|
||||||
# 1. Stage2: current elc compiles the (modified) compiler to C
|
# 1. Stage2: current elc compiles the (modified) compiler to C
|
||||||
./dist/platform/elc elc-cli.el > elc-new.c
|
./dist/platform/elc elc-cli.el > elc-new.c
|
||||||
# 2. Build the new compiler. The C link target is el_runtime.c — it holds the
|
# 2. Build the new compiler. Link the WHOLE runtime set, not el_runtime.c alone:
|
||||||
# engram store + http/json/state impls the compiler output calls. el_runtime.c
|
# el_runtime.c calls into engram_store / engram_vindex / eg_cosine_batch and
|
||||||
# self-hosts elc on its own; el_seed.c is the (aspirational) seed layer and does
|
# wraps el_seed.c, so a one-file link fails at `ld` with undefined symbols
|
||||||
# NOT compile standalone under clang (missing prototypes for the el_runtime.c
|
# (verified 2026-08-16 — the previous single-file line in this doc is stale).
|
||||||
# symbols it wraps — see caveat below), so link el_runtime.c here.
|
cc -std=c11 -O2 -I runtime -I$(brew --prefix openssl@3)/include \
|
||||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
-L$(brew --prefix openssl@3)/lib \
|
||||||
-o dist/platform/elc-new \
|
-o dist/platform/elc-new \
|
||||||
elc-new.c runtime/el_runtime.c
|
elc-new.c runtime/el_runtime.c runtime/el_seed.c \
|
||||||
|
runtime/engram_cognition.c runtime/engram_geometry.c runtime/engram_reason.c \
|
||||||
|
runtime/engram_store.c runtime/engram_verify.c runtime/engram_vindex.c \
|
||||||
|
runtime/eg_cosine_batch.c runtime/eg_cosine_batch_strategy_cpu.c \
|
||||||
|
-lcurl -lssl -lcrypto -lpthread -lm
|
||||||
# 3. Verify self-hosting FIXPOINT (stage3 == stage2 output, byte-identical):
|
# 3. Verify self-hosting FIXPOINT (stage3 == stage2 output, byte-identical):
|
||||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||||
diff elc-new.c elc-verify.c # must be identical
|
diff elc-new.c elc-verify.c # must be identical
|
||||||
mv dist/platform/elc-new dist/platform/elc
|
mv dist/platform/elc-new dist/platform/elc
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Build-path caveat (verified 2026-08-15).** `el_seed.c` is the intended hand-maintained OS-boundary seed, but it does **not** compile standalone under modern clang: it wraps ~16 unprefixed `el_runtime.c` symbols (`http_serve`, `json_*`, `state_*`, `http_response`) without prototypes, and clang treats implicit declarations as errors (C99+). The productionised install (`tools/install.sh`) builds `libel.a` from **both** `el_seed.o` + `el_runtime.o` together, which is why linking succeeds there. To make `el_seed.c` build on its own, add prototypes for those symbols (or `#include "el_runtime.h"`, reconciling the `__http_serve` return-type mismatch first). Until then, `el_runtime.c` is the authoritative single-file link target for the compiler.
|
> **Build-path caveat (verified 2026-08-15).** `el_seed.c` is the intended hand-maintained OS-boundary seed, but it does **not** compile standalone under modern clang: it wraps ~16 unprefixed `el_runtime.c` symbols (`http_serve`, `json_*`, `state_*`, `http_response`) without prototypes, and clang treats implicit declarations as errors (C99+). The productionised install (`tools/install.sh`) builds `libel.a` from **both** `el_seed.o` + `el_runtime.o` together, which is why linking succeeds there. To make `el_seed.c` build on its own, add prototypes for those symbols (or `#include "el_runtime.h"`, reconciling the `__http_serve` return-type mismatch first).
|
||||||
|
>
|
||||||
|
> **There is no single-file link target.** *(Corrected 2026-08-16 — this paragraph previously ended "`el_runtime.c` is the authoritative single-file link target for the compiler". Measured: that is false. Linking `elc-new.c` against `runtime/el_runtime.c` alone fails at `ld` with undefined `engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, and others, because `el_runtime.c` `#include`s six engram headers and calls into all six sibling `.c` files.)* Link the set in `runtime/SOURCES` via `$(../scripts/el-runtime-sources.sh runtime)`.
|
||||||
|
|
||||||
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+481
-603
File diff suppressed because it is too large
Load Diff
@@ -414,6 +414,12 @@ fn parse_import_line(trimmed: String, dir: String) -> String {
|
|||||||
// Accumulates chunks into lists and joins once at the end to avoid the O(n²)
|
// Accumulates chunks into lists and joins once at the end to avoid the O(n²)
|
||||||
// memory growth caused by repeated `prefix = prefix + chunk` concatenation.
|
// memory growth caused by repeated `prefix = prefix + chunk` concatenation.
|
||||||
fn resolve_imports(src_path: String) -> String {
|
fn resolve_imports(src_path: String) -> String {
|
||||||
|
// Only the OUTERMOST call publishes provenance. Nested calls number their
|
||||||
|
// lines from 1 within themselves, so their spans are meaningless once the
|
||||||
|
// text is spliced into the parent.
|
||||||
|
let depth: String = state_get("__elc_prov_depth")
|
||||||
|
if str_eq(depth, "") { state_set("__elc_prov_depth", "1") }
|
||||||
|
let is_top: Bool = str_eq(depth, "")
|
||||||
let seen_key: String = "__elc_imp__:" + src_path
|
let seen_key: String = "__elc_imp__:" + src_path
|
||||||
let already: String = state_get(seen_key)
|
let already: String = state_get(seen_key)
|
||||||
if !str_eq(already, "") { return "" }
|
if !str_eq(already, "") { return "" }
|
||||||
@@ -443,6 +449,7 @@ fn resolve_imports(src_path: String) -> String {
|
|||||||
// Collect chunks into lists — O(1) amortized per append.
|
// Collect chunks into lists — O(1) amortized per append.
|
||||||
// Join once at the end — O(n) single pass.
|
// Join once at the end — O(n) single pass.
|
||||||
let prefix_chunks: [String] = native_list_empty()
|
let prefix_chunks: [String] = native_list_empty()
|
||||||
|
let prefix_paths: [String] = native_list_empty()
|
||||||
let body_chunks: [String] = native_list_empty()
|
let body_chunks: [String] = native_list_empty()
|
||||||
let i: Int = 0
|
let i: Int = 0
|
||||||
while i < n {
|
while i < n {
|
||||||
@@ -454,21 +461,54 @@ fn resolve_imports(src_path: String) -> String {
|
|||||||
// Only check .elh for imported files — never for the entry file itself.
|
// Only check .elh for imported files — never for the entry file itself.
|
||||||
let imp_elh_path: String = str_slice(imp_path, 0, str_len(imp_path) - 3) + ".elh"
|
let imp_elh_path: String = str_slice(imp_path, 0, str_len(imp_path) - 3) + ".elh"
|
||||||
let imp_elh: String = fs_read(imp_elh_path)
|
let imp_elh: String = fs_read(imp_elh_path)
|
||||||
|
// Provenance: record which line range of the combined source came
|
||||||
|
// from which file, so a diagnostic can name the FILE and not just a
|
||||||
|
// line in a string that no longer exists on disk.
|
||||||
if !str_eq(imp_elh, "") {
|
if !str_eq(imp_elh, "") {
|
||||||
// Header exists: mark the .el as seen (so it won't be re-inlined
|
// Header exists: mark the .el as seen (so it won't be re-inlined
|
||||||
// if something else also imports it) and use the header text.
|
// if something else also imports it) and use the header text.
|
||||||
let seen_imp_key: String = "__elc_imp__:" + imp_path
|
let seen_imp_key: String = "__elc_imp__:" + imp_path
|
||||||
state_set(seen_imp_key, "1")
|
state_set(seen_imp_key, "1")
|
||||||
let prefix_chunks = native_list_append(prefix_chunks, imp_elh)
|
let prefix_chunks = native_list_append(prefix_chunks, imp_elh)
|
||||||
|
let prefix_paths = native_list_append(prefix_paths, imp_path)
|
||||||
} else {
|
} else {
|
||||||
let imp_body: String = resolve_imports(imp_path)
|
let imp_body: String = resolve_imports(imp_path)
|
||||||
let prefix_chunks = native_list_append(prefix_chunks, imp_body)
|
let prefix_chunks = native_list_append(prefix_chunks, imp_body)
|
||||||
|
let prefix_paths = native_list_append(prefix_paths, imp_path)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let body_chunks = native_list_append(body_chunks, line + "\n")
|
let body_chunks = native_list_append(body_chunks, line + "\n")
|
||||||
}
|
}
|
||||||
let i = i + 1
|
let i = i + 1
|
||||||
}
|
}
|
||||||
|
// Walk the assembled chunks once and publish <file> spans <start> <end>.
|
||||||
|
// LIMIT: nested imports return a single string, so their internal
|
||||||
|
// boundaries are already lost by the time we see them -- a definition
|
||||||
|
// inside a transitively imported file is attributed to the direct import.
|
||||||
|
// Local, not accumulated in state: a nested call numbers its lines from 1
|
||||||
|
// within itself, so letting it append to a shared buffer republishes
|
||||||
|
// meaningless spans under the parent's name.
|
||||||
|
let prov: String = ""
|
||||||
|
let line_at: Int = 1
|
||||||
|
let ci: Int = 0
|
||||||
|
let nchunks: Int = native_list_len(prefix_chunks)
|
||||||
|
while ci < nchunks {
|
||||||
|
let chunk: String = native_list_get(prefix_chunks, ci)
|
||||||
|
let nlines: Int = str_count_lines(chunk)
|
||||||
|
let src: String = native_list_get(prefix_paths, ci)
|
||||||
|
let prov = prov + src + " spans " + native_int_to_str(line_at) + " " + native_int_to_str(line_at + nlines - 1) + "\n"
|
||||||
|
let line_at = line_at + nlines
|
||||||
|
let ci = ci + 1
|
||||||
|
}
|
||||||
|
let prov = prov + src_path + " spans " + native_int_to_str(line_at) + " 999999\n"
|
||||||
|
if is_top {
|
||||||
|
let prov_out: String = env("EL_RELATIONS_OUT")
|
||||||
|
if !str_eq(prov_out, "") {
|
||||||
|
let existing: String = ""
|
||||||
|
if fs_exists(prov_out) { let existing = fs_read(prov_out) }
|
||||||
|
fs_write(prov_out, existing + prov)
|
||||||
|
}
|
||||||
|
}
|
||||||
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,13 +138,38 @@ fn lex_is_whitespace(ch: String) -> Bool {
|
|||||||
|
|
||||||
// tok_append — append a (kind, value) pair to a flat token list.
|
// tok_append — append a (kind, value) pair to a flat token list.
|
||||||
// Returns the updated list. Gamma combines flat-list + char-code for max savings.
|
// Returns the updated list. Gamma combines flat-list + char-code for max savings.
|
||||||
|
// A token is (kind, value, line). The line comes from state rather than a
|
||||||
|
// parameter so the ~200 existing tok_append call sites are untouched -- the
|
||||||
|
// lexer advances __lex_line as it walks, and every token minted takes the line
|
||||||
|
// it was minted on.
|
||||||
|
//
|
||||||
|
// WHY AT ALL: before this a token carried no position, so no diagnostic in El
|
||||||
|
// could name a place. Every error named a symbol and never a line, and after
|
||||||
|
// textual inlining there was no way to say which FILE a definition came from.
|
||||||
fn tok_append(tokens: [Any], kind: String, value: String) -> [Any] {
|
fn tok_append(tokens: [Any], kind: String, value: String) -> [Any] {
|
||||||
let tokens = native_list_append(tokens, kind)
|
let tokens = native_list_append(tokens, kind)
|
||||||
native_list_append(tokens, value)
|
let tokens = native_list_append(tokens, value)
|
||||||
|
native_list_append(tokens, state_get("__lex_line"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Keyword lookup ------------------------------------------------------------
|
// -- Keyword lookup ------------------------------------------------------------
|
||||||
|
|
||||||
|
// keyword_kind — the language's reserved spellings.
|
||||||
|
//
|
||||||
|
// A grammar is a BASIS: `fn` means function-start because someone said so, and
|
||||||
|
// nothing derives it. But unlike the other tables moved out this session, this
|
||||||
|
// one stays code, and the SHOULD gate is why. The keyword set is closed by the
|
||||||
|
// language definition -- it does not leak the way an allowlist does -- and the
|
||||||
|
// lexer runs before the program is understood, so a program can never declare
|
||||||
|
// its own keywords. Externalising it would cost file I/O on every compile and
|
||||||
|
// buy nothing.
|
||||||
|
//
|
||||||
|
// Removed 2026-08-17: sealed, activate, seed, protocol, impl. Reserved in the
|
||||||
|
// lexer, consumed by no parser or codegen path, and each one stole an
|
||||||
|
// identifier from users for nothing. `test` LOOKED inert by the same measure
|
||||||
|
// and is not -- codegen consumes it at 4135 for --test mode, 408 uses in the
|
||||||
|
// tree. The first measurement checked only parser.el and would have broken all
|
||||||
|
// of them.
|
||||||
fn keyword_kind(word: String) -> String {
|
fn keyword_kind(word: String) -> String {
|
||||||
if word == "let" { return "Let" }
|
if word == "let" { return "Let" }
|
||||||
if word == "fn" { return "Fn" }
|
if word == "fn" { return "Fn" }
|
||||||
@@ -161,14 +186,9 @@ fn keyword_kind(word: String) -> String {
|
|||||||
if word == "from" { return "From" }
|
if word == "from" { return "From" }
|
||||||
if word == "as" { return "As" }
|
if word == "as" { return "As" }
|
||||||
if word == "with" { return "With" }
|
if word == "with" { return "With" }
|
||||||
if word == "sealed" { return "Sealed" }
|
|
||||||
if word == "activate" { return "Activate" }
|
|
||||||
if word == "where" { return "Where" }
|
if word == "where" { return "Where" }
|
||||||
if word == "test" { return "Test" }
|
if word == "test" { return "Test" }
|
||||||
if word == "seed" { return "Seed" }
|
|
||||||
if word == "assert" { return "Assert" }
|
if word == "assert" { return "Assert" }
|
||||||
if word == "protocol" { return "Protocol" }
|
|
||||||
if word == "impl" { return "Impl" }
|
|
||||||
if word == "retry" { return "Retry" }
|
if word == "retry" { return "Retry" }
|
||||||
if word == "times" { return "Times" }
|
if word == "times" { return "Times" }
|
||||||
if word == "fallback" { return "Fallback" }
|
if word == "fallback" { return "Fallback" }
|
||||||
@@ -521,6 +541,12 @@ fn scan_interp_brace(src: String, start: Int, total: Int) -> Map<String, Any> {
|
|||||||
|
|
||||||
// interp_tokens_append_all - copy every (kind, value) pair from flat src list
|
// interp_tokens_append_all - copy every (kind, value) pair from flat src list
|
||||||
// into flat dst list, skipping the trailing Eof pair that lex() always appends.
|
// into flat dst list, skipping the trailing Eof pair that lex() always appends.
|
||||||
|
// Splices re-lexed interpolation tokens into the stream. This walks the token
|
||||||
|
// list DIRECTLY rather than through tok_append, so it carries its own copy of
|
||||||
|
// the stride -- which is why giving tokens a line broke the compiler's second
|
||||||
|
// generation and not its first: the compiler's own source uses string
|
||||||
|
// interpolation, so gen1 (built by the old compiler) was fine and gen2 emitted
|
||||||
|
// a corrupted stream.
|
||||||
fn interp_tokens_append_all(dst: [Any], src: [Any]) -> [Any] {
|
fn interp_tokens_append_all(dst: [Any], src: [Any]) -> [Any] {
|
||||||
let src_len: Int = native_list_len(src)
|
let src_len: Int = native_list_len(src)
|
||||||
let j = 0
|
let j = 0
|
||||||
@@ -531,9 +557,11 @@ fn interp_tokens_append_all(dst: [Any], src: [Any]) -> [Any] {
|
|||||||
let j = src_len
|
let j = src_len
|
||||||
} else {
|
} else {
|
||||||
let val: String = native_list_get(src, j + 1)
|
let val: String = native_list_get(src, j + 1)
|
||||||
|
let ln: String = native_list_get(src, j + 2)
|
||||||
let result = native_list_append(result, kind)
|
let result = native_list_append(result, kind)
|
||||||
let result = native_list_append(result, val)
|
let result = native_list_append(result, val)
|
||||||
let j = j + 2
|
let result = native_list_append(result, ln)
|
||||||
|
let j = j + 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -764,8 +792,14 @@ fn lex(source: String) -> [Any] {
|
|||||||
let total: Int = str_len(source)
|
let total: Int = str_len(source)
|
||||||
let tokens: [Any] = native_list_empty()
|
let tokens: [Any] = native_list_empty()
|
||||||
let i: Int = 0
|
let i: Int = 0
|
||||||
|
state_set("__lex_line", "1")
|
||||||
|
let line_no: Int = 1
|
||||||
|
|
||||||
while i < total {
|
while i < total {
|
||||||
|
if str_eq(str_slice(source, i, i + 1), "\n") {
|
||||||
|
let line_no = line_no + 1
|
||||||
|
state_set("__lex_line", native_int_to_str(line_no))
|
||||||
|
}
|
||||||
let c: Int = str_char_code(source, i)
|
let c: Int = str_char_code(source, i)
|
||||||
|
|
||||||
// Skip whitespace (space=32, tab=9, newline=10, CR=13)
|
// Skip whitespace (space=32, tab=9, newline=10, CR=13)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
// programs. All callers use these helpers -- only these three need updating.
|
// programs. All callers use these helpers -- only these three need updating.
|
||||||
|
|
||||||
fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||||
let kind: String = native_list_get(tokens, pos * 2)
|
let kind: String = native_list_get(tokens, pos * 3)
|
||||||
let value: String = native_list_get(tokens, pos * 2 + 1)
|
let value: String = native_list_get(tokens, pos * 3 + 1)
|
||||||
{ "kind": kind, "value": value }
|
{ "kind": kind, "value": value }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,25 +28,32 @@ fn tok_kind(tokens: [Any], pos: Int) -> String {
|
|||||||
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
|
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
|
||||||
// which matches no delimiter, letting inner parse loops append AST nodes
|
// which matches no delimiter, letting inner parse loops append AST nodes
|
||||||
// forever on malformed input -> unbounded allocation -> OOM.
|
// forever on malformed input -> unbounded allocation -> OOM.
|
||||||
let n: Int = native_list_len(tokens) / 2
|
let n: Int = native_list_len(tokens) / 3
|
||||||
if pos < 0 {
|
if pos < 0 {
|
||||||
return "Eof"
|
return "Eof"
|
||||||
}
|
}
|
||||||
if pos >= n {
|
if pos >= n {
|
||||||
return "Eof"
|
return "Eof"
|
||||||
}
|
}
|
||||||
native_list_get(tokens, pos * 2)
|
native_list_get(tokens, pos * 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tok_line(tokens: [Any], pos: Int) -> String {
|
||||||
|
let n: Int = native_list_len(tokens) / 3
|
||||||
|
if pos < 0 { return "0" }
|
||||||
|
if pos >= n { return "0" }
|
||||||
|
native_list_get(tokens, pos * 3 + 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tok_value(tokens: [Any], pos: Int) -> String {
|
fn tok_value(tokens: [Any], pos: Int) -> String {
|
||||||
let n: Int = native_list_len(tokens) / 2
|
let n: Int = native_list_len(tokens) / 3
|
||||||
if pos < 0 {
|
if pos < 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if pos >= n {
|
if pos >= n {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
native_list_get(tokens, pos * 2 + 1)
|
native_list_get(tokens, pos * 3 + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse_progress_fatal — robustness backstop. Called by the token-consuming
|
// parse_progress_fatal — robustness backstop. Called by the token-consuming
|
||||||
@@ -1230,7 +1237,7 @@ fn parse_block(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
// Runaway backstop: a block can hold at most (token count) statements, since
|
// 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
|
// 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.
|
// off the end without terminating (malformed input) -> fail fast, don't hang.
|
||||||
let blk_total: Int = native_list_len(tokens) / 2
|
let blk_total: Int = native_list_len(tokens) / 3
|
||||||
let blk_iters: Int = 0
|
let blk_iters: Int = 0
|
||||||
while running {
|
while running {
|
||||||
let blk_iters = blk_iters + 1
|
let blk_iters = blk_iters + 1
|
||||||
@@ -1550,7 +1557,10 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
let p = r2["pos"]
|
let p = r2["pos"]
|
||||||
// r2 result map fully consumed — release to free peak heap.
|
// r2 result map fully consumed — release to free peak heap.
|
||||||
el_release(r2)
|
el_release(r2)
|
||||||
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
|
// The definition carries the line it was written on. Without it no
|
||||||
|
// diagnostic can name a place, and after textual inlining there is no
|
||||||
|
// way to say which FILE a definition came from.
|
||||||
|
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type, "line": tok_line(tokens, pos) }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// type definition: `type Name = { field: Type, ... }`
|
// type definition: `type Name = { field: Type, ... }`
|
||||||
@@ -1842,6 +1852,7 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
"params": inner["params"],
|
"params": inner["params"],
|
||||||
"body": inner["body"],
|
"body": inner["body"],
|
||||||
"ret_type": inner["ret_type"],
|
"ret_type": inner["ret_type"],
|
||||||
|
"line": inner["line"],
|
||||||
"decorator": dec_name,
|
"decorator": dec_name,
|
||||||
"decorators": dlist
|
"decorators": dlist
|
||||||
}
|
}
|
||||||
@@ -1976,6 +1987,18 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
// singleton: "id" — process identity. The runtime takes an exclusive
|
// singleton: "id" — process identity. The runtime takes an exclusive
|
||||||
// lock at startup; a SECOND start is refused, loudly,
|
// lock at startup; a SECOND start is refused, loudly,
|
||||||
// instead of two processes sharing one data dir.
|
// instead of two processes sharing one data dir.
|
||||||
|
// guards: <expr> — WHAT that singleton protects: an expression yielding
|
||||||
|
// the path of the guarded state directory, evaluated at
|
||||||
|
// startup. MANDATORY with `singleton:`, because a lock
|
||||||
|
// keyed on a program's NAME rather than on its STATE is
|
||||||
|
// not a guard — measured 2026-08-16, the name-keyed
|
||||||
|
// version refused unrelated instances (different data
|
||||||
|
// dirs) AND permitted concurrent ones (same data dir,
|
||||||
|
// different $TMPDIR). It is an expression and not a
|
||||||
|
// string so a program can point at the resolver that
|
||||||
|
// already OWNS the path (§18.4) instead of restating
|
||||||
|
// its default here, which would give the path two
|
||||||
|
// owners that can disagree.
|
||||||
// env NAME: T = "d" — one configuration entry. Its type and its default
|
// env NAME: T = "d" — one configuration entry. Its type and its default
|
||||||
// are declared ONCE, here, and resolved+validated
|
// are declared ONCE, here, and resolved+validated
|
||||||
// before main() body runs.
|
// before main() body runs.
|
||||||
@@ -1993,6 +2016,8 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
let p = expect(tokens, p, "LBrace")
|
let p = expect(tokens, p, "LBrace")
|
||||||
let singleton = ""
|
let singleton = ""
|
||||||
let has_singleton = false
|
let has_singleton = false
|
||||||
|
let guards_node = { "expr": "Str", "value": "" }
|
||||||
|
let has_guards = false
|
||||||
let entries = native_list_empty()
|
let entries = native_list_empty()
|
||||||
// Entry-scratch declared at loop-body level (not inside the branch) so
|
// Entry-scratch declared at loop-body level (not inside the branch) so
|
||||||
// that inner `let` forms compile to assignment rather than a C-scoped
|
// that inner `let` forms compile to assignment rather than a C-scoped
|
||||||
@@ -2048,13 +2073,26 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
"required": erequired
|
"required": erequired
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// scalar field: `name: "value"`
|
if str_eq(fname, "guards") {
|
||||||
let p = expect(tokens, p, "Colon")
|
// guards: <expr> — the STATE the singleton protects.
|
||||||
let fval = tok_value(tokens, p)
|
// Parsed as a full expression, not a string literal, so
|
||||||
let p = p + 1
|
// it can name the resolver that owns the path
|
||||||
if str_eq(fname, "singleton") {
|
// (`guards: engram_resolve_data_dir()`) rather than
|
||||||
let singleton = fval
|
// duplicating that resolver's default here.
|
||||||
let has_singleton = true
|
let p = expect(tokens, p, "Colon")
|
||||||
|
let g_r = parse_expr(tokens, p)
|
||||||
|
let guards_node = g_r["node"]
|
||||||
|
let p = g_r["pos"]
|
||||||
|
let has_guards = true
|
||||||
|
} else {
|
||||||
|
// scalar field: `name: "value"`
|
||||||
|
let p = expect(tokens, p, "Colon")
|
||||||
|
let fval = tok_value(tokens, p)
|
||||||
|
let p = p + 1
|
||||||
|
if str_eq(fname, "singleton") {
|
||||||
|
let singleton = fval
|
||||||
|
let has_singleton = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let k5 = tok_kind(tokens, p)
|
let k5 = tok_kind(tokens, p)
|
||||||
@@ -2070,6 +2108,8 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
"name": name,
|
"name": name,
|
||||||
"singleton": singleton,
|
"singleton": singleton,
|
||||||
"has_singleton": has_singleton,
|
"has_singleton": has_singleton,
|
||||||
|
"guards": guards_node,
|
||||||
|
"has_guards": has_guards,
|
||||||
"entries": entries
|
"entries": entries
|
||||||
}, p)
|
}, p)
|
||||||
}
|
}
|
||||||
@@ -2129,7 +2169,7 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
|
|
||||||
fn parse(tokens: [Any]) -> [Map<String, Any>] {
|
fn parse(tokens: [Any]) -> [Map<String, Any>] {
|
||||||
// Flat list: 2 entries per token, so divide by 2 for token count.
|
// Flat list: 2 entries per token, so divide by 2 for token count.
|
||||||
let total: Int = native_list_len(tokens) / 2
|
let total: Int = native_list_len(tokens) / 3
|
||||||
let stmts: [Map<String, Any>] = native_list_empty()
|
let stmts: [Map<String, Any>] = native_list_empty()
|
||||||
let pos: Int = 0
|
let pos: Int = 0
|
||||||
let running = true
|
let running = true
|
||||||
@@ -2172,7 +2212,7 @@ fn parse_one(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
// On entry, pos must point at the LBrace token.
|
// On entry, pos must point at the LBrace token.
|
||||||
// Returns the position of the token AFTER the matching RBrace.
|
// Returns the position of the token AFTER the matching RBrace.
|
||||||
fn skip_to_rbrace(tokens: [Any], pos: Int) -> Int {
|
fn skip_to_rbrace(tokens: [Any], pos: Int) -> Int {
|
||||||
let total: Int = native_list_len(tokens) / 2
|
let total: Int = native_list_len(tokens) / 3
|
||||||
let p: Int = pos + 1
|
let p: Int = pos + 1
|
||||||
let depth: Int = 1
|
let depth: Int = 1
|
||||||
let going: Bool = true
|
let going: Bool = true
|
||||||
@@ -2224,7 +2264,7 @@ fn is_stmt_start_kind(k: String) -> Bool {
|
|||||||
// token that could start a new top-level statement, staying depth-aware
|
// token that could start a new top-level statement, staying depth-aware
|
||||||
// so that braces inside expressions don't fool us.
|
// so that braces inside expressions don't fool us.
|
||||||
fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
|
fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
|
||||||
let total: Int = native_list_len(tokens) / 2
|
let total: Int = native_list_len(tokens) / 3
|
||||||
let p: Int = pos
|
let p: Int = pos
|
||||||
let depth: Int = 0
|
let depth: Int = 0
|
||||||
let going: Bool = true
|
let going: Bool = true
|
||||||
@@ -2390,7 +2430,7 @@ fn scan_params_el(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
//
|
//
|
||||||
// Peak memory: O(tokens) with no expression AST allocation.
|
// Peak memory: O(tokens) with no expression AST allocation.
|
||||||
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
|
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
|
||||||
let total: Int = native_list_len(tokens) / 2
|
let total: Int = native_list_len(tokens) / 3
|
||||||
let sigs: [Map<String, Any>] = native_list_empty()
|
let sigs: [Map<String, Any>] = native_list_empty()
|
||||||
let pos: Int = 0
|
let pos: Int = 0
|
||||||
let going: Bool = true
|
let going: Bool = true
|
||||||
@@ -2532,7 +2572,7 @@ fn scan_params_c(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
//
|
//
|
||||||
// The scan allocates only small string values per entry, keeping peak RSS low.
|
// The scan allocates only small string values per entry, keeping peak RSS low.
|
||||||
fn scan_fn_sigs(tokens: [Any]) -> [Map<String, Any>] {
|
fn scan_fn_sigs(tokens: [Any]) -> [Map<String, Any>] {
|
||||||
let total: Int = native_list_len(tokens) / 2
|
let total: Int = native_list_len(tokens) / 3
|
||||||
let sigs: [Map<String, Any>] = native_list_empty()
|
let sigs: [Map<String, Any>] = native_list_empty()
|
||||||
let pos: Int = 0
|
let pos: Int = 0
|
||||||
let going: Bool = true
|
let going: Bool = true
|
||||||
|
|||||||
+41
-9
@@ -49,21 +49,49 @@ download() {
|
|||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
# The runtime is MULTI-FILE. el_runtime.c #includes six engram headers and makes
|
||||||
download "${RELEASE_BASE}/el_runtime.c" "${TMP_DIR}/el_runtime.c"
|
# hard cross-TU calls into all six sibling .c files, so installing el_runtime.c
|
||||||
download "${RELEASE_BASE}/el_runtime.h" "${TMP_DIR}/el_runtime.h"
|
# alone produces a lib/ that CANNOT LINK — `ld` fails with undefined
|
||||||
|
# engram_ground_json / engram_activate_inner / eg_find_relation / cog_assert_two_axis.
|
||||||
|
# This list mirrors lang/runtime/SOURCES (the in-repo source of truth); keep them
|
||||||
|
# in step. install.sh is standalone by design — it runs on machines with no repo
|
||||||
|
# checkout — so it cannot call scripts/el-runtime-sources.sh.
|
||||||
|
RUNTIME_SOURCES=(
|
||||||
|
el_runtime.c el_seed.c
|
||||||
|
engram_store.c engram_vindex.c engram_geometry.c
|
||||||
|
engram_reason.c engram_verify.c engram_cognition.c
|
||||||
|
engram_text.c
|
||||||
|
eg_cosine_batch.c eg_cosine_batch_strategy_cpu.c
|
||||||
|
)
|
||||||
|
RUNTIME_HEADERS=(
|
||||||
|
el_runtime.h el_seed.h
|
||||||
|
engram_store.h engram_vindex.h engram_geometry.h
|
||||||
|
engram_reason.h engram_verify.h engram_cognition.h
|
||||||
|
engram_text.h
|
||||||
|
eg_cosine_batch.h eg_cosine_batch_strategy.h
|
||||||
|
)
|
||||||
|
|
||||||
|
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
||||||
|
for f in "${RUNTIME_SOURCES[@]}" "${RUNTIME_HEADERS[@]}"; do
|
||||||
|
download "${RELEASE_BASE}/${f}" "${TMP_DIR}/${f}"
|
||||||
|
done
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||||
install -m 644 "${TMP_DIR}/el_runtime.c" "${LIB_DIR}/el_runtime.c"
|
for f in "${RUNTIME_SOURCES[@]}" "${RUNTIME_HEADERS[@]}"; do
|
||||||
install -m 644 "${TMP_DIR}/el_runtime.h" "${LIB_DIR}/el_runtime.h"
|
install -m 644 "${TMP_DIR}/${f}" "${LIB_DIR}/${f}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Record the link set so downstream Makefiles can read it instead of hardcoding.
|
||||||
|
printf '%s\n' "${RUNTIME_SOURCES[@]}" > "${TMP_DIR}/SOURCES"
|
||||||
|
install -m 644 "${TMP_DIR}/SOURCES" "${LIB_DIR}/SOURCES"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "==> El SDK installed successfully"
|
echo "==> El SDK installed successfully"
|
||||||
echo
|
echo
|
||||||
echo " elc binary : ${BIN_DIR}/elc"
|
echo " elc binary : ${BIN_DIR}/elc"
|
||||||
echo " runtime : ${LIB_DIR}/el_runtime.c"
|
echo " runtime : ${LIB_DIR}/ (${#RUNTIME_SOURCES[@]} .c files, ${#RUNTIME_HEADERS[@]} headers)"
|
||||||
echo " header : ${LIB_DIR}/el_runtime.h"
|
echo " link set : ${LIB_DIR}/SOURCES"
|
||||||
echo
|
echo
|
||||||
echo "Add the following to your Makefile to build El programs:"
|
echo "Add the following to your Makefile to build El programs:"
|
||||||
echo
|
echo
|
||||||
@@ -71,10 +99,14 @@ echo " EL_LIB := ${LIB_DIR}"
|
|||||||
echo " ELC := elc"
|
echo " ELC := elc"
|
||||||
echo " CC := cc"
|
echo " CC := cc"
|
||||||
echo " CFLAGS := -std=c11 -O2 -I\$(EL_LIB)"
|
echo " CFLAGS := -std=c11 -O2 -I\$(EL_LIB)"
|
||||||
|
echo " LDLIBS := -lcurl -lssl -lcrypto -lpthread -lm"
|
||||||
|
echo
|
||||||
|
echo " # The runtime is multi-file — link the whole set, not el_runtime.c alone."
|
||||||
|
echo " EL_RUNTIME := \$(addprefix \$(EL_LIB)/,\$(shell cat \$(EL_LIB)/SOURCES))"
|
||||||
echo
|
echo
|
||||||
echo " dist/myapp.c: src/myapp.el"
|
echo " dist/myapp.c: src/myapp.el"
|
||||||
echo " \t\$(ELC) src/myapp.el > dist/myapp.c"
|
echo " \t\$(ELC) src/myapp.el > dist/myapp.c"
|
||||||
echo
|
echo
|
||||||
echo " dist/myapp: dist/myapp.c"
|
echo " dist/myapp: dist/myapp.c"
|
||||||
echo " \t\$(CC) \$(CFLAGS) -o dist/myapp dist/myapp.c \$(EL_LIB)/el_runtime.c -lcurl -lpthread"
|
echo " \t\$(CC) \$(CFLAGS) -o dist/myapp dist/myapp.c \$(EL_RUNTIME) \$(LDLIBS)"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# BUDGET — a RATCHET on lang/runtime/el_runtime.c. Enforced by
|
||||||
|
# scripts/check-runtime-growth.sh. These numbers may only ever go DOWN.
|
||||||
|
#
|
||||||
|
# WHY THIS FILE EXISTS
|
||||||
|
# --------------------
|
||||||
|
# scripts/check-single-runtime.sh guards against el_runtime.c being COPIED.
|
||||||
|
# Nothing guarded against it GROWING. It grew from 10,607 lines to 20,527 —
|
||||||
|
# 94% — in 3.5 months, while under an explicit commit-message promise that it
|
||||||
|
# was a temporary shim about to be deleted.
|
||||||
|
#
|
||||||
|
# It grew because lang/AGENTS.md told every agent to grow it: it claimed
|
||||||
|
# el_runtime.c was "the authoritative single-file link target" and that a new
|
||||||
|
# C builtin "must live there to be linkable". That is false — placement is a
|
||||||
|
# link-time concern, `builtin_arity` is an arity guard not a dispatch table,
|
||||||
|
# and the shipped elc already links from ten translation units. The claim is
|
||||||
|
# corrected, and this file is the mechanism that keeps it corrected.
|
||||||
|
#
|
||||||
|
# THIS IS A RATCHET, NOT A LIMIT
|
||||||
|
# ------------------------------
|
||||||
|
# The budget is set at the CURRENT size. There is no headroom, deliberately.
|
||||||
|
# The file cannot grow by even one line. Any new code goes in the .c that owns
|
||||||
|
# the concern — that is the whole point, and every other runtime file is
|
||||||
|
# deliberately UNCAPPED.
|
||||||
|
#
|
||||||
|
# When you move code OUT, lower the number in the same commit. The guard tells
|
||||||
|
# you to when you have earned it.
|
||||||
|
#
|
||||||
|
# FORMAT: <key> <value> — `#` comments and blank lines ignored.
|
||||||
|
|
||||||
|
# Maximum lines in lang/runtime/el_runtime.c.
|
||||||
|
# 2026-08-16: 20,527 — the high-water mark.
|
||||||
|
# 2026-08-16: 20,427 — engram_text.c extracted (tokenize, token hygiene,
|
||||||
|
# word-boundary match, damage signature). Ratcheted down.
|
||||||
|
max_lines 20427
|
||||||
|
|
||||||
|
# Maximum top-level engram/eg_/cog_ function definitions in el_runtime.c.
|
||||||
|
# ~47.5% of the file is engram code, and engram already owns six dedicated
|
||||||
|
# sibling files (engram_{store,vindex,geometry,reason,verify,cognition}.c).
|
||||||
|
# Every one of these belongs in one of them. This is the Stage 3 scoreboard.
|
||||||
|
# 2026-08-16: 279 -> 275 (4 moved to engram_text.c).
|
||||||
|
max_engram_fns 275
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# SOURCES — the canonical El runtime link set.
|
||||||
|
#
|
||||||
|
# THIS FILE IS THE SINGLE SOURCE OF TRUTH for "what do I compile and link to
|
||||||
|
# get the El runtime". Every build path — CI, install.sh, the SDK release, the
|
||||||
|
# docs, elb, the engram test harnesses — reads it via scripts/el-runtime-sources.sh
|
||||||
|
# instead of hardcoding its own list.
|
||||||
|
#
|
||||||
|
# WHY THIS FILE EXISTS
|
||||||
|
# --------------------
|
||||||
|
# The runtime has been multi-translation-unit since the engram siblings landed:
|
||||||
|
# el_runtime.c #includes engram_{store,vindex,geometry,reason,verify,cognition}.h
|
||||||
|
# and makes hard cross-TU calls into all six. Linking el_runtime.c ALONE has been
|
||||||
|
# broken since then — `ld` fails with undefined symbols (engram_ground_json,
|
||||||
|
# engram_activate_inner, eg_find_relation, cog_assert_two_axis, ...).
|
||||||
|
#
|
||||||
|
# It stayed broken because the link set was written out longhand in ~8 different
|
||||||
|
# places, each of which drifted independently. A list copied 8 times is a list
|
||||||
|
# that is wrong in 8 places. It is now written once, here.
|
||||||
|
#
|
||||||
|
# HOW TO USE IT
|
||||||
|
# -------------
|
||||||
|
# scripts/el-runtime-sources.sh # bare names, one per line
|
||||||
|
# scripts/el-runtime-sources.sh lang/runtime # prefixed with a directory
|
||||||
|
# cc ... $(scripts/el-runtime-sources.sh lang/runtime) -lcurl -lssl -lcrypto -lpthread -lm
|
||||||
|
#
|
||||||
|
# ADDING A FILE
|
||||||
|
# -------------
|
||||||
|
# Add the .c here and it is picked up by every build path at once. That is the
|
||||||
|
# point: a new concern gets its own translation unit and costs one line, instead
|
||||||
|
# of being appended to el_runtime.c because appending was the cheaper edit.
|
||||||
|
#
|
||||||
|
# Order is link order. Blank lines and `#` comments are ignored.
|
||||||
|
|
||||||
|
# --- EL core language runtime -------------------------------------------------
|
||||||
|
el_runtime.c
|
||||||
|
el_seed.c
|
||||||
|
|
||||||
|
# --- Engram: store, index, geometry, reasoning, verification, cognition -------
|
||||||
|
# These are the six concern-owned translation units el_runtime.c calls into.
|
||||||
|
engram_store.c
|
||||||
|
engram_vindex.c
|
||||||
|
engram_geometry.c
|
||||||
|
engram_reason.c
|
||||||
|
engram_verify.c
|
||||||
|
engram_cognition.c
|
||||||
|
|
||||||
|
# --- Text: tokenization, token hygiene, damage signature ---------------------
|
||||||
|
# Extracted from el_runtime.c 2026-08-16. Plain C over <ctype.h>/<string.h> —
|
||||||
|
# touches no EL value type and no engram store type. New text helpers go HERE.
|
||||||
|
engram_text.c
|
||||||
|
|
||||||
|
# --- Vector math: batch cosine + its CPU strategy ----------------------------
|
||||||
|
# The ggml strategy (eg_cosine_batch_strategy_ggml.c) is an OPTIONAL swap-in and
|
||||||
|
# is deliberately NOT in the default set — it needs ggml headers. Link it in
|
||||||
|
# place of the cpu strategy when you have them.
|
||||||
|
eg_cosine_batch.c
|
||||||
|
eg_cosine_batch_strategy_cpu.c
|
||||||
+316
-135
@@ -712,6 +712,20 @@ el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) {
|
|||||||
* happen to look like aligned heap pointers are exceedingly unlikely to land
|
* happen to look like aligned heap pointers are exceedingly unlikely to land
|
||||||
* on a page whose first 4 bytes match either magic. */
|
* on a page whose first 4 bytes match either magic. */
|
||||||
|
|
||||||
|
/* el_tagged — THE gate for "is this slot a heap object carrying this tag".
|
||||||
|
*
|
||||||
|
* el_val_t carries both integers and tagged heap pointers, so deciding which
|
||||||
|
* requires checking the value BEFORE dereferencing it. That check was a
|
||||||
|
* convention every author had to know rather than a gate they had to pass
|
||||||
|
* through, and the result is measurable: geom_of and mfld_of call
|
||||||
|
* looks_like_heap_obj and are correct; el_bin_lookup checked only a 4096 floor
|
||||||
|
* -- no alignment, no small-int, no negative -- and reads EIGHT BYTES BACKWARD
|
||||||
|
* from the pointer. sha256_hex(50000) therefore compiled clean and segfaulted.
|
||||||
|
*
|
||||||
|
* Exported, so the engram siblings stop re-deriving it. Anything that
|
||||||
|
* dereferences a slot without passing through here is the defect. */
|
||||||
|
int el_tagged(el_val_t v, uint32_t magic);
|
||||||
|
|
||||||
static int looks_like_heap_obj(el_val_t v) {
|
static int looks_like_heap_obj(el_val_t v) {
|
||||||
if (v == 0) return 0;
|
if (v == 0) return 0;
|
||||||
int64_t s = (int64_t)v;
|
int64_t s = (int64_t)v;
|
||||||
@@ -722,6 +736,12 @@ static int looks_like_heap_obj(el_val_t v) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int el_tagged(el_val_t v, uint32_t magic) {
|
||||||
|
if (!looks_like_heap_obj(v)) return 0;
|
||||||
|
return *(const uint32_t*)(uintptr_t)v == magic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void el_retain(el_val_t v) {
|
void el_retain(el_val_t v) {
|
||||||
if (!looks_like_heap_obj(v)) return;
|
if (!looks_like_heap_obj(v)) return;
|
||||||
ElHeader* h = (ElHeader*)(uintptr_t)v;
|
ElHeader* h = (ElHeader*)(uintptr_t)v;
|
||||||
@@ -8638,6 +8658,7 @@ static char* engram_first_n_chars(const char* s, size_t n) {
|
|||||||
* mutation (node/edge create, forget) is mirrored through the store's
|
* mutation (node/edge create, forget) is mirrored through the store's
|
||||||
* WAL-logged API so neuron.egm/neuron.wal stay authoritative.
|
* WAL-logged API so neuron.egm/neuron.wal stay authoritative.
|
||||||
* ══════════════════════════════════════════════════════════════════════════ */
|
* ══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
#include "engram_text.h" /* text: tokenize, token hygiene, loss signature */
|
||||||
#include "engram_store.h"
|
#include "engram_store.h"
|
||||||
#include "engram_vindex.h" /* M8: ANN (HNSW) index for activation seed selection */
|
#include "engram_vindex.h" /* M8: ANN (HNSW) index for activation seed selection */
|
||||||
#include "engram_geometry.h" /* M9: centered relational-neighborhood geometry (priming) */
|
#include "engram_geometry.h" /* M9: centered relational-neighborhood geometry (priming) */
|
||||||
@@ -9061,31 +9082,9 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
|||||||
* RIGHT NOW" — which is the regression question, and the one that
|
* RIGHT NOW" — which is the regression question, and the one that
|
||||||
* would have caught this in a day instead of two months.
|
* would have caught this in a day instead of two months.
|
||||||
*
|
*
|
||||||
* SIGNATURE. Conservative on purpose — a false alarm that cries corruption
|
* The SIGNATURE itself (eg_text_loss_signature) moved to engram_text.c on
|
||||||
* over ordinary punctuation is worse than useless. Two patterns, both of
|
* 2026-08-16 — it is plain C over <ctype.h> and touches nothing in here. The
|
||||||
* which are essentially absent from well-formed English prose:
|
* stock/flow gauges below stay, because they touch store and EL value types. */
|
||||||
* (a) alnum '?' alnum — "na?ve", "caf?s", "don?t". A real question mark
|
|
||||||
* never sits between two word characters.
|
|
||||||
* (b) ' ? ' followed by a lowercase letter — a lost em/en dash. A real
|
|
||||||
* question mark is not preceded by a space, and
|
|
||||||
* what follows one starts a new sentence.
|
|
||||||
* Deliberately NOT flagged: a trailing '?' after a word, '? ' before a
|
|
||||||
* capital, or '?' at end of string — all legitimate. This under-counts (it
|
|
||||||
* cannot see a mangled 'café ' where the '?' landed before a space), so the
|
|
||||||
* census is a floor on the damage, never an exaggeration of it. */
|
|
||||||
static int eg_text_loss_signature(const char* s) {
|
|
||||||
if (!s) return 0;
|
|
||||||
for (const char* p = s; *p; p++) {
|
|
||||||
if (*p != '?') continue;
|
|
||||||
unsigned char prev = (p == s) ? 0 : (unsigned char)p[-1];
|
|
||||||
unsigned char next = (unsigned char)p[1];
|
|
||||||
/* (a) sandwiched between word characters. */
|
|
||||||
if (isalnum(prev) && isalnum(next)) return 1;
|
|
||||||
/* (b) spaced, with lowercase continuation — a lost dash. */
|
|
||||||
if (prev == ' ' && next == ' ' && islower((unsigned char)p[2])) return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Damaged-node creations since process start. See the block comment above. */
|
/* Damaged-node creations since process start. See the block comment above. */
|
||||||
static int64_t _eg_txt_write_damaged = 0;
|
static int64_t _eg_txt_write_damaged = 0;
|
||||||
@@ -9697,37 +9696,7 @@ static int istr_contains(const char* hay, const char* needle) {
|
|||||||
* fix landed but never reached this release runtime — the copy the engram
|
* fix landed but never reached this release runtime — the copy the engram
|
||||||
* binary actually builds against.) */
|
* binary actually builds against.) */
|
||||||
#define ENGRAM_MAX_QTOKENS 32
|
#define ENGRAM_MAX_QTOKENS 32
|
||||||
#define ENGRAM_QTOK_LEN 256
|
/* ENGRAM_QTOK_LEN and engram_tokenize_query moved to engram_text.h/.c. */
|
||||||
|
|
||||||
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
|
|
||||||
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
|
|
||||||
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
|
|
||||||
static int engram_tokenize_query(const char* q,
|
|
||||||
char toks[][ENGRAM_QTOK_LEN], int maxtok) {
|
|
||||||
int n = 0;
|
|
||||||
if (!q) return 0;
|
|
||||||
const char* p = q;
|
|
||||||
while (*p && n < maxtok) {
|
|
||||||
while (*p && isspace((unsigned char)*p)) p++;
|
|
||||||
if (!*p) break;
|
|
||||||
char buf[ENGRAM_QTOK_LEN];
|
|
||||||
size_t tl = 0;
|
|
||||||
while (*p && !isspace((unsigned char)*p)) {
|
|
||||||
if (tl < sizeof(buf) - 1) buf[tl++] = *p;
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
buf[tl] = '\0';
|
|
||||||
if (tl == 0) continue;
|
|
||||||
int dup = 0;
|
|
||||||
for (int s = 0; s < n; s++) {
|
|
||||||
if (strcasecmp(toks[s], buf) == 0) { dup = 1; break; }
|
|
||||||
}
|
|
||||||
if (dup) continue;
|
|
||||||
memcpy(toks[n], buf, tl + 1);
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Count how many of the ntok distinct query tokens appear (case-insensitive)
|
/* Count how many of the ntok distinct query tokens appear (case-insensitive)
|
||||||
* in the node's content, label, or tags. 0 == no match. */
|
* in the node's content, label, or tags. 0 == no match. */
|
||||||
@@ -14430,7 +14399,67 @@ el_val_t engram_ise_log_append(el_val_t content_v){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fputs("\"}\n", f);
|
fputs("\"}\n", f);
|
||||||
|
/* RETENTION (2026-08-17 self-review). The on-graph ISE branch in
|
||||||
|
* server.el calls engram_prune_telemetry(48h) on every insert, but that
|
||||||
|
* branch is DEAD in production: ENGRAM_ISE_OFFGRAPH=1 is the live
|
||||||
|
* setting, so every state event lands here instead — and this path had
|
||||||
|
* no retention of any kind. Measured: 17.1 MB / 14,305 events over 3.56
|
||||||
|
* days = 4.81 MB/day, growing without bound (~1.76 GB/year). The graph
|
||||||
|
* got its telemetry-growth fix on 2026-07-16; moving telemetry off-graph
|
||||||
|
* moved the leak rather than closing it.
|
||||||
|
*
|
||||||
|
* Byte-bounded rather than time-bounded on purpose: this is a flat
|
||||||
|
* append-only file with no index, so size is the property that actually
|
||||||
|
* has to be bounded, and a byte check is O(1) against the handle we
|
||||||
|
* already hold (ftell) instead of an O(file) timestamp scan per append.
|
||||||
|
* At the measured rate the 64 MB default retains ~13 days — comfortably
|
||||||
|
* more history than the 48h the on-graph path kept.
|
||||||
|
*
|
||||||
|
* Compaction keeps the TAIL, never the head: engram_dreams_json reads
|
||||||
|
* the last ~2 MB of this file for dream-recall, so the recent end is the
|
||||||
|
* end that has a reader. KEEP is held well above that 2 MB window so
|
||||||
|
* recall is never truncated by a rotation. The honesty rail is
|
||||||
|
* preserved exactly as before — rotated-out remains "I don't remember",
|
||||||
|
* never a synthesized dream; this only makes the forgetting bounded and
|
||||||
|
* explicit instead of deferred forever. */
|
||||||
|
long pos = ftell(f);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
{
|
||||||
|
long maxb = 64L*1024L*1024L;
|
||||||
|
long keepb = 16L*1024L*1024L;
|
||||||
|
const char* mv = getenv("ENGRAM_ISE_LOG_MAX_BYTES");
|
||||||
|
if (mv && *mv) { long v = atol(mv); if (v > 0) maxb = v; }
|
||||||
|
if (keepb > maxb/2) keepb = maxb/2;
|
||||||
|
if (pos > 0 && pos > maxb) {
|
||||||
|
FILE* rf = fopen(path, "rb");
|
||||||
|
if (rf) {
|
||||||
|
if (fseek(rf, pos - keepb, SEEK_SET) == 0) {
|
||||||
|
char* buf = (char*)malloc((size_t)keepb + 1);
|
||||||
|
if (buf) {
|
||||||
|
size_t rd = fread(buf, 1, (size_t)keepb, rf);
|
||||||
|
buf[rd] = 0;
|
||||||
|
/* Resume at the first LINE boundary so the tail never
|
||||||
|
* begins with a half-written JSON record. */
|
||||||
|
char* start = memchr(buf, '\n', rd);
|
||||||
|
start = start ? start + 1 : buf;
|
||||||
|
size_t keep_n = rd - (size_t)(start - buf);
|
||||||
|
char tmp[4096];
|
||||||
|
snprintf(tmp, sizeof tmp, "%s/state-events.jsonl.tmp", dir);
|
||||||
|
FILE* wf = fopen(tmp, "wb");
|
||||||
|
if (wf) {
|
||||||
|
int ok = (fwrite(start, 1, keep_n, wf) == keep_n);
|
||||||
|
fclose(wf);
|
||||||
|
/* Only replace the live log if the tail was written
|
||||||
|
* in full — a short write must not destroy history. */
|
||||||
|
if (ok) rename(tmp, path); else remove(tmp);
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(rf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return EL_INT(1);
|
return EL_INT(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16389,29 +16418,6 @@ el_val_t engram_label_df(el_val_t term) {
|
|||||||
#define ENGRAM_ST_TOKLEN 64
|
#define ENGRAM_ST_TOKLEN 64
|
||||||
#define ENGRAM_ST_SCANCHARS 400
|
#define ENGRAM_ST_SCANCHARS 400
|
||||||
|
|
||||||
/* Trim leading/trailing non-alphanumerics, then accept only tokens whose core
|
|
||||||
* is alphanumeric plus '-' and '_' with at least 3 letters. This subsumes the
|
|
||||||
* quoted-title guard (2026-07-25) and the "<!--" flood (2026-08-03)
|
|
||||||
* structurally: markup and punctuation-bearing tokens never become
|
|
||||||
* candidates, rather than being blocklisted after the fact. */
|
|
||||||
static int eg_st_clean_token(const char* raw, size_t rawlen,
|
|
||||||
char* out, size_t outcap) {
|
|
||||||
size_t s = 0, e = rawlen;
|
|
||||||
while (s < e && !isalnum((unsigned char)raw[s])) s++;
|
|
||||||
while (e > s && !isalnum((unsigned char)raw[e - 1])) e--;
|
|
||||||
size_t len = e - s;
|
|
||||||
if (len < 4 || len >= outcap) return 0;
|
|
||||||
int alpha = 0;
|
|
||||||
for (size_t i = 0; i < len; i++) {
|
|
||||||
unsigned char c = (unsigned char)raw[s + i];
|
|
||||||
if (isalpha(c)) alpha++;
|
|
||||||
else if (!isdigit(c) && c != '-' && c != '_') return 0;
|
|
||||||
}
|
|
||||||
if (alpha < 3) return 0;
|
|
||||||
memcpy(out, raw + s, len);
|
|
||||||
out[len] = '\0';
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ENGRAM_ST_DEBUG=1 dumps the full scored candidate set to stderr. One
|
/* ENGRAM_ST_DEBUG=1 dumps the full scored candidate set to stderr. One
|
||||||
* cached branch in production. This exists because the first live run of this
|
* cached branch in production. This exists because the first live run of this
|
||||||
@@ -16424,32 +16430,6 @@ static int _eg_st_debug(void) {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Word-boundary document frequency. engram_label_df uses istr_contains, i.e.
|
|
||||||
* SUBSTRING matching, and that is the wrong estimator for term specificity on
|
|
||||||
* short tokens: "them" hits inside "theme" and "anthem", "about" and "whole"
|
|
||||||
* come back with df 2 and 1 rather than 0. That matters here specifically
|
|
||||||
* because the min_df floor is what rejects English function words, and it can
|
|
||||||
* only do that job if their df is honestly zero. Substring df quietly handed
|
|
||||||
* them a survival ticket. Measured on the live store before this fix, "whole"
|
|
||||||
* (df=1, idf=8.76) and "about" (df=2, idf=8.36) were outscoring real topical
|
|
||||||
* terms and losing only on position — one node whose text happened to open
|
|
||||||
* with a function word would have seeded on it.
|
|
||||||
*
|
|
||||||
* engram_label_df keeps substring semantics: it is a separate published
|
|
||||||
* measure with existing callers, and changing it underneath them is not this
|
|
||||||
* change's business. */
|
|
||||||
static int eg_st_label_has_word(const char* hay, const char* word) {
|
|
||||||
size_t wl = strlen(word);
|
|
||||||
for (const char* p = hay; *p; p++) {
|
|
||||||
if (strncasecmp(p, word, wl) != 0) continue;
|
|
||||||
char before = (p == hay) ? '\0' : p[-1];
|
|
||||||
char after = p[wl];
|
|
||||||
if (before && (isalnum((unsigned char)before) || before == '_')) continue;
|
|
||||||
if (after && (isalnum((unsigned char)after) || after == '_')) continue;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* YAKE T_Case, adapted to this corpus. YAKE up-weights all-caps tokens
|
/* YAKE T_Case, adapted to this corpus. YAKE up-weights all-caps tokens
|
||||||
* because in ordinary prose an acronym is rare and carries topic. That
|
* because in ordinary prose an acronym is rare and carries topic. That
|
||||||
@@ -17091,24 +17071,140 @@ void dharma_emit(el_val_t event_type, el_val_t payload) {
|
|||||||
free(b.buf);
|
free(b.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* engram_boundary_beat(op_name) — the decorated-fn boundary AUTO-EMIT (VBD seam).
|
/* engram_boundary_beat(op_name, construct) — decorated-fn boundary AUTO-EMIT
|
||||||
* codegen injects a single call to this at the entry of every @manager/@accessor
|
* (VBD seam). codegen injects a single call to this at the entry of every
|
||||||
* decorated fn, so a decorated op self-reports with ZERO hand-written
|
* @manager/@accessor decorated fn, so a decorated op self-reports with ZERO
|
||||||
* instrumentation in its body:
|
* hand-written instrumentation in its body:
|
||||||
* (1) afferent counter++ — the boundary was crossed
|
* (1) afferent counter++ — the boundary was crossed
|
||||||
* (2) engram_chrono_tick() — interoception: the mind senses its own op firing
|
* (2) engram_chrono_tick() — interoception: the mind senses its own op firing
|
||||||
* (3) engram_strengthen(self-anchor)— reinforce the self-activity anchor (an
|
* (3) engram_strengthen(self-anchor)— reinforce the self-activity anchor (an
|
||||||
* activation-count/salience bump, NOT a content/edge write — identity
|
* activation-count/salience bump, NOT a content/edge write — identity
|
||||||
* write-protection is untouched)
|
* write-protection is untouched)
|
||||||
* (4) dharma_emit(neuron.op.<name>) — provenance on the shared bus transport
|
* (4) dharma_emit(neuron.op.<name>) — provenance on the shared bus transport
|
||||||
* (same bus the swarm peers field on); bumps _eg_dharma_emits. */
|
* (same bus the swarm peers field on); bumps _eg_dharma_emits.
|
||||||
el_val_t engram_boundary_beat(el_val_t op_name) {
|
*
|
||||||
|
* `construct` is the DECORATOR that caused the beat ("manager" / "accessor"),
|
||||||
|
* carried in the payload. Before it, the beat reported which fn crossed a
|
||||||
|
* boundary but never which construct put the beat there — so boundary events
|
||||||
|
* accumulated in the graph with no attribution, and no decorator could ever be
|
||||||
|
* measured. "Is this construct earning its keep" was an argument; with the
|
||||||
|
* attribution it is a traversal. The payload is built here rather than at the
|
||||||
|
* call site so the format has exactly one author.
|
||||||
|
*
|
||||||
|
* The construct name comes from a closed set codegen controls, so the
|
||||||
|
* unescaped snprintf below cannot be injected through. That is NOT true of
|
||||||
|
* dharma_emit generally — its payloads are hand-concatenated at 39 call sites
|
||||||
|
* with no escaping, and a value containing a quote silently corrupts the
|
||||||
|
* event. Fixing that is a separate change; this one does not add to it. */
|
||||||
|
/* ── Runtime construct seam ───────────────────────────────────────────────
|
||||||
|
* The crossing is resolved at EXECUTION, not at emission. Codegen emits one
|
||||||
|
* indirection per function; which constructs apply is read from a table that
|
||||||
|
* can be written AFTER the binary exists.
|
||||||
|
*
|
||||||
|
* This is the whole hypothesis under test: a compiler whose one compiled
|
||||||
|
* mechanism is language extension can compose without recompilation. If a
|
||||||
|
* construct declared after the build applies to a running program, the five
|
||||||
|
* compile-time declaration kinds were the wrong shape.
|
||||||
|
*
|
||||||
|
* Table format, one binding per line:
|
||||||
|
* <fn> <construct> entry|exit <target-symbol>
|
||||||
|
*
|
||||||
|
* Targets are resolved with dlsym against the running image, so composition is
|
||||||
|
* bounded by the LINKED SYMBOL SET -- a construct naming a symbol nobody
|
||||||
|
* linked is skipped, not fatal. That bound is the honest limit on "endless". */
|
||||||
|
#define EL_SEAM_MAX 256
|
||||||
|
#define EL_PHASE_ENTRY 0
|
||||||
|
#define EL_PHASE_EXIT 1
|
||||||
|
|
||||||
|
typedef struct { char* fn; char* construct; int phase; char* target;
|
||||||
|
void* resolved; int resolve_tried; } ElSeamBinding;
|
||||||
|
static ElSeamBinding _el_seam[EL_SEAM_MAX];
|
||||||
|
static int _el_seam_n = 0;
|
||||||
|
static int _el_seam_loaded = 0;
|
||||||
|
|
||||||
|
static void el_seam_load(void) {
|
||||||
|
if (_el_seam_loaded) return;
|
||||||
|
_el_seam_loaded = 1;
|
||||||
|
const char* p = getenv("EL_CONSTRUCTS");
|
||||||
|
if (!p || !*p) return;
|
||||||
|
FILE* f = fopen(p, "r");
|
||||||
|
if (!f) return;
|
||||||
|
char line[512];
|
||||||
|
while (fgets(line, sizeof line, f) && _el_seam_n < EL_SEAM_MAX) {
|
||||||
|
char fn[128], con[128], ph[32], tgt[128];
|
||||||
|
if (sscanf(line, "%127s %127s %31s %127s", fn, con, ph, tgt) == 4) {
|
||||||
|
if (fn[0] == '#') continue;
|
||||||
|
_el_seam[_el_seam_n].fn = el_strdup(fn);
|
||||||
|
_el_seam[_el_seam_n].construct = el_strdup(con);
|
||||||
|
_el_seam[_el_seam_n].phase = (strcmp(ph, "exit") == 0) ? EL_PHASE_EXIT :
|
||||||
|
(strcmp(ph, "wrap") == 0) ? 2 : EL_PHASE_ENTRY;
|
||||||
|
_el_seam[_el_seam_n].target = el_strdup(tgt);
|
||||||
|
_el_seam_n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el_seam_wrap — the seam calls the body itself, so a bound construct can
|
||||||
|
* control invocation: run it zero times, N times, or around a transaction.
|
||||||
|
* With no binding it is a direct call through the thunk, which is what the
|
||||||
|
* unwrapped code did anyway. */
|
||||||
|
el_val_t el_seam_wrap(el_val_t fn_v, el_val_t (*body)(void*), void* env) {
|
||||||
|
if (!_el_seam_loaded) el_seam_load();
|
||||||
|
if (_el_seam_n == 0) return body(env);
|
||||||
|
const char* fn = EL_CSTR(fn_v);
|
||||||
|
if (!fn) return body(env);
|
||||||
|
for (int i = 0; i < _el_seam_n; i++) {
|
||||||
|
if (_el_seam[i].phase != 2) continue; /* 2 = wrap */
|
||||||
|
if (strcmp(_el_seam[i].fn, fn) != 0) continue;
|
||||||
|
if (!_el_seam[i].resolve_tried) {
|
||||||
|
_el_seam[i].resolved = dlsym(RTLD_DEFAULT, _el_seam[i].target);
|
||||||
|
_el_seam[i].resolve_tried = 1;
|
||||||
|
}
|
||||||
|
if (!_el_seam[i].resolved) continue;
|
||||||
|
el_val_t (*fp)(el_val_t, el_val_t, el_val_t (*)(void*), void*) =
|
||||||
|
(el_val_t (*)(el_val_t, el_val_t, el_val_t (*)(void*), void*))_el_seam[i].resolved;
|
||||||
|
return fp(fn_v, el_wrap_str(el_strdup(_el_seam[i].construct)), body, env);
|
||||||
|
}
|
||||||
|
return body(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t el_seam_run(el_val_t fn_v, el_val_t phase_v, el_val_t result) {
|
||||||
|
if (!_el_seam_loaded) el_seam_load();
|
||||||
|
if (_el_seam_n == 0) return result; /* the common path: no bindings */
|
||||||
|
const char* fn = EL_CSTR(fn_v);
|
||||||
|
if (!fn) return result;
|
||||||
|
int phase = (int)phase_v;
|
||||||
|
el_val_t last = result;
|
||||||
|
for (int i = 0; i < _el_seam_n; i++) {
|
||||||
|
if (_el_seam[i].phase != phase) continue;
|
||||||
|
if (strcmp(_el_seam[i].fn, fn) != 0) continue;
|
||||||
|
/* Resolve ONCE. dlsym walks the dynamic symbol table on every call, and
|
||||||
|
* measured at 6.6x on a hot path with two bindings -- the table scan was
|
||||||
|
* never the cost. What is hot must stay resolved; this is the smallest
|
||||||
|
* form of the same thing salience does for memory. */
|
||||||
|
if (!_el_seam[i].resolve_tried) {
|
||||||
|
_el_seam[i].resolved = dlsym(RTLD_DEFAULT, _el_seam[i].target);
|
||||||
|
_el_seam[i].resolve_tried = 1;
|
||||||
|
}
|
||||||
|
void* sym = _el_seam[i].resolved;
|
||||||
|
if (!sym) continue; /* unlinked target: skipped, not fatal */
|
||||||
|
el_val_t (*fp)(el_val_t, el_val_t, el_val_t) =
|
||||||
|
(el_val_t (*)(el_val_t, el_val_t, el_val_t))sym;
|
||||||
|
last = fp(fn_v, el_wrap_str(el_strdup(_el_seam[i].construct)), last);
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct) {
|
||||||
_eg_aff_boundary_ops++;
|
_eg_aff_boundary_ops++;
|
||||||
engram_chrono_tick();
|
engram_chrono_tick();
|
||||||
engram_strengthen(EL_STR("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"));
|
engram_strengthen(EL_STR("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"));
|
||||||
const char* nm = EL_CSTR(op_name); if (!nm) nm = "";
|
const char* nm = EL_CSTR(op_name); if (!nm) nm = "";
|
||||||
|
const char* ct = EL_CSTR(construct); if (!ct) ct = "";
|
||||||
char ev[160]; snprintf(ev, sizeof ev, "neuron.op.%s", nm);
|
char ev[160]; snprintf(ev, sizeof ev, "neuron.op.%s", nm);
|
||||||
dharma_emit(el_wrap_str(el_strdup(ev)), EL_STR(""));
|
char pl[192]; snprintf(pl, sizeof pl, "{\"construct\":\"%s\"}", ct);
|
||||||
|
dharma_emit(el_wrap_str(el_strdup(ev)), el_wrap_str(el_strdup(pl)));
|
||||||
return (el_val_t)0;
|
return (el_val_t)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18196,8 +18292,11 @@ static int el_bin_lookup(const void* p, size_t* out_len) {
|
|||||||
/* Avoid reading off the front of a page on tiny pointers (e.g. NULs
|
/* Avoid reading off the front of a page on tiny pointers (e.g. NULs
|
||||||
* passed in as int-cast values). 4096 is a safe lower bound on any
|
* passed in as int-cast values). 4096 is a safe lower bound on any
|
||||||
* platform we target. */
|
* platform we target. */
|
||||||
if ((uintptr_t)p < 4096) return 0;
|
/* Reads BACKWARD, so the HEADER address is what must be validated -- and a
|
||||||
const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t));
|
* 4096 floor alone let sha256_hex(50000) through to a SIGSEGV. */
|
||||||
|
const char* hp = (const char*)p - sizeof(el_bin_hdr_t);
|
||||||
|
if (!looks_like_heap_obj((el_val_t)(uintptr_t)hp)) return 0;
|
||||||
|
const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)hp;
|
||||||
if (hdr->magic != EL_MAGIC_BIN) return 0;
|
if (hdr->magic != EL_MAGIC_BIN) return 0;
|
||||||
*out_len = hdr->length;
|
*out_len = hdr->length;
|
||||||
return 1;
|
return 1;
|
||||||
@@ -18207,7 +18306,12 @@ static int el_bin_lookup(const void* p, size_t* out_len) {
|
|||||||
static size_t el_input_len(const char* s) {
|
static size_t el_input_len(const char* s) {
|
||||||
size_t n;
|
size_t n;
|
||||||
if (el_bin_lookup(s, &n)) return n;
|
if (el_bin_lookup(s, &n)) return n;
|
||||||
return s ? strlen(s) : 0;
|
/* The FALLBACK is the hazard, not the tagged lookup. A NULL check does not
|
||||||
|
* establish that a slot is a pointer: el_val_t carries integers too, so
|
||||||
|
* strlen() on `sha256_hex(50000)` walks address 50000. Guarding the tagged
|
||||||
|
* path alone left this untouched and the SIGSEGV unchanged -- measured. */
|
||||||
|
if (!looks_like_heap_obj((el_val_t)(uintptr_t)s)) return 0;
|
||||||
|
return strlen(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */
|
/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */
|
||||||
@@ -19809,22 +19913,84 @@ void log_warn(el_val_t msg_v) {
|
|||||||
* become a convention. */
|
* become a convention. */
|
||||||
static int el_singleton_fd = -1;
|
static int el_singleton_fd = -1;
|
||||||
static char el_singleton_path[1024];
|
static char el_singleton_path[1024];
|
||||||
|
static char el_singleton_state[1024];
|
||||||
|
|
||||||
static const char* el_singleton_dir(void) {
|
/* el_singleton_acquire — claim exclusive use of the guarded STATE, or refuse to
|
||||||
const char* d = getenv("EL_SINGLETON_DIR");
|
* start. Compiler-injected as the FIRST statement of main() for any program
|
||||||
if (d && *d) return d;
|
* whose `program` block declares `singleton:` (which must also declare
|
||||||
d = getenv("TMPDIR");
|
* `guards:` — see lang/spec/language.md §18.2).
|
||||||
if (d && *d) return d;
|
*
|
||||||
return "/tmp";
|
* GUARD THE THING, NOT THE NAME.
|
||||||
}
|
*
|
||||||
|
* Until 2026-08-16 this lock was keyed on the program's NAME and on $TMPDIR —
|
||||||
/* el_singleton_acquire — claim exclusive process identity, or refuse to start.
|
* `$EL_SINGLETON_DIR|$TMPDIR|/tmp` + `/el-singleton-<name>.lock` — and never
|
||||||
* Compiler-injected as the FIRST statement of main() for any program whose
|
* consulted the state it claimed to protect. Its own refusal message said
|
||||||
* `program` block declares `singleton:`. */
|
* "Refusing to start a second instance against the same state" while it had not
|
||||||
el_val_t el_singleton_acquire(el_val_t id_v) {
|
* looked at any state. Measured, it failed in BOTH directions:
|
||||||
|
*
|
||||||
|
* - FALSE POSITIVE: two engrams against genuinely DIFFERENT data dirs could
|
||||||
|
* not coexist. The second was refused, naming the first's pid — for sharing
|
||||||
|
* a name, not a store.
|
||||||
|
* - FALSE NEGATIVE (the dangerous one): `TMPDIR=/tmp/other` let a second
|
||||||
|
* instance start against the SAME data dir with no complaint. That is
|
||||||
|
* exactly the two-instance data-loss condition the guard exists to prevent,
|
||||||
|
* and the workaround was one environment variable.
|
||||||
|
*
|
||||||
|
* Both are one error: the identity of the resource had been replaced by a label
|
||||||
|
* for it. The fix is to put the lock file INSIDE the state it guards:
|
||||||
|
*
|
||||||
|
* <state>/.el-singleton-<id>.lock
|
||||||
|
*
|
||||||
|
* That placement is the whole mechanism, and it is why there is no hashing, no
|
||||||
|
* canonical-path registry, and no environment variable left to subvert:
|
||||||
|
*
|
||||||
|
* - Same directory => same file => same inode => the flock CONTENDS. There is
|
||||||
|
* no TMPDIR in the key, so there is nothing to change to get past it.
|
||||||
|
* - Different dirs => different files => no contention. Two stores are two
|
||||||
|
* stores; they were never in conflict and are no longer treated as if they
|
||||||
|
* were.
|
||||||
|
* - Different SPELLINGS of one directory — trailing slash, `x/../x`, a symlink
|
||||||
|
* — resolve to the same inode in the kernel's own path walk, so they contend
|
||||||
|
* without this code comparing strings at all. Path canonicalisation here is
|
||||||
|
* for the human-readable message, never for the decision.
|
||||||
|
*
|
||||||
|
* Kept, deliberately, from the version this replaces: it is an flock and not a
|
||||||
|
* pidfile (the kernel releases it on crash and on SIGKILL, so there is no stale
|
||||||
|
* state and therefore no "delete the lock file to get unstuck" ritual), and it
|
||||||
|
* reports the HOLDER'S PID (added because a stale process survived `pkill -f`
|
||||||
|
* and went on answering probes; "already running" is not actionable, a pid is).
|
||||||
|
*
|
||||||
|
* Changed: the message is now TRUE. It says "the same state" because the lock it
|
||||||
|
* failed to take lives in that state, and it names the state it checked. */
|
||||||
|
el_val_t el_singleton_acquire(el_val_t id_v, el_val_t state_v) {
|
||||||
const char* id = EL_CSTR(id_v);
|
const char* id = EL_CSTR(id_v);
|
||||||
if (!id || !*id) return EL_NULL;
|
if (!id || !*id) return EL_NULL;
|
||||||
|
|
||||||
|
/* A singleton with nothing to guard is the defect this function exists to
|
||||||
|
* remove; refuse rather than silently fall back to name-keying. The compiler
|
||||||
|
* rejects `singleton:` without `guards:`, so reaching this is a toolchain
|
||||||
|
* mismatch, not a user mistake — say so. */
|
||||||
|
const char* state = EL_CSTR(state_v);
|
||||||
|
if (!state || !*state) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[el] FATAL: singleton '%s' was given no state to guard.\n"
|
||||||
|
"[el] A lock keyed on a program's NAME instead of on the state it\n"
|
||||||
|
"[el] protects is not a guard: it refuses unrelated instances and\n"
|
||||||
|
"[el] permits concurrent ones. Declare `guards: <path>` alongside\n"
|
||||||
|
"[el] `singleton:` in the program block (spec §18.2).\n", id);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canonicalise so the operator is told WHICH directory was checked, in one
|
||||||
|
* spelling, whatever spelling they typed. This is a readability measure, not
|
||||||
|
* the mechanism: realpath() may fail (the directory may not exist yet) and
|
||||||
|
* correctness must not depend on it — when it succeeds it names the same
|
||||||
|
* directory, and when it does not we fall back to the path as given and the
|
||||||
|
* kernel's own path walk still collapses the spellings at open() time. */
|
||||||
|
char* rp = realpath(state, NULL);
|
||||||
|
snprintf(el_singleton_state, sizeof(el_singleton_state), "%s", rp ? rp : state);
|
||||||
|
free(rp);
|
||||||
|
|
||||||
/* Sanitise the id into a filename. */
|
/* Sanitise the id into a filename. */
|
||||||
char safe[256];
|
char safe[256];
|
||||||
size_t si = 0;
|
size_t si = 0;
|
||||||
@@ -19835,13 +20001,25 @@ el_val_t el_singleton_acquire(el_val_t id_v) {
|
|||||||
safe[si++] = (char)(ok ? c : '-');
|
safe[si++] = (char)(ok ? c : '-');
|
||||||
}
|
}
|
||||||
safe[si] = '\0';
|
safe[si] = '\0';
|
||||||
|
/* THE MECHANISM: the lock lives inside the state it guards. Two spellings of
|
||||||
|
* one directory name one file; two directories name two files. Note there is
|
||||||
|
* no $TMPDIR and no $EL_SINGLETON_DIR in this path — the escape hatch that
|
||||||
|
* made the guard bypassable is gone because there is nowhere left to put it. */
|
||||||
snprintf(el_singleton_path, sizeof(el_singleton_path),
|
snprintf(el_singleton_path, sizeof(el_singleton_path),
|
||||||
"%s/el-singleton-%s.lock", el_singleton_dir(), safe);
|
"%s/.el-singleton-%s.lock", el_singleton_state, safe);
|
||||||
|
|
||||||
int fd = open(el_singleton_path, O_RDWR | O_CREAT, 0644);
|
int fd = open(el_singleton_path, O_RDWR | O_CREAT, 0644);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
fprintf(stderr, "[el] FATAL: singleton '%s': cannot open lock file %s: %s\n",
|
/* Unguardable state. Refusing is the only honest option: starting anyway
|
||||||
id, el_singleton_path, strerror(errno));
|
* would mean running unguarded against exactly the store the guard is
|
||||||
|
* here to protect. */
|
||||||
|
fprintf(stderr,
|
||||||
|
"[el] FATAL: singleton '%s': cannot open the lock inside the state it guards.\n"
|
||||||
|
"[el] state: %s\n"
|
||||||
|
"[el] lock: %s (%s)\n"
|
||||||
|
"[el] The guarded directory must exist and be writable. Refusing to\n"
|
||||||
|
"[el] start unguarded against it.\n",
|
||||||
|
id, el_singleton_state, el_singleton_path, strerror(errno));
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||||
@@ -19857,11 +20035,14 @@ el_val_t el_singleton_acquire(el_val_t id_v) {
|
|||||||
fprintf(stderr, "[el] FATAL: another instance of '%s' is already running", id);
|
fprintf(stderr, "[el] FATAL: another instance of '%s' is already running", id);
|
||||||
if (holder > 0) fprintf(stderr, " (pid %ld)", holder);
|
if (holder > 0) fprintf(stderr, " (pid %ld)", holder);
|
||||||
fprintf(stderr, ".\n"
|
fprintf(stderr, ".\n"
|
||||||
"[el] lock: %s\n"
|
"[el] state: %s\n"
|
||||||
|
"[el] lock: %s\n"
|
||||||
"[el] Refusing to start a second instance against the same\n"
|
"[el] Refusing to start a second instance against the same\n"
|
||||||
"[el] state. Stop the running one and VERIFY it is gone\n"
|
"[el] state. Two writers against one store is data loss, not a\n"
|
||||||
"[el] (ps -p <pid>) before retrying.\n",
|
"[el] warning. Stop the running one and VERIFY it is gone\n"
|
||||||
el_singleton_path);
|
"[el] (ps -p %ld) before retrying — or point this instance at a\n"
|
||||||
|
"[el] different state, which is permitted and is not refused.\n",
|
||||||
|
el_singleton_state, el_singleton_path, holder > 0 ? holder : (long)0);
|
||||||
close(fd);
|
close(fd);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -887,7 +887,10 @@ el_val_t engram_age_field(el_val_t delta_ms);
|
|||||||
el_val_t engram_age_field_catchup(void);
|
el_val_t engram_age_field_catchup(void);
|
||||||
el_val_t engram_chrono_persist_tick(void);
|
el_val_t engram_chrono_persist_tick(void);
|
||||||
el_val_t engram_chrono_tick(void);
|
el_val_t engram_chrono_tick(void);
|
||||||
el_val_t engram_boundary_beat(el_val_t op_name); /* API-reshape decorator-seam auto-emit */
|
el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct);
|
||||||
|
int el_tagged(el_val_t v, uint32_t magic); /* the gate: validate a slot BEFORE dereferencing it */
|
||||||
|
el_val_t el_seam_run(el_val_t fn_name, el_val_t phase, el_val_t result); /* runtime construct seam */
|
||||||
|
el_val_t el_seam_wrap(el_val_t fn_name, el_val_t (*body)(void*), void* env); /* runtime invocation control */ /* API-reshape decorator-seam auto-emit; construct = the decorator that caused the beat */
|
||||||
el_val_t engram_self_anchor_capture(void);
|
el_val_t engram_self_anchor_capture(void);
|
||||||
el_val_t engram_self_drift_json(void);
|
el_val_t engram_self_drift_json(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_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||||
@@ -1177,7 +1180,7 @@ el_val_t __env_get(el_val_t key);
|
|||||||
* All three are COMPILER-INJECTED at the head of main() — they are not meant to
|
* All three are COMPILER-INJECTED at the head of main() — they are not meant to
|
||||||
* be written by hand, which is the point: the guarantee cannot be forgotten at a
|
* be written by hand, which is the point: the guarantee cannot be forgotten at a
|
||||||
* call site because there is no call site. */
|
* call site because there is no call site. */
|
||||||
el_val_t el_singleton_acquire(el_val_t id); /* §18.1 process identity */
|
el_val_t el_singleton_acquire(el_val_t id, el_val_t state); /* §18.2 process identity — keyed on the guarded state */
|
||||||
el_val_t el_config_declare(el_val_t name, el_val_t type,
|
el_val_t el_config_declare(el_val_t name, el_val_t type,
|
||||||
el_val_t deflt, el_val_t has_default,
|
el_val_t deflt, el_val_t has_default,
|
||||||
el_val_t required); /* §18.2 config schema */
|
el_val_t required); /* §18.2 config schema */
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* engram_text.c — see engram_text.h.
|
||||||
|
*
|
||||||
|
* Moved verbatim out of el_runtime.c (2026-08-16). Bodies are unchanged; only
|
||||||
|
* `static` was dropped so they link from this translation unit, and each
|
||||||
|
* function's doc comment travelled with it.
|
||||||
|
*/
|
||||||
|
#include "engram_text.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
|
||||||
|
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
|
||||||
|
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
|
||||||
|
int engram_tokenize_query(const char* q,
|
||||||
|
char toks[][ENGRAM_QTOK_LEN], int maxtok) {
|
||||||
|
int n = 0;
|
||||||
|
if (!q) return 0;
|
||||||
|
const char* p = q;
|
||||||
|
while (*p && n < maxtok) {
|
||||||
|
while (*p && isspace((unsigned char)*p)) p++;
|
||||||
|
if (!*p) break;
|
||||||
|
char buf[ENGRAM_QTOK_LEN];
|
||||||
|
size_t tl = 0;
|
||||||
|
while (*p && !isspace((unsigned char)*p)) {
|
||||||
|
if (tl < sizeof(buf) - 1) buf[tl++] = *p;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
buf[tl] = '\0';
|
||||||
|
if (tl == 0) continue;
|
||||||
|
int dup = 0;
|
||||||
|
for (int s = 0; s < n; s++) {
|
||||||
|
if (strcasecmp(toks[s], buf) == 0) { dup = 1; break; }
|
||||||
|
}
|
||||||
|
if (dup) continue;
|
||||||
|
memcpy(toks[n], buf, tl + 1);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trim leading/trailing non-alphanumerics, then accept only tokens whose core
|
||||||
|
* is alphanumeric plus '-' and '_' with at least 3 letters. This subsumes the
|
||||||
|
* quoted-title guard (2026-07-25) and the "<!--" flood (2026-08-03)
|
||||||
|
* structurally: markup and punctuation-bearing tokens never become
|
||||||
|
* candidates, rather than being blocklisted after the fact. */
|
||||||
|
int eg_st_clean_token(const char* raw, size_t rawlen,
|
||||||
|
char* out, size_t outcap) {
|
||||||
|
size_t s = 0, e = rawlen;
|
||||||
|
while (s < e && !isalnum((unsigned char)raw[s])) s++;
|
||||||
|
while (e > s && !isalnum((unsigned char)raw[e - 1])) e--;
|
||||||
|
size_t len = e - s;
|
||||||
|
if (len < 4 || len >= outcap) return 0;
|
||||||
|
int alpha = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
unsigned char c = (unsigned char)raw[s + i];
|
||||||
|
if (isalpha(c)) alpha++;
|
||||||
|
else if (!isdigit(c) && c != '-' && c != '_') return 0;
|
||||||
|
}
|
||||||
|
if (alpha < 3) return 0;
|
||||||
|
memcpy(out, raw + s, len);
|
||||||
|
out[len] = '\0';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Word-boundary document frequency. engram_label_df uses istr_contains, i.e.
|
||||||
|
* SUBSTRING matching, and that is the wrong estimator for term specificity on
|
||||||
|
* short tokens: "them" hits inside "theme" and "anthem", "about" and "whole"
|
||||||
|
* come back with df 2 and 1 rather than 0. That matters here specifically
|
||||||
|
* because the min_df floor is what rejects English function words, and it can
|
||||||
|
* only do that job if their df is honestly zero. Substring df quietly handed
|
||||||
|
* them a survival ticket. Measured on the live store before this fix, "whole"
|
||||||
|
* (df=1, idf=8.76) and "about" (df=2, idf=8.36) were outscoring real topical
|
||||||
|
* terms and losing only on position — one node whose text happened to open
|
||||||
|
* with a function word would have seeded on it.
|
||||||
|
*
|
||||||
|
* engram_label_df keeps substring semantics: it is a separate published
|
||||||
|
* measure with existing callers, and changing it underneath them is not this
|
||||||
|
* change's business. */
|
||||||
|
int eg_st_label_has_word(const char* hay, const char* word) {
|
||||||
|
size_t wl = strlen(word);
|
||||||
|
for (const char* p = hay; *p; p++) {
|
||||||
|
if (strncasecmp(p, word, wl) != 0) continue;
|
||||||
|
char before = (p == hay) ? '\0' : p[-1];
|
||||||
|
char after = p[wl];
|
||||||
|
if (before && (isalnum((unsigned char)before) || before == '_')) continue;
|
||||||
|
if (after && (isalnum((unsigned char)after) || after == '_')) continue;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-damage signature. Extracted with the function from el_runtime.c's
|
||||||
|
* "Text-integrity instrumentation" block; the stock/flow gauges that use it
|
||||||
|
* (engram_text_health_json, _eg_txt_write_damaged) stay there because they
|
||||||
|
* touch store and EL value types.
|
||||||
|
*
|
||||||
|
* SIGNATURE. Conservative on purpose — a false alarm that cries corruption
|
||||||
|
* over ordinary punctuation is worse than useless. Two patterns, both of
|
||||||
|
* which are essentially absent from well-formed English prose:
|
||||||
|
* (a) alnum '?' alnum — "na?ve", "caf?s", "don?t". A real question mark
|
||||||
|
* never sits between two word characters.
|
||||||
|
* (b) ' ? ' followed by a lowercase letter — a lost em/en dash. A real
|
||||||
|
* question mark is not preceded by a space, and
|
||||||
|
* what follows one starts a new sentence.
|
||||||
|
* Deliberately NOT flagged: a trailing '?' after a word, '? ' before a
|
||||||
|
* capital, or '?' at end of string — all legitimate. This under-counts (it
|
||||||
|
* cannot see a mangled 'café ' where the '?' landed before a space), so the
|
||||||
|
* census is a floor on the damage, never an exaggeration of it. */
|
||||||
|
int eg_text_loss_signature(const char* s) {
|
||||||
|
if (!s) return 0;
|
||||||
|
for (const char* p = s; *p; p++) {
|
||||||
|
if (*p != '?') continue;
|
||||||
|
unsigned char prev = (p == s) ? 0 : (unsigned char)p[-1];
|
||||||
|
unsigned char next = (unsigned char)p[1];
|
||||||
|
/* (a) sandwiched between word characters. */
|
||||||
|
if (isalnum(prev) && isalnum(next)) return 1;
|
||||||
|
/* (b) spaced, with lowercase continuation — a lost dash. */
|
||||||
|
if (prev == ' ' && next == ' ' && islower((unsigned char)p[2])) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* engram_text.h — text handling for the engram: query tokenization, candidate
|
||||||
|
* token hygiene, word-boundary matching, and the text-damage signature.
|
||||||
|
*
|
||||||
|
* WHY THIS FILE EXISTS
|
||||||
|
* --------------------
|
||||||
|
* These functions lived in el_runtime.c, which is a 2026-05-03 build shim that
|
||||||
|
* was scheduled for deletion, never retired, and grew to 20,527 lines. They do
|
||||||
|
* not belong there: they touch no EL value type and no engram store type. They
|
||||||
|
* are plain C over <ctype.h>/<string.h> operating on char buffers, and they are
|
||||||
|
* a concern of their own — so they get a translation unit of their own.
|
||||||
|
*
|
||||||
|
* Adding a new text helper? Add it HERE, not to el_runtime.c. A new .c costs
|
||||||
|
* exactly one line in lang/runtime/SOURCES, and every build path picks it up.
|
||||||
|
* Placement is a LINK-TIME concern: the compiler cannot tell which .c a symbol
|
||||||
|
* came from (builtin_arity is an arity guard, not a dispatch table), so a
|
||||||
|
* function defined here is exactly as linkable as one defined in el_runtime.c.
|
||||||
|
*/
|
||||||
|
#ifndef ENGRAM_TEXT_H
|
||||||
|
#define ENGRAM_TEXT_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Max bytes per query token, including the NUL. */
|
||||||
|
#define ENGRAM_QTOK_LEN 256
|
||||||
|
|
||||||
|
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
|
||||||
|
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
|
||||||
|
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
|
||||||
|
int engram_tokenize_query(const char* q, char toks[][ENGRAM_QTOK_LEN], int maxtok);
|
||||||
|
|
||||||
|
/* Trim leading/trailing non-alphanumerics, then accept only tokens whose core
|
||||||
|
* is alphanumeric plus '-' and '_' with at least 3 letters. This subsumes the
|
||||||
|
* quoted-title guard (2026-07-25) and the "<!--" flood (2026-08-03)
|
||||||
|
* structurally: markup and punctuation-bearing tokens never become
|
||||||
|
* candidates, rather than being blocklisted after the fact. */
|
||||||
|
int eg_st_clean_token(const char* raw, size_t rawlen, char* out, size_t outcap);
|
||||||
|
|
||||||
|
/* Word-boundary document frequency. engram_label_df uses istr_contains, i.e.
|
||||||
|
* SUBSTRING matching, and that is the wrong estimator for term specificity on
|
||||||
|
* short tokens: "them" hits inside "theme" and "anthem", "about" and "whole"
|
||||||
|
* come back with df 2 and 1 rather than 0. That matters here specifically
|
||||||
|
* because the min_df floor is what rejects English function words, and it can
|
||||||
|
* only do that job if their df is honestly zero. Substring df quietly handed
|
||||||
|
* them a survival ticket. Measured on the live store before this fix, "whole"
|
||||||
|
* (df=1, idf=8.76) and "about" (df=2, idf=8.36) were outscoring real topical
|
||||||
|
* terms and losing only on position — one node whose text happened to open
|
||||||
|
* with a function word would have seeded on it.
|
||||||
|
*
|
||||||
|
* engram_label_df keeps substring semantics: it is a separate published
|
||||||
|
* measure with existing callers, and changing it underneath them is not this
|
||||||
|
* change's business. */
|
||||||
|
int eg_st_label_has_word(const char* hay, const char* word);
|
||||||
|
|
||||||
|
/* Whether s carries the text-loss signature left by the \uXXXX -> '?' parser
|
||||||
|
* defect. Conservative by design; see engram_text.c for the full rationale. */
|
||||||
|
int eg_text_loss_signature(const char* s);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* ENGRAM_TEXT_H */
|
||||||
+49
-9
@@ -460,7 +460,8 @@ The `@` token followed by an identifier attaches a decorator to the next `FnDef`
|
|||||||
| Decorator | Structural effect |
|
| Decorator | Structural effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `@manager` | Permits calls to `dharma_emit` / `dharma_field`. Calling either from a non-`@manager` fn emits a `#error` into the generated C — a compile-time failure, not a lint. |
|
| `@manager` | Permits calls to `dharma_emit` / `dharma_field`. Calling either from a non-`@manager` fn emits a `#error` into the generated C — a compile-time failure, not a lint. |
|
||||||
| `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat(<fn name>)` at function entry. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event) with **zero** hand-written instrumentation in its body. |
|
| `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat(<fn name>, <construct>)` at function entry, where `<construct>` is the decorator that caused the beat. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event carrying `{"construct":"..."}`) with **zero** hand-written instrumentation in its body. Without the construct argument the graph accumulates boundary events with no attribution, so no construct can be measured. |
|
||||||
|
| `@decorator(kind, target)` | **Declares a construct.** The decorated `fn`'s name becomes a usable decorator whose meaning is `target`. Codegen reads the declaration; it does not know the construct. Adding a construct is a declaration in the program, not a compiler edit. Two kinds exist: `"injects_at_entry"` calls `target(<fn>, <construct>)` at entry, result discarded — this is what `@manager`/`@accessor` are, seeded as the compiled-in core. `"guards_at_entry"` calls `target(<fn>, <construct>)` at entry and a **non-zero return short-circuits the decorated fn and becomes its result**. Guards run before injections (a refused call must not report a crossing) and *every* guard on a fn runs, whereas the topmost injecting construct wins. The compiler knows nothing about authentication, rate limiting or validation: the program points the construct at its own function. |
|
||||||
| `@route(path, method, …)` | Records a route into a generated dispatch table. |
|
| `@route(path, method, …)` | Records a route into a generated dispatch table. |
|
||||||
|
|
||||||
Decorators with no registered meaning are accepted and ignored.
|
Decorators with no registered meaning are accepted and ignored.
|
||||||
@@ -697,12 +698,22 @@ Every compiled program links against:
|
|||||||
- `el_runtime.h` — declaration header
|
- `el_runtime.h` — declaration header
|
||||||
- `el_runtime.c` — implementation
|
- `el_runtime.c` — implementation
|
||||||
|
|
||||||
|
The runtime is **multi-file**: `el_runtime.c` `#include`s the six `engram_*.h`
|
||||||
|
headers and calls into all six sibling translation units, so linking it alone
|
||||||
|
fails at `ld`. The canonical link set is `<runtime-dir>/SOURCES`.
|
||||||
|
|
||||||
Compile command:
|
Compile command:
|
||||||
|
|
||||||
```
|
```
|
||||||
cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c el_runtime.c
|
cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c \
|
||||||
|
$(sed 's|^|<runtime-dir>/|' <runtime-dir>/SOURCES) \
|
||||||
|
-lcurl -lssl -lcrypto -lpthread -lm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Inside this repo, `scripts/el-runtime-sources.sh <runtime-dir>` prints that list
|
||||||
|
(it strips comments; the raw `sed` above works against an installed SDK's
|
||||||
|
`SOURCES`, which `install.sh` writes comment-free).
|
||||||
|
|
||||||
### 13.4 Output Format
|
### 13.4 Output Format
|
||||||
|
|
||||||
```c
|
```c
|
||||||
@@ -1133,6 +1144,7 @@ The `program` block is where a concern of this shape is declared once and enforc
|
|||||||
```
|
```
|
||||||
program "engram" {
|
program "engram" {
|
||||||
singleton: "engram"
|
singleton: "engram"
|
||||||
|
guards: engram_resolve_data_dir()
|
||||||
env ENGRAM_BIND: String = ":8742"
|
env ENGRAM_BIND: String = ":8742"
|
||||||
env GUIDE_PORT: Int = "8771"
|
env GUIDE_PORT: Int = "8771"
|
||||||
env ENGRAM_API_KEY: String required
|
env ENGRAM_API_KEY: String required
|
||||||
@@ -1145,24 +1157,50 @@ Grammar:
|
|||||||
|
|
||||||
```ebnf
|
```ebnf
|
||||||
program_block = "program" string "{" { program_field } "}" ;
|
program_block = "program" string "{" { program_field } "}" ;
|
||||||
program_field = singleton_field | env_field ;
|
program_field = singleton_field | guards_field | env_field ;
|
||||||
singleton_field = "singleton" ":" string [ "," ] ;
|
singleton_field = "singleton" ":" string [ "," ] ;
|
||||||
|
guards_field = "guards" ":" expr [ "," ] ;
|
||||||
env_field = "env" ident ":" type
|
env_field = "env" ident ":" type
|
||||||
[ "=" string ] [ "required" ] [ "," ] ;
|
[ "=" string ] [ "required" ] [ "," ] ;
|
||||||
```
|
```
|
||||||
|
|
||||||
`singleton` and `env` are **not** reserved words. They are read as identifier token values by the block's own parse loop, so they remain usable as ordinary identifiers everywhere else. `program` is the only keyword this section adds.
|
`singleton`, `guards` and `env` are **not** reserved words. They are read as identifier token values by the block's own parse loop, so they remain usable as ordinary identifiers everywhere else. `program` is the only keyword this section adds.
|
||||||
|
|
||||||
### 18.2 Process identity — `singleton`
|
### 18.2 Process identity — `singleton` and `guards`
|
||||||
|
|
||||||
`singleton: "id"` compiles to an `el_singleton_acquire("id")` call injected as the **first statement of `main()`**, before any user statement runs.
|
`singleton: "id"` with `guards: <expr>` compiles to `el_singleton_acquire("id", <expr>)`, injected as the **first statement of `main()`**, before any user statement runs. `<expr>` evaluates to the path of the **state** the singleton protects.
|
||||||
|
|
||||||
The runtime takes an exclusive non-blocking `flock` on `<dir>/el-singleton-<id>.lock`, where `<dir>` is `$EL_SINGLETON_DIR`, else `$TMPDIR`, else `/tmp`. On success it writes its pid and holds the descriptor open for the life of the process. On contention it **refuses to start**: it reports the holder's pid, names the lock file, and exits 1.
|
**`guards:` is mandatory.** A `singleton:` without one is a compile error. This is not defensive strictness; it is the correction of a defect measured in this tree on 2026-08-16, and the rule the rest of this section exists to state:
|
||||||
|
|
||||||
Two properties are deliberate:
|
> **Guard the thing, not the name.** A lock that protects state must be keyed on the state.
|
||||||
|
|
||||||
- **It is a lock, not a pidfile.** The kernel releases an `flock` when the owning process dies — including on `SIGKILL` and on crash. There is therefore no stale-lock state, and so no "delete the lock file to get unstuck" recovery ritual. Such a ritual would itself be a convention, which is the thing this section exists to remove.
|
Until that date the lock was `<dir>/el-singleton-<id>.lock` where `<dir>` was `$EL_SINGLETON_DIR`, else `$TMPDIR`, else `/tmp`. It was keyed on the program's **name** and on a temp directory, and it never consulted the state it claimed to protect — while its own refusal message read *"Refusing to start a second instance against the same state."* Measured, it failed in **both** directions:
|
||||||
|
|
||||||
|
| Situation | Correct answer | Name-keyed lock gave |
|
||||||
|
|---|---|---|
|
||||||
|
| same data dir, same `$TMPDIR` | refuse | refuse ✅ |
|
||||||
|
| same data dir, different `$TMPDIR` | refuse | **started** ❌ — the two-writer data-loss condition, defeated by one environment variable |
|
||||||
|
| different data dirs, same `$TMPDIR` | both start | **refused**, naming an unrelated pid ❌ |
|
||||||
|
| same dir spelled differently, different `$TMPDIR` | refuse | **started** ❌ |
|
||||||
|
|
||||||
|
Both failure directions are one error: the identity of a resource had been replaced by a label for it. The false negative is the dangerous one — a guard whose bypass is `TMPDIR=/tmp/other` is not a guard.
|
||||||
|
|
||||||
|
**The mechanism.** The lock file lives **inside the guarded directory**: `<state>/.el-singleton-<id>.lock`. The runtime takes an exclusive non-blocking `flock` on it, writes its pid, and holds the descriptor open for the life of the process.
|
||||||
|
|
||||||
|
That single placement decision is the whole fix, and it is why there is no hashing, no canonical-path registry, and no environment variable left to subvert:
|
||||||
|
|
||||||
|
- **Same directory** ⇒ same file ⇒ same inode ⇒ the `flock` contends. `$TMPDIR` is not in the key, so there is nothing to change to get past it. `$EL_SINGLETON_DIR` no longer exists.
|
||||||
|
- **Different directories** ⇒ different files ⇒ no contention. Two stores are two stores; they were never in conflict, and are no longer treated as if they were.
|
||||||
|
- **Different spellings of one directory** — trailing slash, `x/../x`, a symlink — resolve to the same inode during the kernel's own path walk, so they contend without this code comparing strings. Path canonicalisation happens only to make the diagnostic name one directory in one spelling; the *decision* never depends on it.
|
||||||
|
- **An unguardable state** — the directory is missing, or read-only — is a **refusal**, not a fallback. Starting unguarded against the store the guard exists to protect is the failure being removed.
|
||||||
|
|
||||||
|
**Why `guards:` is an expression and not a string.** The runtime cannot know, generically, which environment variable holds an arbitrary program's state; and a program whose state path already has an owner must not restate it. The engram's data dir is resolved by `engram_resolve_data_dir()`, which owns both the `$ENGRAM_DATA_DIR` read and the `$HOME/.neuron/engram` fallback (§18.4). Writing `guards: engram_resolve_data_dir()` points the guard at that owner. A `guards:` that took a string would force the path's default to be written down twice, and a guard that resolved the path its own way could end up locking a directory the program never writes to — the same two-owners defect §18.4 exists to prevent.
|
||||||
|
|
||||||
|
Three properties are deliberate:
|
||||||
|
|
||||||
|
- **It is a lock, not a pidfile.** The kernel releases an `flock` when the owning process dies — including on `SIGKILL` and on crash. There is therefore no stale-lock state, and so no "delete the lock file to get unstuck" recovery ritual. Such a ritual would itself be a convention, which is the thing this section exists to remove. (A lock file left behind inside a copied data directory — `cp -Rc` and friends — is inert: it carries no lock, only a stale pid string that the next holder overwrites.)
|
||||||
- **It reports the holder's pid.** "Already running" is not actionable. A pid is. This is the direct answer to the observed failure where a stale process survived a `pkill` and went on answering probes.
|
- **It reports the holder's pid.** "Already running" is not actionable. A pid is. This is the direct answer to the observed failure where a stale process survived a `pkill` and went on answering probes.
|
||||||
|
- **The message is true.** It names the state it checked and the lock it failed to take, and it says "the same state" only because the lock it contended for is *in* that state. A diagnostic that asserts a check that did not happen is worse than no diagnostic: it is what let the name-keyed version read as correct for as long as it did.
|
||||||
|
|
||||||
Refusal is loud and total. It is not a warning, and the program does not continue degraded. This matters more than it looks: today a second engram whose `bind()` fails merely *returns* from `http_serve` — after it has already replayed the WAL and written boot-time backup files — and then exits **0**, indistinguishable from a clean run. `singleton` refuses before the first side effect.
|
Refusal is loud and total. It is not a warning, and the program does not continue degraded. This matters more than it looks: today a second engram whose `bind()` fails merely *returns* from `http_serve` — after it has already replayed the WAL and written boot-time backup files — and then exits **0**, indistinguishable from a clean run. `singleton` refuses before the first side effect.
|
||||||
|
|
||||||
@@ -1184,6 +1222,8 @@ Some values look like configuration and are not. `ENGRAM_DATA_DIR` already has a
|
|||||||
|
|
||||||
The rule: **a variable belongs in the program block when the block would be its only owner.** If a resolver already owns it, leave it there.
|
The rule: **a variable belongs in the program block when the block would be its only owner.** If a resolver already owns it, leave it there.
|
||||||
|
|
||||||
|
This is also why `guards:` (§18.2) takes an expression: it lets the block *reference* the existing owner — `guards: engram_resolve_data_dir()` — rather than become a second one.
|
||||||
|
|
||||||
`HOME` is likewise not configuration. It is an environment fact, and stays a raw `env()` read.
|
`HOME` is likewise not configuration. It is an environment fact, and stays a raw `env()` read.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# annotation_query.sh — a declared type must match what it annotates.
|
||||||
|
#
|
||||||
|
# El had annotations and no checking: the annotation fed dispatch and was never
|
||||||
|
# verified against the value, so a mismatch did not fail, it REINTERPRETED
|
||||||
|
# MEMORY. let x: Int = "hello" printed 4343631981 (a string pointer used as an
|
||||||
|
# integer); let s: String = 42 dereferenced address 42.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: annotation_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
cd "$LANG_DIR"
|
||||||
|
|
||||||
|
printf 'fn main() { let x: Int = "hello" println("x") }\n' > "$W/a.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/a.el" >/dev/null 2>&1
|
||||||
|
out=$(./tools/check/annotations.sh "$W/r.txt" 2>&1); rc=$?
|
||||||
|
chk "Int annotated on a String literal is caught" "1" "$rc"
|
||||||
|
chk "and names the variable" "1" "$(echo "$out" | grep -c "'x' is declared Int")"
|
||||||
|
|
||||||
|
printf 'fn main() { let s: String = 42 println(s) }\n' > "$W/b.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/b.el" >/dev/null 2>&1
|
||||||
|
./tools/check/annotations.sh "$W/r2.txt" >/dev/null 2>&1
|
||||||
|
chk "String annotated on an Int literal is caught" "1" "$?"
|
||||||
|
|
||||||
|
printf 'fn main() { let n: Int = 42 let s: String = "ok" println(s + int_to_str(n)) }\n' > "$W/c.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r3.txt" "$ELC" "$W/c.el" >/dev/null 2>&1
|
||||||
|
./tools/check/annotations.sh "$W/r3.txt" >/dev/null 2>&1
|
||||||
|
chk "correct annotations are clean" "0" "$?"
|
||||||
|
|
||||||
|
EL_RELATIONS_OUT="$W/r4.txt" "$ELC" elc-cli.el >/dev/null 2>&1
|
||||||
|
./tools/check/annotations.sh "$W/r4.txt" >/dev/null 2>&1
|
||||||
|
chk "the compiler's own source is clean — no false positives" "0" "$?"
|
||||||
|
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for arity-from-header: the runtime declares its own surface, so the
|
||||||
|
# compiler does not carry a second copy of it.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: arity_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
|
||||||
|
printf 'fn main() {\n println("a", "b")\n}\n' > "$W/bad.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r1.txt" "$ELC" "$W/bad.el" >/dev/null 2>&1
|
||||||
|
chk "the emitter does not adjudicate arity" "0" "$("$ELC" "$W/bad.el" 2>/dev/null | grep -c 'arity error')"
|
||||||
|
out=$("$LANG_DIR/tools/check/arity.sh" "$W/r1.txt" 2>&1); rc=$?
|
||||||
|
chk "a wrong-arity call is caught" "1" "$rc"
|
||||||
|
chk "the expected count is correct" "1" "$(echo "$out" | grep -c "takes 1 arguments, called with 2")"
|
||||||
|
|
||||||
|
printf 'fn main() {\n println("a")\n}\n' > "$W/ok.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/ok.el" >/dev/null 2>&1
|
||||||
|
"$LANG_DIR/tools/check/arity.sh" "$W/r2.txt" >/dev/null 2>&1
|
||||||
|
chk "a correct call is clean" "0" "$?"
|
||||||
|
|
||||||
|
# multi-line declarations must not parse as zero params
|
||||||
|
n=$("$LANG_DIR/tools/check/arity.sh" "$W/r2.txt" | grep -oE '[0-9]+ signatures')
|
||||||
|
chk "signatures parsed from the header" "503 signatures" "$n"
|
||||||
|
|
||||||
|
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# async_future.sh — REPLICATION of cycle 18.
|
||||||
|
#
|
||||||
|
# STATUS: replication, not a blind test. The outcomes were already observed on
|
||||||
|
# 2026-08-17 before this harness existed, so the expectations below are not
|
||||||
|
# predictions committed in advance. Its evidentiary value is that the artifact
|
||||||
|
# lives in the repository and a third party can run it — not that it was called
|
||||||
|
# ahead of time. The original run's artifact was written in /tmp and lost when
|
||||||
|
# the worktrees were removed, which broke the chain; this replaces the claim
|
||||||
|
# with something reproducible rather than reconstructing the missing file.
|
||||||
|
#
|
||||||
|
# CLAIM UNDER TEST: @async requires no compiler change. A future is one more
|
||||||
|
# magic-tagged heap object, and el_seam_wrap lets a construct bound AFTER the
|
||||||
|
# build decide whether and when to invoke the body.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: async_future.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/..}"
|
||||||
|
LANG_DIR="$(cd "$LANG_DIR" && pwd)"
|
||||||
|
FIX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/fixtures/future.c"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
cd "$LANG_DIR"
|
||||||
|
SRCS=$(../scripts/el-runtime-sources.sh runtime)
|
||||||
|
CF="-std=c11 -O2 -rdynamic -I runtime"; LF=""
|
||||||
|
for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do
|
||||||
|
[ -d "$d" ] && CF="$CF -I $d/include" && LF="-L $d/lib"
|
||||||
|
done
|
||||||
|
LF="$LF -lcurl -lssl -lcrypto -lpthread -lm"
|
||||||
|
|
||||||
|
cat > "$W/p.el" <<'EOF'
|
||||||
|
extern fn el_await(h: Int) -> Int
|
||||||
|
|
||||||
|
fn work(k: Int) -> Int {
|
||||||
|
return k * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let h: Int = work(21)
|
||||||
|
println("CALLER_CONTINUED")
|
||||||
|
let r: Int = el_await(h)
|
||||||
|
println("RESULT " + int_to_str(r))
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
"$ELC" "$W/p.el" > "$W/p.c" 2>/dev/null
|
||||||
|
cc $CF -o "$W/p" "$W/p.c" "$FIX" $SRCS $LF 2>/dev/null || { echo " FAIL probe did not build"; exit 1; }
|
||||||
|
|
||||||
|
out=$(cd "$W" && ./p 2>&1); rc=$?
|
||||||
|
chk "unbound: no construct, synchronous, correct result" "0" "$rc"
|
||||||
|
chk "unbound: el_await on a non-future passes through, no crash" "1" "$(echo "$out" | grep -c '^RESULT 42$')"
|
||||||
|
|
||||||
|
printf 'work async wrap defer\n' > "$W/c.txt"
|
||||||
|
out=$(cd "$W" && EL_CONSTRUCTS=c.txt ./p 2>&1); rc=$?
|
||||||
|
chk "bound: does not crash" "0" "$rc"
|
||||||
|
chk "bound: the awaited result is correct" "1" "$(echo "$out" | grep -c '^RESULT 42$')"
|
||||||
|
|
||||||
|
wrap=$(echo "$out" | awk '/^WRAP_RETURNED/{print $2}')
|
||||||
|
bend=$(echo "$out" | awk '/^BODY_END/{print $2}')
|
||||||
|
caller_before_body_end=$(echo "$out" | awk '/CALLER_CONTINUED/{c=NR} /^BODY_END/{b=NR} END{print (c<b)?1:0}')
|
||||||
|
chk "bound: the caller continues BEFORE the body finishes" "1" "$caller_before_body_end"
|
||||||
|
chk "bound: the wrap returns in under 10ms while the body takes 50ms" "1" "$([ "${wrap:-999999}" -lt 10000 ] && [ "${bend:-0}" -gt 40000 ] && echo 1 || echo 0)"
|
||||||
|
|
||||||
|
echo; echo " 6 assertions, $((6-F)) passed, $F failed"; exit $F
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for capability-as-policy: the compiler records the program's kind and
|
||||||
|
# its call graph; the shipped policy file and the checker decide.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: capability_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
|
||||||
|
cat > "$W/u.el" <<'EOF'
|
||||||
|
fn leaky() -> Int {
|
||||||
|
dharma_emit("x", "y")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fn main() { println("ok") }
|
||||||
|
EOF
|
||||||
|
EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/u.el" >/dev/null 2>&1
|
||||||
|
chk "the emitter does not adjudicate" "0" "$("$ELC" "$W/u.el" 2>/dev/null | grep -c 'capability violation')"
|
||||||
|
"$LANG_DIR/tools/check/capabilities.sh" "$W/r.txt" > "$W/o.txt" 2>&1; rc=$?
|
||||||
|
chk "a utility calling a DHARMA primitive is caught" "1" "$rc"
|
||||||
|
chk "the offending fn is named" "1" "$(grep -c 'called from leaky' "$W/o.txt")"
|
||||||
|
|
||||||
|
cat > "$W/c.el" <<'EOF'
|
||||||
|
fn quiet() -> Int { return 1 }
|
||||||
|
fn main() { println("ok") }
|
||||||
|
EOF
|
||||||
|
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/c.el" >/dev/null 2>&1
|
||||||
|
"$LANG_DIR/tools/check/capabilities.sh" "$W/r2.txt" >/dev/null 2>&1
|
||||||
|
chk "a clean program exits 0" "0" "$?"
|
||||||
|
|
||||||
|
# the policy is DATA: editing it changes enforcement, with no compiler rebuild
|
||||||
|
printf 'utility prohibits_within println\n' > "$W/policy.rel"
|
||||||
|
"$LANG_DIR/tools/check/capabilities.sh" "$W/r2.txt" "$W/policy.rel" >/dev/null 2>&1
|
||||||
|
chk "editing the policy file changes enforcement, no rebuild" "1" "$?"
|
||||||
|
|
||||||
|
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for duplicate-definition detection.
|
||||||
|
#
|
||||||
|
# El has no namespacing: import is textual inlining, so two modules defining the
|
||||||
|
# same name emit two C functions into one translation unit.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: definitions_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
|
||||||
|
printf 'fn helper() -> Int { return 1 }\n' > "$W/a.el"
|
||||||
|
printf 'fn helper() -> Int { return 2 }\n' > "$W/b.el"
|
||||||
|
printf 'import "a.el"\nimport "b.el"\nfn main() { println(int_to_str(helper())) }\n' > "$W/m.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/m.el" >/dev/null 2>&1
|
||||||
|
out=$("$LANG_DIR/tools/check/definitions.sh" "$W/r.txt" 2>&1); rc=$?
|
||||||
|
chk "a collision across modules is caught at El level" "1" "$rc"
|
||||||
|
chk "the colliding name is reported" "1" "$(echo "$out" | grep -c "'helper' is defined 2 times")"
|
||||||
|
chk "and the reason is given" "1" "$(echo "$out" | grep -c 'no namespacing')"
|
||||||
|
chk "both source FILES are named" "1" "$(echo "$out" | grep -c 'a.el:1')"
|
||||||
|
chk "with file-local line numbers, not combined ones" "1" "$(echo "$out" | grep -c 'b.el:1')"
|
||||||
|
|
||||||
|
printf 'fn only_once() -> Int { return 1 }\nfn main() { println(int_to_str(only_once())) }\n' > "$W/c.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/c.el" >/dev/null 2>&1
|
||||||
|
"$LANG_DIR/tools/check/definitions.sh" "$W/r2.txt" >/dev/null 2>&1
|
||||||
|
chk "a clean program exits 0" "0" "$?"
|
||||||
|
echo; echo " 6 assertions, $((6-F)) passed, $F failed"; exit $F
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* future.c — a FUTURE as one more magic-tagged heap object.
|
||||||
|
*
|
||||||
|
* Fixture for tests/integration/async_future.sh. Linked into the probe but
|
||||||
|
* never referenced from El source: everything here is reached only by binding
|
||||||
|
* a construct AFTER the binary exists.
|
||||||
|
*
|
||||||
|
* The claim under test: @async needs no compiler change. el_val_t already
|
||||||
|
* carries List, Map, Geometry, Manifold and Bin as magic-tagged heap pointers;
|
||||||
|
* a future is one more, and el_seam_wrap hands the target the body so it can
|
||||||
|
* decide whether and when to invoke it.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
typedef int64_t el_val_t;
|
||||||
|
|
||||||
|
#define EL_MAGIC_FUT 0xE1F07000u
|
||||||
|
typedef struct { uint32_t magic; pthread_t th; el_val_t result; int done;
|
||||||
|
el_val_t (*body)(void*); void* env; } ElFuture;
|
||||||
|
|
||||||
|
static long t0_us;
|
||||||
|
static long now_us(void){ struct timespec ts; clock_gettime(CLOCK_MONOTONIC,&ts);
|
||||||
|
return ts.tv_sec*1000000L + ts.tv_nsec/1000; }
|
||||||
|
|
||||||
|
static void* fut_runner(void* v){
|
||||||
|
ElFuture* f = (ElFuture*)v;
|
||||||
|
printf("BODY_START %ld\n", now_us()-t0_us);
|
||||||
|
usleep(50000); /* 50ms, so interleaving is visible */
|
||||||
|
f->result = f->body(f->env);
|
||||||
|
f->done = 1;
|
||||||
|
printf("BODY_END %ld\n", now_us()-t0_us);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wraps_body target: returns the HANDLE immediately, never the result */
|
||||||
|
el_val_t defer(el_val_t fn, el_val_t con, el_val_t (*b)(void*), void* e){
|
||||||
|
(void)fn; (void)con;
|
||||||
|
t0_us = now_us();
|
||||||
|
ElFuture* f = calloc(1,sizeof(ElFuture));
|
||||||
|
f->magic = EL_MAGIC_FUT; f->body = b; f->env = e;
|
||||||
|
pthread_create(&f->th, NULL, fut_runner, f);
|
||||||
|
printf("WRAP_RETURNED %ld\n", now_us()-t0_us);
|
||||||
|
return (el_val_t)(intptr_t)f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el_await — block on the handle and yield the real result.
|
||||||
|
*
|
||||||
|
* NEVER dereference to decide whether a slot is a pointer. el_val_t carries
|
||||||
|
* integers too, so reading ->magic off an integer dereferences that integer AS
|
||||||
|
* AN ADDRESS. The first version of this function did exactly that and
|
||||||
|
* SIGSEGV'd on the unbound path -- sixty seconds after the same defect was
|
||||||
|
* diagnosed elsewhere in the runtime. Check the floor and alignment first. */
|
||||||
|
el_val_t el_await(el_val_t h){
|
||||||
|
if (h < 0x10000) return h; /* small ints / low addresses */
|
||||||
|
if (h & 0x7) return h; /* malloc returns 8-aligned */
|
||||||
|
ElFuture* f = (ElFuture*)(intptr_t)h;
|
||||||
|
if (f->magic != EL_MAGIC_FUT) return h; /* safe to read now */
|
||||||
|
pthread_join(f->th, NULL);
|
||||||
|
el_val_t r = f->result;
|
||||||
|
free(f);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for prohibition-as-query: the compiler records, the checker decides.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: prohibition_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
|
||||||
|
cat > "$W/p.el" <<'EOF'
|
||||||
|
@decorator("prohibits_outside", "raw_sql")
|
||||||
|
fn repository() {}
|
||||||
|
|
||||||
|
fn sneaky() -> Int { raw_sql("DROP") return 1 }
|
||||||
|
|
||||||
|
@repository
|
||||||
|
fn allowed() -> Int { raw_sql("SELECT") return 2 }
|
||||||
|
|
||||||
|
fn main() { println("ok") }
|
||||||
|
EOF
|
||||||
|
EL_RELATIONS_OUT="$W/rel.txt" "$ELC" "$W/p.el" >/dev/null 2>&1
|
||||||
|
"$LANG_DIR/tools/check/prohibitions.sh" "$W/rel.txt" > "$W/out.txt" 2>&1; rc=$?
|
||||||
|
chk "a violation outside the boundary is caught" "1" "$rc"
|
||||||
|
chk "the offending fn is named" "1" "$(grep -c 'sneaky is not one' "$W/out.txt")"
|
||||||
|
chk "a call inside the boundary is NOT flagged" "0" "$(grep -c 'allowed is not one' "$W/out.txt")"
|
||||||
|
|
||||||
|
cat > "$W/q.el" <<'EOF'
|
||||||
|
@decorator("prohibits_outside", "raw_sql")
|
||||||
|
fn repository() {}
|
||||||
|
|
||||||
|
@repository
|
||||||
|
fn only_allowed() -> Int { raw_sql("SELECT") return 1 }
|
||||||
|
|
||||||
|
fn main() { println("ok") }
|
||||||
|
EOF
|
||||||
|
EL_RELATIONS_OUT="$W/rel2.txt" "$ELC" "$W/q.el" >/dev/null 2>&1
|
||||||
|
"$LANG_DIR/tools/check/prohibitions.sh" "$W/rel2.txt" >/dev/null 2>&1
|
||||||
|
chk "a clean program exits 0" "0" "$?"
|
||||||
|
echo; echo " 4 assertions, $((4-F)) passed, $F failed"; exit $F
|
||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# seam_binding.sh — integration control for the runtime construct seam.
|
||||||
|
#
|
||||||
|
# The seam's whole claim is that a construct declared AFTER a binary exists
|
||||||
|
# applies to that already-built program. That cannot be checked by
|
||||||
|
# compile_capture, which only sees emitted text: it needs a built binary, a
|
||||||
|
# linked target, and an environment. Hence a harness rather than a unit test.
|
||||||
|
#
|
||||||
|
# usage: seam_binding.sh <elc-binary> [lang-dir]
|
||||||
|
# exit 0 = all assertions held; non-zero = number of failures
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: seam_binding.sh <elc-binary> [lang-dir]}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||||
|
FAILS=0
|
||||||
|
|
||||||
|
ok() { printf ' ok %s\n' "$1"; }
|
||||||
|
fail() { printf ' FAIL %s\n expected: %s\n actual: %s\n' "$1" "$2" "$3"; FAILS=$((FAILS+1)); }
|
||||||
|
check(){ [ "$2" = "$3" ] && ok "$1" || fail "$1" "$2" "$3"; }
|
||||||
|
|
||||||
|
SRCS=$("$LANG_DIR/../scripts/el-runtime-sources.sh" "$LANG_DIR/runtime")
|
||||||
|
CFLAGS="-std=c11 -O2 -rdynamic -I $LANG_DIR/runtime"
|
||||||
|
for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do
|
||||||
|
[ -d "$d" ] && CFLAGS="$CFLAGS -I $d/include" && LDFLAGS="-L $d/lib"
|
||||||
|
done
|
||||||
|
LDFLAGS="${LDFLAGS:-} -lcurl -lssl -lcrypto -lpthread -lm"
|
||||||
|
|
||||||
|
# A construct target that is LINKED but never referenced from El source.
|
||||||
|
cat > "$WORK/targets.c" <<'EOF'
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef int64_t el_val_t;
|
||||||
|
el_val_t observe(el_val_t fn, el_val_t con, el_val_t r){
|
||||||
|
printf("SEEN %s/%s\n", (const char*)(intptr_t)fn, (const char*)(intptr_t)con);
|
||||||
|
return r; /* zero = do not refuse */
|
||||||
|
}
|
||||||
|
el_val_t double_result(el_val_t fn, el_val_t con, el_val_t r){
|
||||||
|
(void)fn; (void)con; return r * 2; /* exit: replace the result */
|
||||||
|
}
|
||||||
|
el_val_t refuse(el_val_t fn, el_val_t con, el_val_t r){
|
||||||
|
(void)fn; (void)con; (void)r; return 42; /* non-zero = short-circuit */
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# A program with NO construct anywhere in its source.
|
||||||
|
cat > "$WORK/prog.el" <<'EOF'
|
||||||
|
fn work() -> Int {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println(int_to_str(work()))
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
"$ELC" "$WORK/prog.el" > "$WORK/prog.c" 2>/dev/null
|
||||||
|
cc $CFLAGS -o "$WORK/prog" "$WORK/prog.c" "$WORK/targets.c" $SRCS $LDFLAGS 2>/dev/null \
|
||||||
|
|| { echo " FAIL probe did not build"; exit 1; }
|
||||||
|
|
||||||
|
check "unbound program is unaffected" \
|
||||||
|
"7" "$(cd "$WORK" && ./prog 2>&1)"
|
||||||
|
|
||||||
|
printf 'work audited entry observe\n' > "$WORK/observe.txt"
|
||||||
|
check "a construct declared AFTER the build applies" \
|
||||||
|
"SEEN work/audited
|
||||||
|
7" "$(cd "$WORK" && EL_CONSTRUCTS=observe.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
printf 'work denied entry refuse\n' > "$WORK/refuse.txt"
|
||||||
|
check "a construct declared after the build can REFUSE" \
|
||||||
|
"42" "$(cd "$WORK" && EL_CONSTRUCTS=refuse.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
printf 'work ghost entry no_such_symbol_anywhere\n' > "$WORK/ghost.txt"
|
||||||
|
check "an unlinked target is skipped, not fatal" \
|
||||||
|
"7" "$(cd "$WORK" && EL_CONSTRUCTS=ghost.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
printf 'other_fn x entry refuse\n' > "$WORK/other.txt"
|
||||||
|
check "a binding for a different fn does not fire" \
|
||||||
|
"7" "$(cd "$WORK" && EL_CONSTRUCTS=other.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
printf 'work a entry observe\nwork b entry observe\n' > "$WORK/two.txt"
|
||||||
|
check "two constructs compose on one crossing" \
|
||||||
|
"SEEN work/a
|
||||||
|
SEEN work/b
|
||||||
|
7" "$(cd "$WORK" && EL_CONSTRUCTS=two.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
cat >> "$WORK/targets.c" <<'TGT'
|
||||||
|
el_val_t thrice(el_val_t fn, el_val_t con, el_val_t (*b)(void*), void* e){
|
||||||
|
(void)fn; (void)con; return b(e) + b(e) + b(e); /* wrap: invoke N times */
|
||||||
|
}
|
||||||
|
TGT
|
||||||
|
printf 'work doubler exit double_result\n' > "$WORK/exit.txt"
|
||||||
|
check "an EXIT construct declared after the build replaces the result" \
|
||||||
|
"14" "$(cd "$WORK" && EL_CONSTRUCTS=exit.txt ./prog 2>&1)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " 7 assertions, $((7-FAILS)) passed, $FAILS failed"
|
||||||
|
exit $FAILS
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# tagged_gate.sh — a slot must be validated before it is dereferenced.
|
||||||
|
#
|
||||||
|
# el_val_t carries both integers and tagged heap pointers, so "is this a
|
||||||
|
# pointer" is undecidable without checking first. That check was a CONVENTION
|
||||||
|
# every author had to know rather than a GATE they had to pass through:
|
||||||
|
# geom_of, mfld_of call looks_like_heap_obj correct
|
||||||
|
# el_bin_lookup checked only a 4096 floor read 8 bytes backward
|
||||||
|
# el_input_len checked only for NULL strlen'd an integer
|
||||||
|
# sha256_hex(50000) therefore compiled clean and segfaulted (exit 139).
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: tagged_gate.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
cd "$LANG_DIR"
|
||||||
|
SRCS=$(../scripts/el-runtime-sources.sh runtime)
|
||||||
|
CF="-std=c11 -O2 -I runtime"; for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do [ -d "$d" ] && CF="$CF -I $d/include" && LF="-L $d/lib"; done
|
||||||
|
LF="${LF:-} -lcurl -lssl -lcrypto -lpthread -lm"
|
||||||
|
|
||||||
|
build(){ "$ELC" "$1" > "$W/t.c" 2>/dev/null && cc $CF -o "$W/t" "$W/t.c" $SRCS $LF 2>/dev/null; }
|
||||||
|
|
||||||
|
printf 'fn main() { let h: String = sha256_hex(50000) println("got " + h) }\n' > "$W/a.el"
|
||||||
|
build "$W/a.el"; "$W/t" >"$W/o" 2>&1; chk "an integer where a string is expected does not crash" "0" "$?"
|
||||||
|
chk "and yields the empty-string hash, not memory" "1" "$(grep -c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 "$W/o")"
|
||||||
|
|
||||||
|
printf 'fn main() { let h: String = sha256_hex(-5) println("got " + h) }\n' > "$W/b.el"
|
||||||
|
build "$W/b.el"; "$W/t" >/dev/null 2>&1; chk "a NEGATIVE integer does not crash" "0" "$?"
|
||||||
|
|
||||||
|
printf 'fn main() { println(sha256_hex("abc")) }\n' > "$W/c.el"
|
||||||
|
build "$W/c.el"; out=$("$W/t" 2>&1)
|
||||||
|
chk "a legitimate string still hashes correctly" "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "$out"
|
||||||
|
|
||||||
|
chk "the gate is exported, so siblings stop re-deriving it" "1" "$(grep -c 'int *el_tagged(el_val_t' runtime/el_runtime.h)"
|
||||||
|
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for temporal adjudication as a query.
|
||||||
|
#
|
||||||
|
# The emitter records which illegal combination it saw and still emits a
|
||||||
|
# TIME_TYPE_ERROR placeholder -- it has to emit SOMETHING for an illegal
|
||||||
|
# expression. What moved out is the judgment and the wording.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: temporal_query.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
cd "$LANG_DIR"
|
||||||
|
|
||||||
|
printf 'fn main() {\n let a: Instant = now()\n let b: Instant = now()\n let c: Instant = a + b\n println("x")\n}\n' > "$W/b.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/b.el" >/dev/null 2>&1
|
||||||
|
chk "the illegal combination is recorded, not judged, by the emitter" \
|
||||||
|
"1" "$(grep -c 'temporal:instant_plus_instant' "$W/r.txt")"
|
||||||
|
chk "the emitter no longer authors the message" \
|
||||||
|
"0" "$("$ELC" "$W/b.el" 2>/dev/null | grep -c 'is not allowed')"
|
||||||
|
chk "a placeholder is still emitted for the illegal expression" \
|
||||||
|
"1" "$("$ELC" "$W/b.el" 2>/dev/null | grep -c TIME_TYPE_ERROR)"
|
||||||
|
out=$("./tools/check/temporal.sh" "$W/r.txt" 2>&1); rc=$?
|
||||||
|
chk "the query judges it" "1" "$rc"
|
||||||
|
chk "and explains why, from data" "1" "$(echo "$out" | grep -c 'a point plus a point is not a point')"
|
||||||
|
|
||||||
|
printf 'fn main() {\n let a: Instant = now()\n let d: Duration = el_duration_from_nanos(1)\n let c: Instant = a + d\n println("x")\n}\n' > "$W/g.el"
|
||||||
|
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/g.el" >/dev/null 2>&1
|
||||||
|
"./tools/check/temporal.sh" "$W/r2.txt" >/dev/null 2>&1
|
||||||
|
chk "a legal program exits 0" "0" "$?"
|
||||||
|
echo; echo " 6 assertions, $((6-F)) passed, $F failed"; exit $F
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Control for temporal signatures as data.
|
||||||
|
#
|
||||||
|
# Unlike the other checks this one is read BY the compiler, not after it: the
|
||||||
|
# El-level return type decides which runtime wrapper to emit, and that is
|
||||||
|
# dispatch, not adjudication. What moved out is the data.
|
||||||
|
set -uo pipefail
|
||||||
|
ELC="${1:?usage: temporal_signatures.sh <elc>}"
|
||||||
|
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||||
|
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||||
|
cd "$LANG_DIR"
|
||||||
|
|
||||||
|
printf 'fn main() {\n let a = now()\n let b = el_duration_from_nanos(5)\n let c = a + b\n println("ok")\n}\n' > "$W/i.el"
|
||||||
|
chk "an inferred Instant + Duration dispatches to the typed wrapper" \
|
||||||
|
"1" "$("$ELC" "$W/i.el" 2>/dev/null | grep -c el_instant_add_dur)"
|
||||||
|
chk "with no signature file, the type is unknown and it does not" \
|
||||||
|
"0" "$(EL_SIGNATURES=/nonexistent "$ELC" "$W/i.el" 2>/dev/null | grep -c el_instant_add_dur)"
|
||||||
|
|
||||||
|
printf 'fn main() {\n let a: Instant = now()\n let b: Instant = now()\n let c: Instant = a + b\n println("x")\n}\n' > "$W/b.el"
|
||||||
|
chk "Instant + Instant is still refused" \
|
||||||
|
"1" "$("$ELC" "$W/b.el" 2>/dev/null | grep -c 'TIME_TYPE_ERROR: Instant + Instant')"
|
||||||
|
|
||||||
|
printf 'fn main() {\n let a: Instant = now()\n let d: Duration = el_duration_from_nanos(1)\n let c: Instant = a + d\n println("x")\n}\n' > "$W/g.el"
|
||||||
|
chk "Instant + Duration is allowed" \
|
||||||
|
"0" "$("$ELC" "$W/g.el" 2>/dev/null | grep -c TIME_TYPE_ERROR)"
|
||||||
|
|
||||||
|
echo; echo " 4 assertions, $((4-F)) passed, $F failed"; exit $F
|
||||||
@@ -18,7 +18,9 @@ import "../../el-compiler/src/compiler.el"
|
|||||||
// ── Lexer helpers ─────────────────────────────────────────────────────────────
|
// ── Lexer helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn tok_count(tokens: [Any]) -> Int {
|
fn tok_count(tokens: [Any]) -> Int {
|
||||||
native_list_len(tokens) / 2
|
// A token is (kind, value, line). This helper carried its own copy of the
|
||||||
|
// stride, so it escaped a search scoped to the compiler sources.
|
||||||
|
native_list_len(tokens) / 3
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Codegen helper: capture compile() stdout to a string ─────────────────────
|
// ── Codegen helper: capture compile() stdout to a string ─────────────────────
|
||||||
@@ -259,22 +261,28 @@ test "lex-multiline-source" {
|
|||||||
assert tok_kind(tokens, 0) == "Let", "first token is Let"
|
assert tok_kind(tokens, 0) == "Let", "first token is Let"
|
||||||
}
|
}
|
||||||
|
|
||||||
test "lex-flat-stride-2-layout" {
|
test "lex-flat-stride-3-layout" {
|
||||||
// Verify that the flat stride-2 layout: token i has kind at index 2*i, value at 2*i+1
|
// A token is (kind, value, line): token i has kind at 3*i, value at 3*i+1,
|
||||||
|
// line at 3*i+2. Before 2026-08-17 a token carried no position at all, so
|
||||||
|
// no diagnostic in El could name a place.
|
||||||
let tokens: [Any] = lex("fn foo")
|
let tokens: [Any] = lex("fn foo")
|
||||||
// tokens[0] = "Fn", tokens[1] = "fn", tokens[2] = "Ident", tokens[3] = "foo", ...
|
|
||||||
let raw_len: Int = native_list_len(tokens)
|
let raw_len: Int = native_list_len(tokens)
|
||||||
assert raw_len == 6, "fn + foo + Eof = 3 tokens = 6 raw entries"
|
assert raw_len == 9, "fn + foo + Eof = 3 tokens = 9 raw entries"
|
||||||
let kind0: String = native_list_get(tokens, 0)
|
assert native_list_get(tokens, 0) == "Fn", "raw[0] is the kind"
|
||||||
let val0: String = native_list_get(tokens, 1)
|
assert native_list_get(tokens, 1) == "fn", "raw[1] is the value"
|
||||||
let kind1: String = native_list_get(tokens, 2)
|
assert native_list_get(tokens, 2) == "1", "raw[2] is the line"
|
||||||
let val1: String = native_list_get(tokens, 3)
|
assert native_list_get(tokens, 3) == "Ident", "raw[3] is the next kind"
|
||||||
assert kind0 == "Fn", "raw[0] is Fn kind"
|
assert native_list_get(tokens, 5) == "1", "still line 1"
|
||||||
assert val0 == "fn", "raw[1] is fn value"
|
|
||||||
assert kind1 == "Ident", "raw[2] is Ident kind"
|
|
||||||
assert val1 == "foo", "raw[3] is foo value"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "lexer-tracks-line-numbers" {
|
||||||
|
let tokens: [Any] = lex("fn a\nfn b\nfn c")
|
||||||
|
assert tok_line(tokens, 0) == "1", "first fn is on line 1"
|
||||||
|
assert tok_line(tokens, 2) == "2", "second fn is on line 2"
|
||||||
|
assert tok_line(tokens, 4) == "3", "third fn is on line 3"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Parser tests ──────────────────────────────────────────────────────────────
|
// ── Parser tests ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn get_first_stmt_kind(src: String) -> String {
|
fn get_first_stmt_kind(src: String) -> String {
|
||||||
@@ -726,3 +734,264 @@ test "compiler-stdint-include" {
|
|||||||
let out: String = compile_capture(src)
|
let out: String = compile_capture(src)
|
||||||
assert str_contains(out, "stdint.h"), "output includes stdint.h"
|
assert str_contains(out, "stdint.h"), "output includes stdint.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Decorator seam: boundary-beat attribution ────────────────────────────────
|
||||||
|
//
|
||||||
|
// The beat carries the CONSTRUCT that caused it, not only the fn that beat.
|
||||||
|
// Without the second argument the graph accumulates boundary events with no
|
||||||
|
// way to attribute them to the decorator responsible, so no construct can ever
|
||||||
|
// be measured and "is this decorator earning its keep" stays an argument
|
||||||
|
// instead of a query.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "decorator-undecorated-fn-has-no-beat" {
|
||||||
|
let src: String = "fn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert !str_contains(out, "engram_boundary_beat"), "an undecorated fn does not beat"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Decorator seam: the twelve inert names ───────────────────────────────────
|
||||||
|
//
|
||||||
|
// PINS A KNOWN DEFECT. codegen calls fn_has_decorator for exactly three names
|
||||||
|
// (manager, accessor, route). Twelve others parse, attach as {name,args}, and
|
||||||
|
// compile to nothing — including four that look like protection:
|
||||||
|
// @authenticate (6 uses), @authorize (3), @rate_limit (3), @validate (2).
|
||||||
|
//
|
||||||
|
// This test asserts the CURRENT behaviour so that fixing it is a visible
|
||||||
|
// change rather than a silent one. When a pass wires or rejects these, this
|
||||||
|
// test flips and that flip is the proof.
|
||||||
|
|
||||||
|
test "decorator-authenticate-compiles-to-nothing" {
|
||||||
|
let src: String = "@authenticate\nfn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
let bare: String = compile_capture("fn f() -> Int { return 1 }")
|
||||||
|
assert str_eq(out, bare), "KNOWN DEFECT: @authenticate emits identical C to no decorator at all"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Declared constructs ──────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// A construct declares its own meaning and codegen reads it. Adding a
|
||||||
|
// construct is a declaration in the program; it does not touch the compiler.
|
||||||
|
|
||||||
|
|
||||||
|
test "declared-construct-name-unknown-to-codegen" {
|
||||||
|
// The name is arbitrary. Nothing in the compiler mentions it.
|
||||||
|
let src: String = "@decorator(\"injects_at_entry\", \"engram_boundary_beat\")\nfn zzq_unlikely_name() {}\n@zzq_unlikely_name\nfn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "EL_STR(\"zzq_unlikely_name\")"), "an arbitrary construct name works"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "undeclared-construct-still-injects-nothing" {
|
||||||
|
let src: String = "@nobody_declared_this\nfn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert !str_contains(out, "engram_boundary_beat"), "an undeclared construct injects nothing"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Declared constructs: guards ──────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// A guard is an injection that may refuse. Non-zero return short-circuits the
|
||||||
|
// decorated fn. This is what @authenticate/@authorize/@rate_limit/@validate
|
||||||
|
// needed and never had — fourteen applications that read as protection and
|
||||||
|
// emitted no instruction.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "undeclared-guard-emits-nothing" {
|
||||||
|
let src: String = "@not_a_declared_guard\nfn handler() -> Int { return 7 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert !str_contains(out, "if (__g)"), "an undeclared construct guards nothing"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Declared constructs: exit injection and composition ──────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ── Declared constructs: wraps and prohibitions ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ── Runtime seam ─────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// CONTROL for the finding that a crossing can be resolved at execution rather
|
||||||
|
// than at emission. Codegen emits one unconditional indirection per fn; which
|
||||||
|
// constructs apply is read from a table written after the binary exists.
|
||||||
|
|
||||||
|
test "seam-indirection-emitted-on-every-fn" {
|
||||||
|
let src: String = "fn a() -> Int { return 1 }\nfn b() -> Int { return 2 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_seam_run(EL_STR(\"a\"), 0, 0);"), "fn a carries the indirection"
|
||||||
|
assert str_contains(out, "el_seam_run(EL_STR(\"b\"), 0, 0);"), "fn b carries the indirection"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "seam-emitted-without-any-decorator" {
|
||||||
|
// The point of the seam: source need not mention a construct at all.
|
||||||
|
let src: String = "fn undecorated() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_seam_run"), "an undecorated fn is still bindable at runtime"
|
||||||
|
assert !str_contains(out, "engram_boundary_beat"), "and nothing is inlined for it"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Runtime seam: what replaced the compile-time entry mechanism ─────────────
|
||||||
|
//
|
||||||
|
// Entry injection and refusal moved from emission to execution. These assert
|
||||||
|
// the emitted shape; the BEHAVIOUR — that a construct declared after the build
|
||||||
|
// applies, refuses, composes, and that an unlinked target is skipped — is
|
||||||
|
// covered by tests/integration/seam_binding.sh, which needs a built binary and
|
||||||
|
// an environment and therefore cannot be a compile_capture test.
|
||||||
|
|
||||||
|
test "seam-replaces-inlined-entry-injection" {
|
||||||
|
let src: String = "@manager\nfn m() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_seam_run(EL_STR(\"m\")"), "the crossing goes through the seam"
|
||||||
|
assert !str_contains(out, "engram_boundary_beat(EL_STR(\"m\")"), "nothing is inlined at the crossing any more"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "seam-entry-is-refusable" {
|
||||||
|
let src: String = "fn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "if (__s) return __s;"), "a bound construct can short-circuit the fn"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "seam-is-emitted-for-undecorated-fns" {
|
||||||
|
let src: String = "fn plain() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_seam_run(EL_STR(\"plain\")"), "any fn is bindable later, decorated or not"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Exit crossings resolve at runtime too ────────────────────────────────────
|
||||||
|
//
|
||||||
|
// The wrapper is now UNCONDITIONAL. It has to be: early returns must route
|
||||||
|
// through something for an exit construct to see them, and codegen cannot know
|
||||||
|
// which fns will be bound after the binary exists. Measured cost of always
|
||||||
|
// emitting it: 0.37s -> 0.38s across ten self-compiles.
|
||||||
|
|
||||||
|
test "every-fn-gets-a-body-helper-and-wrapper" {
|
||||||
|
let src: String = "fn plain(k: Int) -> Int { if k > 0 { return 1 } return 2 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "static el_val_t __el_body_plain"), "the body is a helper"
|
||||||
|
assert str_contains(out, "el_val_t plain(el_val_t k) {"), "the visible fn is a wrapper"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "exit-crossing-goes-through-the-seam" {
|
||||||
|
let src: String = "fn f() -> Int { return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "__r = el_seam_run(EL_STR(\"f\"), 1, __r);"), "the exit crossing is resolved at execution and may replace the result"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "early-returns-route-through-the-exit-seam" {
|
||||||
|
let src: String = "fn early(k: Int) -> Int { if k > 0 { return 99 } return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
let helper: Int = str_index_of(out, "__el_body_early")
|
||||||
|
let seam: Int = str_index_of(out, "el_seam_run(EL_STR(\"early\"), 1")
|
||||||
|
assert helper < seam, "the early return is inside the helper, so it passes through the exit seam"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Invocation control resolves at runtime ───────────────────────────────────
|
||||||
|
//
|
||||||
|
// Every fn gets an env struct and a thunk, because codegen cannot know which
|
||||||
|
// fns a wrap construct will be bound to after the binary exists. That the bound
|
||||||
|
// construct can invoke the body zero or N times is behaviour, so it lives in
|
||||||
|
// tests/integration/seam_binding.sh.
|
||||||
|
|
||||||
|
test "every-fn-gets-a-closure" {
|
||||||
|
let src: String = "fn f(k: Int) -> Int { return k }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "struct __env_f { el_val_t k; };"), "captured environment"
|
||||||
|
assert str_contains(out, "static el_val_t __thunk_f(void* __v)"), "thunk over that environment"
|
||||||
|
assert str_contains(out, "el_seam_wrap(EL_STR(\"f\"), __thunk_f, &__env)"), "invocation goes through the seam"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "zero-param-fn-emits-valid-c" {
|
||||||
|
// An empty struct is a GNU extension and an empty initialiser is C23.
|
||||||
|
let src: String = "fn noargs() -> Int { return 3 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "struct __env_noargs { char __e0; };"), "zero-param env has a field"
|
||||||
|
assert !str_contains(out, "__env = { }"), "and no empty initialiser"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Prohibition is a query, not an emission ─────────────────────────────────
|
||||||
|
//
|
||||||
|
// The compiler records what it saw -- who calls what, who carries what, who
|
||||||
|
// prohibits what. Whether that is legal is decided by tools/check/prohibitions.sh
|
||||||
|
// against the emitted relations, at build time. An emitter that also adjudicates
|
||||||
|
// has to contain every rule anyone will ever want.
|
||||||
|
|
||||||
|
test "compiler-no-longer-emits-prohibition-errors" {
|
||||||
|
let src: String = "@decorator(\"prohibits_outside\", \"raw_sql\")\nfn repository() {}\nfn sneaky() -> Int { raw_sql(\"DROP\") return 1 }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert !str_contains(out, "boundary violation"), "the emitter does not adjudicate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Int return types drive + dispatch ────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// El has one type, so `a + b` must be dispatched from what the operands ARE.
|
||||||
|
// The 35 Int-returning builtins moved to signatures.rel; the dispatch stayed,
|
||||||
|
// because choosing between arithmetic and concatenation is emission.
|
||||||
|
|
||||||
|
test "int-returning-builtin-drives-arithmetic-dispatch" {
|
||||||
|
let src: String = "fn main() { let a = str_len(\"hello\") let b = str_len(\"hi\") let c = a + b println(int_to_str(c)) }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "(a + b)"), "Int + Int is arithmetic"
|
||||||
|
assert !str_contains(out, "el_str_concat(a, b)"), "and NOT concatenation"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "string-plus-string-still-concatenates" {
|
||||||
|
let src: String = "fn main() { let s = \"a\" + \"b\" println(s) }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_str_concat"), "String + String still concatenates"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reserved words that reserved nothing ─────────────────────────────────────
|
||||||
|
//
|
||||||
|
// sealed, activate, seed, protocol and impl were keywords in the lexer and were
|
||||||
|
// consumed by no parser or codegen path. Each stole an identifier from users
|
||||||
|
// for nothing, and using one silently miscompiled: `let seed = 42` compiled
|
||||||
|
// clean and produced the wrong value with no diagnostic at any layer.
|
||||||
|
|
||||||
|
test "freed-identifiers-compile-as-identifiers" {
|
||||||
|
let src: String = "fn main() { let seed = 42 let impl = seed + 1 println(int_to_str(impl)) }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_val_t seed"), "seed is an identifier"
|
||||||
|
assert str_contains(out, "el_val_t impl"), "impl is an identifier"
|
||||||
|
assert str_contains(out, "(seed + 1)"), "and arithmetic on them dispatches correctly"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "test-keyword-is-still-reserved" {
|
||||||
|
// `test` LOOKED inert by the same measure and is not: codegen consumes it
|
||||||
|
// for --test mode, 408 uses in the tree. Measuring only parser.el would
|
||||||
|
// have removed it.
|
||||||
|
let src: String = "fn main() { println(\"x\") }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "int main"), "the suite still compiles, which requires test to remain a keyword"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── A bare literal is a magnitude with no axis ───────────────────────────────
|
||||||
|
//
|
||||||
|
// Duration + Int was already refused because an Int carries no unit. Adding one
|
||||||
|
// to a POINT is worse: it moves the instant by an unspecified amount. The
|
||||||
|
// asymmetry had no justification; it was simply never written.
|
||||||
|
|
||||||
|
test "instant-plus-bare-int-is-refused" {
|
||||||
|
let src: String = "fn main() { let t: Instant = now() let u: Instant = t + 3 println(\"x\") }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "TIME_TYPE_ERROR: Instant + Int"), "3 of what?"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "instant-plus-unit-suffix-is-allowed" {
|
||||||
|
// .hour supplies the axis, so the magnitude becomes a displacement.
|
||||||
|
let src: String = "fn main() { let t: Instant = now() let u: Instant = t + 1.hour println(\"x\") }"
|
||||||
|
let out: String = compile_capture(src)
|
||||||
|
assert str_contains(out, "el_instant_add_dur"), "a unit suffix makes it a Duration"
|
||||||
|
assert !str_contains(out, "TIME_TYPE_ERROR"), "and the addition is legal"
|
||||||
|
}
|
||||||
|
|||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# annotations.sh — verify that a declared type matches what it annotates.
|
||||||
|
#
|
||||||
|
# El had annotations and no checking. The annotation fed dispatch (deciding
|
||||||
|
# whether `a + b` is arithmetic or concatenation) and was never verified against
|
||||||
|
# the value, so a mismatch did not fail -- it reinterpreted memory:
|
||||||
|
#
|
||||||
|
# let x: Int = "hello" a string pointer used as an integer
|
||||||
|
# let s: String = 42 address 42 dereferenced as a string
|
||||||
|
#
|
||||||
|
# The second is an arbitrary-read primitive if the integer is influenced.
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: annotations.sh <relations-file>}"
|
||||||
|
[ -f "$REL" ] || exit 0
|
||||||
|
locate() {
|
||||||
|
awk -v L="$1" '$2=="spans" && $3<=L && $4>=L {printf "%s:%d", $1, L-$3+1; exit}' "$REL" 2>/dev/null
|
||||||
|
}
|
||||||
|
V=0
|
||||||
|
while read -r caller _ rest; do
|
||||||
|
[ "${rest#typemismatch:}" = "$rest" ] && continue
|
||||||
|
body="${rest#typemismatch:}"
|
||||||
|
declared="${body%%:*}"; body="${body#*:}"
|
||||||
|
actual="${body%%:*}"; var="${body#*:}"
|
||||||
|
printf "type error in %s: '%s' is declared %s but assigned a %s literal — the annotation drives dispatch, so the value will be reinterpreted rather than rejected\n" \
|
||||||
|
"$caller" "$var" "$declared" "$actual"
|
||||||
|
V=$((V+1))
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
[ "$V" -eq 0 ] && echo "annotations: clean"
|
||||||
|
exit "$V"
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# arity.sh — check call arity against the runtime's OWN declarations.
|
||||||
|
#
|
||||||
|
# codegen.el carried builtin_arity(): 344 lines, 300 entries, of which 243 were
|
||||||
|
# an exact duplicate of el_runtime.h. Measured drift between them was zero --
|
||||||
|
# the duplicate had been maintained correctly -- but 199 functions the runtime
|
||||||
|
# declares had NO entry, so calling them with the wrong argument count produced
|
||||||
|
# no El-level diagnostic at all. The table was not wrong, it was 40% incomplete.
|
||||||
|
#
|
||||||
|
# Deriving from the header fixes the coverage and makes drift impossible.
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: arity.sh <relations-file> [runtime-header]}"
|
||||||
|
HDR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/runtime/el_runtime.h}"
|
||||||
|
[ -f "$REL" ] || exit 0
|
||||||
|
[ -f "$HDR" ] || { echo "no header: $HDR" >&2; exit 0; }
|
||||||
|
|
||||||
|
SIG=$(mktemp); trap 'rm -f "$SIG"' EXIT
|
||||||
|
# Declarations may span lines, so join continuations before parsing. Reading
|
||||||
|
# only the first line silently yields 0 params, and a checker that reports the
|
||||||
|
# wrong expected count is worse than no checker at all.
|
||||||
|
sed 's://.*::' "$HDR" | tr '\n' ' ' | sed 's:/\*[^*]*\*/: :g; s/;/;\n/g' | awk '
|
||||||
|
/el_val_t[[:space:]]+[a-z0-9_]+[[:space:]]*\(/ {
|
||||||
|
line=$0
|
||||||
|
match(line, /el_val_t[[:space:]]+[a-z0-9_]+/); name=substr(line,RSTART,RLENGTH)
|
||||||
|
sub(/el_val_t[[:space:]]+/,"",name)
|
||||||
|
match(line, /\(.*\)/); params=substr(line,RSTART+1,RLENGTH-2)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/,"",params)
|
||||||
|
if (params=="void" || params=="") n=0
|
||||||
|
else { n=1; for(i=1;i<=length(params);i++) if(substr(params,i,1)==",") n++ }
|
||||||
|
if (line ~ /\.\.\./) n=-1
|
||||||
|
print name, n
|
||||||
|
}' | sort -u > "$SIG"
|
||||||
|
|
||||||
|
V=0
|
||||||
|
while read -r callee _ rest; do
|
||||||
|
[ "${rest#arity:}" = "$rest" ] && continue
|
||||||
|
actual="${rest#arity:}"
|
||||||
|
expected=$(awk -v n="$callee" '$1==n {print $2; exit}' "$SIG")
|
||||||
|
# 60 of 500 runtime decls carry a __ prefix: El's `println` is C's
|
||||||
|
# `__println`. codegen owns that mapping and its table carried BOTH keys.
|
||||||
|
# One rule covers every one of them.
|
||||||
|
[ -n "$expected" ] || expected=$(awk -v n="__$callee" '$1==n {print $2; exit}' "$SIG")
|
||||||
|
[ -n "$expected" ] || continue # not a runtime builtin
|
||||||
|
[ "$expected" = "-1" ] && continue # variadic
|
||||||
|
if [ "$actual" != "$expected" ]; then
|
||||||
|
printf "arity error: '%s' takes %s arguments, called with %s\n" "$callee" "$expected" "$actual"
|
||||||
|
V=$((V+1))
|
||||||
|
fi
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
|
||||||
|
[ "$V" -eq 0 ] && echo "arity: clean ($(wc -l < "$SIG" | tr -d ' ') signatures from the header)"
|
||||||
|
exit "$V"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# capabilities.rel — the capability policy, as shipped data.
|
||||||
|
#
|
||||||
|
# A program's tier bounds what it may call. This is policy that comes from
|
||||||
|
# OUTSIDE the program: a utility cannot be trusted to declare its own
|
||||||
|
# restrictions, because it would declare none. So unlike prohibits_outside,
|
||||||
|
# which a program declares about itself, this ships with the language and is
|
||||||
|
# editable without a compiler release.
|
||||||
|
#
|
||||||
|
# Previously: four functions and eighteen string literals inside codegen.el.
|
||||||
|
#
|
||||||
|
# <kind> prohibits_within <comma-separated names>
|
||||||
|
|
||||||
|
service prohibits_within llm_call_agentic,llm_register_tool,dharma_emit,dharma_field
|
||||||
|
|
||||||
|
utility prohibits_within dharma_connect,dharma_send,dharma_activate,dharma_emit,dharma_field,dharma_strengthen,dharma_relationship,dharma_peers
|
||||||
|
utility prohibits_within llm_call,llm_call_system,llm_call_agentic,llm_vision,llm_register_tool,llm_models
|
||||||
|
|
||||||
|
# cgi is unrestricted: self-formation is what a cgi program is for.
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# capabilities.sh — enforce the capability tier as a QUERY over emitted
|
||||||
|
# relations plus a shipped policy file. The compiler records the program's kind
|
||||||
|
# and its call graph; deciding what that tier may call is not an emitter's job.
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: capabilities.sh <relations-file> [policy]}"
|
||||||
|
POLICY="${2:-$(dirname "${BASH_SOURCE[0]}")/capabilities.rel}"
|
||||||
|
[ -f "$REL" ] || exit 0
|
||||||
|
KIND=$(grep -m1 '^program calls is_kind:' "$REL" | sed 's/.*is_kind://')
|
||||||
|
[ -n "$KIND" ] || KIND=utility
|
||||||
|
V=0
|
||||||
|
while read -r kind rel names; do
|
||||||
|
[ "$kind" = "$KIND" ] && [ "$rel" = "prohibits_within" ] || continue
|
||||||
|
IFS=',' read -ra NAMES <<< "$names"
|
||||||
|
for n in "${NAMES[@]}"; do
|
||||||
|
while read -r caller _ callee; do
|
||||||
|
[ "$callee" = "$n" ] || continue
|
||||||
|
printf "capability violation: '%s' programs may not call '%s' (called from %s)\n" "$KIND" "$n" "$caller"
|
||||||
|
V=$((V+1))
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
done
|
||||||
|
done < <(grep -v '^#' "$POLICY" | grep -v '^[[:space:]]*$')
|
||||||
|
[ "$V" -eq 0 ] && echo "capabilities: clean ($KIND)"
|
||||||
|
exit "$V"
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# definitions.sh — catch duplicate top-level definitions, and name the files.
|
||||||
|
#
|
||||||
|
# El has no namespacing. `import` is textual inlining, so two modules defining
|
||||||
|
# the same name emit two C functions into one translation unit. cc catches it,
|
||||||
|
# but reports the generated helpers (__el_body_f, __env_f, __thunk_f) before the
|
||||||
|
# user's own function, so the first three errors name symbols nobody wrote.
|
||||||
|
#
|
||||||
|
# Naming the FILES needed provenance threaded end to end: tokens had no line
|
||||||
|
# numbers at all, so no diagnostic in El could name a place. Now a token is
|
||||||
|
# (kind, value, line), FnDef carries its line, and resolve_imports publishes
|
||||||
|
# which line range of the combined source came from which file.
|
||||||
|
#
|
||||||
|
# LIMIT: a nested import returns one string, so a definition inside a
|
||||||
|
# transitively imported file is attributed to the direct import.
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: definitions.sh <relations-file>}"
|
||||||
|
[ -f "$REL" ] || exit 0
|
||||||
|
|
||||||
|
# line in the COMBINED source -> "file:line-within-that-file". Reporting the
|
||||||
|
# combined line against a filename would point at a line that file does not
|
||||||
|
# have, which is worse than reporting no line at all.
|
||||||
|
locate() {
|
||||||
|
awk -v L="$1" '$2=="spans" && $3<=L && $4>=L {printf "%s:%d", $1, L-$3+1; found=1; exit}
|
||||||
|
END{ if(!found) printf "" }' "$REL"
|
||||||
|
}
|
||||||
|
|
||||||
|
V=0
|
||||||
|
while read -r name; do
|
||||||
|
lines=$(grep -E "^$name calls defines_at:" "$REL" | sed 's/.*defines_at://' | sort -un)
|
||||||
|
n=$(echo "$lines" | wc -l | tr -d ' ')
|
||||||
|
[ "$n" -gt 1 ] || continue
|
||||||
|
printf "duplicate definition: '%s' is defined %s times — El has no namespacing, so imported modules share one global scope\n" "$name" "$n"
|
||||||
|
for l in $lines; do
|
||||||
|
loc=$(locate "$l")
|
||||||
|
[ -n "$loc" ] && printf " %s\n" "$loc" || printf " combined line %s\n" "$l"
|
||||||
|
done
|
||||||
|
V=$((V+1))
|
||||||
|
done < <(grep ' calls defines_at:' "$REL" | awk '{print $1}' | sort -u)
|
||||||
|
|
||||||
|
[ "$V" -eq 0 ] && echo "definitions: clean"
|
||||||
|
exit "$V"
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# prohibitions.sh — enforce boundary prohibitions as a QUERY over relations the
|
||||||
|
# compiler emitted, rather than as a rule the compiler contains.
|
||||||
|
#
|
||||||
|
# A prohibition is a containment relation over the call graph: "these calls may
|
||||||
|
# appear only inside a fn carrying construct C". The compiler's job is to say
|
||||||
|
# what it saw — who calls what, who carries what, who prohibits what. Deciding
|
||||||
|
# whether that is legal is a query, and a query does not belong in an emitter.
|
||||||
|
#
|
||||||
|
# Detection still happens at BUILD time. What moved is where the rule and the
|
||||||
|
# checker live, which is what "a #error has no runtime" was hiding.
|
||||||
|
#
|
||||||
|
# usage: prohibitions.sh <relations-file>
|
||||||
|
# exit 0 = clean; exit N = N violations
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: prohibitions.sh <relations-file>}"
|
||||||
|
[ -f "$REL" ] || { echo "no relations file: $REL" >&2; exit 0; }
|
||||||
|
|
||||||
|
V=0
|
||||||
|
# construct -> prohibited names
|
||||||
|
while read -r construct _ rest; do
|
||||||
|
[ "${rest#prohibits:}" = "$rest" ] && continue
|
||||||
|
names="${rest#prohibits:}"
|
||||||
|
IFS=',' read -ra NAMES <<< "$names"
|
||||||
|
for n in "${NAMES[@]}"; do
|
||||||
|
# every fn that calls a prohibited name
|
||||||
|
while read -r caller _ callee; do
|
||||||
|
[ "$callee" = "$n" ] || continue
|
||||||
|
# ...must carry the owning construct
|
||||||
|
if ! grep -qx "$caller calls @$construct" "$REL"; then
|
||||||
|
printf 'boundary violation: %s may only be called from an @%s fn, but %s is not one\n' \
|
||||||
|
"$n" "$construct" "$caller"
|
||||||
|
V=$((V+1))
|
||||||
|
fi
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
done
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
|
||||||
|
[ "$V" -eq 0 ] && echo "prohibitions: clean"
|
||||||
|
exit "$V"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# signatures.rel — El-level return types for runtime builtins.
|
||||||
|
#
|
||||||
|
# el_runtime.h declares every builtin as returning el_val_t, because El has ONE
|
||||||
|
# type. That single type is why the whole seam is cheap, and it is also why the
|
||||||
|
# header cannot say that now() returns an Instant while unix_seconds() returns
|
||||||
|
# an Int. The El-level type is real and the C boundary erases it.
|
||||||
|
#
|
||||||
|
# So the compiler needs this, and unlike the other checks it needs it at
|
||||||
|
# EMISSION time: Instant + Duration must become el_instant_add_dur, and that is
|
||||||
|
# dispatch, not adjudication. What moved here is the DATA -- previously 19
|
||||||
|
# hardcoded names across two functions in codegen.el. What stays in the emitter
|
||||||
|
# is choosing which call to emit, which is an emitter's actual job.
|
||||||
|
#
|
||||||
|
# <builtin> returns <El type>
|
||||||
|
|
||||||
|
now returns Instant
|
||||||
|
el_now_instant returns Instant
|
||||||
|
unix_seconds returns Instant
|
||||||
|
unix_millis returns Instant
|
||||||
|
instant_from_iso8601 returns Instant
|
||||||
|
el_instant_add_dur returns Instant
|
||||||
|
el_instant_sub_dur returns Instant
|
||||||
|
|
||||||
|
el_duration_from_nanos returns Duration
|
||||||
|
duration_seconds returns Duration
|
||||||
|
duration_millis returns Duration
|
||||||
|
duration_nanos returns Duration
|
||||||
|
el_instant_diff returns Duration
|
||||||
|
el_duration_add returns Duration
|
||||||
|
el_duration_sub returns Duration
|
||||||
|
el_duration_scale returns Duration
|
||||||
|
el_duration_div returns Duration
|
||||||
|
ttl_cache_age returns Duration
|
||||||
|
|
||||||
|
# Int-returning builtins. Previously 35 hardcoded names in is_int_call().
|
||||||
|
# These decide whether `a + b` is arithmetic or concatenation, so the
|
||||||
|
# compiler reads them at emission time -- dispatch, not adjudication.
|
||||||
|
|
||||||
|
str_len returns Int
|
||||||
|
str_index_of returns Int
|
||||||
|
str_to_int returns Int
|
||||||
|
str_char_code returns Int
|
||||||
|
str_count returns Int
|
||||||
|
str_count_chars returns Int
|
||||||
|
str_count_bytes returns Int
|
||||||
|
str_count_lines returns Int
|
||||||
|
str_count_words returns Int
|
||||||
|
str_count_letters returns Int
|
||||||
|
str_count_digits returns Int
|
||||||
|
str_last_index_of returns Int
|
||||||
|
str_find_chars returns Int
|
||||||
|
native_list_len returns Int
|
||||||
|
el_list_len returns Int
|
||||||
|
len returns Int
|
||||||
|
json_get_int returns Int
|
||||||
|
json_array_len returns Int
|
||||||
|
engram_node_count returns Int
|
||||||
|
engram_edge_count returns Int
|
||||||
|
time_now returns Int
|
||||||
|
time_now_utc returns Int
|
||||||
|
time_diff returns Int
|
||||||
|
time_add returns Int
|
||||||
|
time_from_parts returns Int
|
||||||
|
el_abs returns Int
|
||||||
|
el_max returns Int
|
||||||
|
el_min returns Int
|
||||||
|
float_to_int returns Int
|
||||||
|
unix_timestamp returns Int
|
||||||
|
instant_to_unix_seconds returns Int
|
||||||
|
instant_to_unix_millis returns Int
|
||||||
|
duration_to_seconds returns Int
|
||||||
|
duration_to_millis returns Int
|
||||||
|
duration_to_nanos returns Int
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# temporal.rel — the affine algebra of time, as data.
|
||||||
|
#
|
||||||
|
# An Instant is a POINT and a Duration is a DISPLACEMENT. Every rule below
|
||||||
|
# follows from that, and the set is closed because there are only two kinds of
|
||||||
|
# thing: you may add a displacement to a point, subtract two points to get a
|
||||||
|
# displacement, and combine displacements. Nothing else is meaningful.
|
||||||
|
#
|
||||||
|
# The emitter records which illegal combination it saw; this file says what that
|
||||||
|
# means and how to say it.
|
||||||
|
#
|
||||||
|
# <kind> means <message>
|
||||||
|
|
||||||
|
instant_plus_instant means Instant + Instant is not allowed — a point plus a point is not a point. Subtract them for a Duration, or add a Duration.
|
||||||
|
instant_plus_int means Instant + Int is not allowed — a bare literal is a magnitude with no axis. 3 of what? Adding it to a point moves the instant by an unspecified amount. Use a Duration.
|
||||||
|
duration_plus_int means Duration + Int is not allowed — an Int carries no unit. Use duration_seconds(n) or N.seconds.
|
||||||
|
duration_minus_int means Duration - Int is not allowed — an Int carries no unit.
|
||||||
|
instant_cmp_duration means Instant < Duration is not allowed — a point and a displacement are not on the same scale.
|
||||||
|
duration_cmp_instant means Duration < Instant is not allowed — a displacement and a point are not on the same scale.
|
||||||
|
caltime_plus_caltime means CalendarTime + CalendarTime is not allowed — a CalendarTime already projects an Instant under a Calendar. Use cal_to_instant first.
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# temporal.sh — report temporal type violations from emitted relations.
|
||||||
|
#
|
||||||
|
# The emitter still has to emit SOMETHING for an illegal expression, so the
|
||||||
|
# TIME_TYPE_ERROR placeholder stays in the generated C. What moved out is the
|
||||||
|
# judgment and the wording: codegen records "temporal:instant_plus_instant" and
|
||||||
|
# this decides what that means.
|
||||||
|
set -uo pipefail
|
||||||
|
REL="${1:?usage: temporal.sh <relations-file> [rules]}"
|
||||||
|
RULES="${2:-$(dirname "${BASH_SOURCE[0]}")/temporal.rel}"
|
||||||
|
[ -f "$REL" ] || exit 0
|
||||||
|
V=0
|
||||||
|
while read -r caller _ rest; do
|
||||||
|
[ "${rest#temporal:}" = "$rest" ] && continue
|
||||||
|
kind="${rest#temporal:}"
|
||||||
|
msg=$(awk -v k="$kind" '$1==k && $2=="means" {sub(/^[^ ]+[ ]+means[ ]+/,""); print; exit}' "$RULES")
|
||||||
|
[ -n "$msg" ] || msg="$kind"
|
||||||
|
printf 'temporal type error in %s: %s\n' "$caller" "$msg"
|
||||||
|
V=$((V+1))
|
||||||
|
done < <(sort -u "$REL")
|
||||||
|
[ "$V" -eq 0 ] && echo "temporal: clean"
|
||||||
|
exit "$V"
|
||||||
Executable
+161
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# check-runtime-growth.sh — GROWTH guard for lang/runtime/el_runtime.c.
|
||||||
|
#
|
||||||
|
# Sibling to scripts/check-single-runtime.sh. That one guards against the file
|
||||||
|
# being COPIED (a lagging fork shipped to prod and dropped learned hebb edges).
|
||||||
|
# Nothing guarded against it GROWING — so it grew from 10,607 to 20,527 lines in
|
||||||
|
# 3.5 months, while under an explicit commit-message promise that it was a
|
||||||
|
# temporary shim about to be deleted.
|
||||||
|
#
|
||||||
|
# This enforces the RATCHET in lang/runtime/BUDGET: the numbers may only go down.
|
||||||
|
#
|
||||||
|
# It also checks two invariants that keep the multi-file runtime honest:
|
||||||
|
# * every .c in lang/runtime/ is either in SOURCES or explicitly optional
|
||||||
|
# * lang/install.sh's hardcoded download list matches SOURCES
|
||||||
|
#
|
||||||
|
# Exits non-zero on any violation. Run from anywhere; resolves the repo root.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
RUNTIME_DIR="lang/runtime"
|
||||||
|
TARGET="$RUNTIME_DIR/el_runtime.c"
|
||||||
|
BUDGET_FILE="$RUNTIME_DIR/BUDGET"
|
||||||
|
SOURCES_FILE="$RUNTIME_DIR/SOURCES"
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
for f in "$TARGET" "$BUDGET_FILE" "$SOURCES_FILE"; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "FATAL: required file missing: $f" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
budget() {
|
||||||
|
local key="$1"
|
||||||
|
sed -e 's/#.*//' "$BUDGET_FILE" | awk -v k="$key" '$1==k {print $2; found=1} END{if(!found) exit 1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
MAX_LINES="$(budget max_lines)" || { echo "FATAL: no 'max_lines' in $BUDGET_FILE" >&2; exit 1; }
|
||||||
|
MAX_ENGRAM="$(budget max_engram_fns)" || { echo "FATAL: no 'max_engram_fns' in $BUDGET_FILE" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# The message every failure prints. The guard that existed before this one told
|
||||||
|
# you what was wrong but not where the code should go — so it was easy to
|
||||||
|
# "fix" by arguing with the guard. This one names the destination.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
where_it_goes() {
|
||||||
|
cat >&2 <<'MSG'
|
||||||
|
|
||||||
|
WHERE THE CODE ACTUALLY GOES
|
||||||
|
----------------------------
|
||||||
|
Placement is a LINK-TIME concern. The compiler cannot tell which .c a symbol
|
||||||
|
came from: `builtin_arity` in el-compiler/src/codegen.el maps NAME -> ARITY
|
||||||
|
INT only, the El name is emitted as the exact C symbol, and `ld` resolves it.
|
||||||
|
The SHIPPED compiler already links from ten translation units — check it:
|
||||||
|
|
||||||
|
nm lang/dist/platform/elc | grep -E 'T _(engram_think|vindex_insert)'
|
||||||
|
|
||||||
|
So a builtin defined in a sibling .c is EXACTLY as linkable as one defined in
|
||||||
|
el_runtime.c. Pick the file that owns the concern:
|
||||||
|
|
||||||
|
engram store ops ......... lang/runtime/engram_store.c
|
||||||
|
ANN / vector index ....... lang/runtime/engram_vindex.c
|
||||||
|
geometry, priming ........ lang/runtime/engram_geometry.c
|
||||||
|
reasoning operators ...... lang/runtime/engram_reason.c
|
||||||
|
grounding, consistency ... lang/runtime/engram_verify.c
|
||||||
|
think, stance ............ lang/runtime/engram_cognition.c
|
||||||
|
|
||||||
|
No existing file owns it? Create one, add ONE line to lang/runtime/SOURCES,
|
||||||
|
and every build path picks it up. Every runtime file EXCEPT el_runtime.c is
|
||||||
|
deliberately uncapped.
|
||||||
|
|
||||||
|
Belongs to a downstream program, not the runtime? Declare
|
||||||
|
`c_source "path/to/file.c"` in that program's manifest.el — elb already links
|
||||||
|
it (parse_manifest_c_sources, lang/elb.el:82).
|
||||||
|
|
||||||
|
See lang/AGENTS.md "Where a new C builtin goes".
|
||||||
|
MSG
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. Line-count ratchet ---------------------------------------------------
|
||||||
|
LINES="$(wc -l < "$TARGET" | tr -d ' ')"
|
||||||
|
if [ "$LINES" -gt "$MAX_LINES" ]; then
|
||||||
|
echo "FAIL: $TARGET grew past its budget." >&2
|
||||||
|
echo " now: $LINES lines" >&2
|
||||||
|
echo " budget: $MAX_LINES lines (lang/runtime/BUDGET: max_lines)" >&2
|
||||||
|
echo " over by: $((LINES - MAX_LINES))" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "This file is a 2026-05-03 build shim that was scheduled for deletion and" >&2
|
||||||
|
echo "never retired. It does not get to grow. Do NOT raise the budget." >&2
|
||||||
|
where_it_goes
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. Engram-concern ratchet ----------------------------------------------
|
||||||
|
# ~47.5% of el_runtime.c is engram code, and engram already owns six sibling
|
||||||
|
# files. This count is the Stage 3 scoreboard: it may only go down.
|
||||||
|
ENGRAM_FNS="$(grep -cE '^(static +)?[A-Za-z_][A-Za-z0-9_ *]*\b(engram|eg|cog)_[a-z0-9_]+\(' "$TARGET" || true)"
|
||||||
|
if [ "$ENGRAM_FNS" -gt "$MAX_ENGRAM" ]; then
|
||||||
|
echo "FAIL: new engram/eg_/cog_ function(s) added to $TARGET." >&2
|
||||||
|
echo " now: $ENGRAM_FNS definitions" >&2
|
||||||
|
echo " budget: $MAX_ENGRAM (lang/runtime/BUDGET: max_engram_fns)" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Engram code belongs in the six engram_*.c files that already exist." >&2
|
||||||
|
where_it_goes
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. Ratchet-down nudge (advisory, never fails) ---------------------------
|
||||||
|
if [ "$LINES" -lt "$MAX_LINES" ]; then
|
||||||
|
echo "NOTE: $TARGET is $((MAX_LINES - LINES)) lines under budget — lower" >&2
|
||||||
|
echo " 'max_lines' to $LINES in $BUDGET_FILE in this same commit, so the" >&2
|
||||||
|
echo " ground you gained cannot be quietly given back." >&2
|
||||||
|
fi
|
||||||
|
if [ "$ENGRAM_FNS" -lt "$MAX_ENGRAM" ]; then
|
||||||
|
echo "NOTE: $((MAX_ENGRAM - ENGRAM_FNS)) engram fn(s) moved out — lower" >&2
|
||||||
|
echo " 'max_engram_fns' to $ENGRAM_FNS in $BUDGET_FILE in this same commit." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. Every runtime .c is accounted for ------------------------------------
|
||||||
|
# A new .c that is in neither SOURCES nor the optional list will not be
|
||||||
|
# compiled by any build path — it would be silently dead. Catch that here.
|
||||||
|
OPTIONAL_RE='^(el_android|el_gtk4|el_lvgl|el_sdl2|el_win32|el_runtime_win32|eg_cosine_batch_strategy_ggml|vindex_bench)\.c$'
|
||||||
|
mapfile -t IN_SOURCES < <(scripts/el-runtime-sources.sh)
|
||||||
|
for path in "$RUNTIME_DIR"/*.c; do
|
||||||
|
base="$(basename "$path")"
|
||||||
|
if printf '%s\n' "${IN_SOURCES[@]}" | grep -qxF "$base"; then continue; fi
|
||||||
|
if [[ "$base" =~ $OPTIONAL_RE ]]; then continue; fi
|
||||||
|
echo "FAIL: $path is in neither lang/runtime/SOURCES nor the platform-optional" >&2
|
||||||
|
echo " list in this guard. It will not be compiled by any build path." >&2
|
||||||
|
echo " Add it to SOURCES (one line), or add it to OPTIONAL_RE here if it" >&2
|
||||||
|
echo " is a platform/strategy variant that is linked in deliberately." >&2
|
||||||
|
FAIL=1
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 5. install.sh must not drift from SOURCES -------------------------------
|
||||||
|
# install.sh runs on machines with no repo checkout, so it cannot call
|
||||||
|
# el-runtime-sources.sh and has to hardcode the list. That copy is exactly the
|
||||||
|
# kind of duplicate that silently drifted before — so it is checked, not trusted.
|
||||||
|
INSTALL_SH="lang/install.sh"
|
||||||
|
if [ -f "$INSTALL_SH" ]; then
|
||||||
|
EXPECTED="$(scripts/el-runtime-sources.sh | sort)"
|
||||||
|
ACTUAL="$(sed -n '/^RUNTIME_SOURCES=(/,/^)/p' "$INSTALL_SH" \
|
||||||
|
| grep -oE '[a-z_0-9]+\.c' | sort)"
|
||||||
|
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
||||||
|
echo "FAIL: $INSTALL_SH RUNTIME_SOURCES has drifted from $SOURCES_FILE." >&2
|
||||||
|
echo " Only in SOURCES: $(comm -23 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
|
||||||
|
echo " Only in install.sh: $(comm -13 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
|
||||||
|
echo " An SDK that ships the wrong set produces a lib/ that cannot link." >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FAIL" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: el_runtime.c within budget ($LINES/$MAX_LINES lines, $ENGRAM_FNS/$MAX_ENGRAM engram fns);"
|
||||||
|
echo " runtime sources accounted for; install.sh in step with SOURCES."
|
||||||
@@ -81,14 +81,14 @@ fi
|
|||||||
echo "OK: single canonical runtime source — $CANONICAL (no un-allowlisted forks)."
|
echo "OK: single canonical runtime source — $CANONICAL (no un-allowlisted forks)."
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# CI wire-in:
|
# CI wire-in — DONE (2026-08-16). This block used to describe the wire-in as a
|
||||||
# foundation/el .gitea/workflows/ci-dev.yaml, ci-stage.yaml, sdk-release.yaml
|
# TODO, and it had never been done: the guard existed but ran nowhere, so it
|
||||||
# Add an early step (before the build/publish steps). It must run from the
|
# caught nothing for as long as it has been in the tree. It is now an early step
|
||||||
# REPO ROOT, so override the job's `defaults.run.working-directory: lang`:
|
# in ci-dev.yaml, ci-stage.yaml and sdk-release.yaml (each with
|
||||||
|
# `working-directory: ${{ github.workspace }}`, since the jobs default to lang/),
|
||||||
|
# and it runs in .githooks/pre-commit.
|
||||||
#
|
#
|
||||||
# - name: Guard - single canonical runtime source
|
# Its sibling scripts/check-runtime-growth.sh is wired in at the same points and
|
||||||
# working-directory: ${{ github.workspace }}
|
# guards the other half of the problem: this script stops el_runtime.c being
|
||||||
# run: bash scripts/check-single-runtime.sh
|
# COPIED, that one stops it GROWING.
|
||||||
#
|
|
||||||
# Also add to .githooks/pre-commit so drift is caught before it is committed.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# el-runtime-sources.sh — print the canonical El runtime link set.
|
||||||
|
#
|
||||||
|
# Reads lang/runtime/SOURCES (the single source of truth) and prints one path
|
||||||
|
# per line, optionally prefixed with a directory. Use it anywhere a link line
|
||||||
|
# would otherwise spell the runtime .c files out longhand:
|
||||||
|
#
|
||||||
|
# cc -std=c11 -O2 -I lang/runtime -o app app.c \
|
||||||
|
# $(scripts/el-runtime-sources.sh lang/runtime) \
|
||||||
|
# -lcurl -lssl -lcrypto -lpthread -lm
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --headers print the shipped headers instead of the .c sources
|
||||||
|
# --check verify every listed file exists; exit non-zero if any is missing
|
||||||
|
#
|
||||||
|
# WHY: linking el_runtime.c alone has been broken since el_runtime.c started
|
||||||
|
# calling into the engram siblings. The list was duplicated across ~8 build
|
||||||
|
# paths and drifted. It lives in exactly one place now — see lang/runtime/SOURCES.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SOURCES="${ROOT}/lang/runtime/SOURCES"
|
||||||
|
|
||||||
|
if [ ! -f "$SOURCES" ]; then
|
||||||
|
echo "FATAL: canonical runtime source list missing: $SOURCES" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MODE="sources"
|
||||||
|
PREFIX=""
|
||||||
|
CHECK=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--headers) MODE="headers" ;;
|
||||||
|
--check) CHECK=1 ;;
|
||||||
|
-*) echo "el-runtime-sources.sh: unknown option: $arg" >&2; exit 2 ;;
|
||||||
|
*) PREFIX="${arg%/}/" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Strip comments and blank lines. Order is preserved — it is link order.
|
||||||
|
mapfile -t FILES < <(sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$SOURCES" | grep -v '^$')
|
||||||
|
|
||||||
|
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "FATAL: $SOURCES lists no sources" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MODE" = "headers" ]; then
|
||||||
|
# Every .c's matching .h, plus the headers that carry no .c of their own.
|
||||||
|
HDRS=()
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
h="${f%.c}.h"
|
||||||
|
[ -f "${ROOT}/lang/runtime/${h}" ] && HDRS+=("$h")
|
||||||
|
done
|
||||||
|
# Interface-only headers: no matching .c, but required to compile against.
|
||||||
|
for h in eg_cosine_batch_strategy.h el_native_target.h el_platform_win.h; do
|
||||||
|
[ -f "${ROOT}/lang/runtime/${h}" ] && HDRS+=("$h")
|
||||||
|
done
|
||||||
|
FILES=("${HDRS[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
RC=0
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
if [ "$CHECK" -eq 1 ] && [ ! -f "${ROOT}/lang/runtime/${f}" ]; then
|
||||||
|
echo "MISSING: lang/runtime/${f} (listed in lang/runtime/SOURCES)" >&2
|
||||||
|
RC=1
|
||||||
|
fi
|
||||||
|
printf '%s%s\n' "$PREFIX" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
exit $RC
|
||||||
Reference in New Issue
Block a user