Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6df362f84d | |||
| 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 | |||
| c26b6aac82 | |||
| 99ef855b98 | |||
| 5503e1d9a4 | |||
| c23c5112a7 | |||
| b40af4a5f3 | |||
| c7a78ab1eb | |||
| 8c94d92033 |
@@ -19,6 +19,16 @@ jobs:
|
||||
- name: Checkout
|
||||
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
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -41,7 +51,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -56,7 +66,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -87,14 +97,20 @@ jobs:
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
# The runtime is MULTI-FILE (see lang/runtime/SOURCES). Every .c is compiled
|
||||
# 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: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
rm -rf /tmp/elrt && mkdir -p /tmp/elrt
|
||||
for src in $(../scripts/el-runtime-sources.sh --check "$RUNTIME"); do
|
||||
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)
|
||||
run: |
|
||||
@@ -102,7 +118,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -112,7 +128,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -122,7 +138,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -132,7 +148,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -142,7 +158,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -152,7 +168,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -162,7 +178,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -172,7 +188,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -182,7 +198,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -306,8 +322,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# 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
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
@@ -29,6 +29,16 @@ jobs:
|
||||
fi
|
||||
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
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -48,7 +58,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -86,7 +96,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -96,7 +106,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -106,7 +116,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -116,7 +126,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -126,7 +136,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -136,7 +146,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -146,7 +156,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -156,7 +166,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -166,7 +176,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -178,7 +188,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -290,8 +300,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# 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
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
@@ -29,6 +29,16 @@ jobs:
|
||||
fi
|
||||
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
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -49,7 +59,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -64,7 +74,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -123,7 +133,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -133,7 +143,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -143,7 +153,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -153,7 +163,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -163,7 +173,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -173,7 +183,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -183,7 +193,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -193,7 +203,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -203,7 +213,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
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
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -216,10 +226,17 @@ jobs:
|
||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||
cp lang/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.c dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.h dist/sdk/runtime/
|
||||
# Ship the WHOLE runtime link set, not el_runtime.c alone. el_runtime.c
|
||||
# #includes six engram headers and calls into all six sibling .c files,
|
||||
# so an SDK carrying only el_runtime.c{,.h} + engram_store.c{,.h} cannot
|
||||
# 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/
|
||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||
@@ -274,12 +291,16 @@ jobs:
|
||||
"${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/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/runtime/el_runtime.h el_runtime.h
|
||||
upload_asset lang/runtime/engram_store.c engram_store.c
|
||||
upload_asset lang/runtime/engram_store.h engram_store.h
|
||||
for f in $(scripts/el-runtime-sources.sh --check) \
|
||||
$(scripts/el-runtime-sources.sh --headers --check); do
|
||||
upload_asset "lang/runtime/${f}" "${f}"
|
||||
done
|
||||
upload_asset lang/runtime/SOURCES SOURCES
|
||||
|
||||
# SDK bundle and installer binary
|
||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||
@@ -350,6 +371,26 @@ jobs:
|
||||
--version="${VERSION}" \
|
||||
--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"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
@@ -386,8 +427,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# 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
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
+42
-3
@@ -9,13 +9,52 @@ LANG_DIR="$ROOT/lang"
|
||||
RUNTIME="$LANG_DIR/runtime"
|
||||
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 [ ! -x "$ELC" ]; then
|
||||
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
|
||||
echo " Build it first: cd lang && gcc -O2 -I runtime dist/elc-bootstrap.c runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I runtime /tmp/elc.c runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
echo " Build it first: see 'Rebuilding the Compiler' in lang/AGENTS.md"
|
||||
echo " (link \$($ROOT/scripts/el-runtime-sources.sh $RUNTIME) — NOT el_runtime.c alone)"
|
||||
exit 0
|
||||
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..."
|
||||
PASS=0
|
||||
FAIL=0
|
||||
@@ -27,8 +66,8 @@ for test_file in "$LANG_DIR"/tests/native/test_*.el; do
|
||||
tmp_bin="/tmp/el_hook_${name}"
|
||||
|
||||
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
|
||||
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||
&& gcc -O2 -I "$RUNTIME" $SSL_INC $SSL_LIB "$tmp_c" "$HOOK_LIB" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||
&& "$tmp_bin" 2>/dev/null; then
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
# organ: local device state and its own engram store — never production's
|
||||
peripheral/.consent.json
|
||||
peripheral/.resume.json
|
||||
peripheral/.engram/
|
||||
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`.
|
||||
|
||||
> ### 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):
|
||||
```bash
|
||||
cd lang
|
||||
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 \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
gcc -O2 -I runtime dist/elc-gen2.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-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):
|
||||
```bash
|
||||
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
|
||||
```
|
||||
`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:**
|
||||
```bash
|
||||
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`:
|
||||
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):
|
||||
cc -c el_runtime.c -o el_runtime.o
|
||||
elc eltest.el > eltest.c && cc -c eltest.c -o eltest.o
|
||||
ar rcs libeltest.a el_runtime.o eltest.o
|
||||
# The runtime is MULTI-FILE — compile every .c named in lang/runtime/SOURCES.
|
||||
# Linking el_runtime.c alone fails: it calls into the six engram sibling TUs.
|
||||
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:
|
||||
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.
|
||||
+195
-6
@@ -1479,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
|
||||
// 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)
|
||||
// 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
|
||||
// independent top-level per-node updates. Here the region is the unit: one
|
||||
// isolate, one atomic set-replace, one persist, one verify — iterating members
|
||||
@@ -1696,6 +1703,174 @@ fn reframe_core(region: [String], manifold: String, reason: String, do_rebind: I
|
||||
",\"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.
|
||||
// Body: {vantage?, region_ids?(csv), k?, expand?, manifold(json array), reason?, rebind?}
|
||||
// region_ids (explicit) wins; else cosine-isolate around vantage.
|
||||
@@ -1732,18 +1907,32 @@ fn route_reframe(method: String, path: String, body: String) -> String {
|
||||
return reframe_core(region, manifold, reason, do_rebind)
|
||||
}
|
||||
|
||||
// write — DEGENERATE n=1 of reframe: region=∅, manifold=[1 node]. The SAME
|
||||
// reframe_core path. rebind off so the pure-add matches plain node creation.
|
||||
// POST /api/write {content, node_type?, tier?, tags?}
|
||||
// write — INTAKE OF A SIGNAL. Not "reframe with a manifold of one node": the
|
||||
// route no longer decides how many nodes the signal is. It hands the signal to
|
||||
// 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 {
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { return err_json("write: content required") }
|
||||
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, "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 manifold: String = "[" + body + "]" // the body IS a valid manifold node object
|
||||
return reframe_core(empty, manifold, "write", 0)
|
||||
return intake_signal(content, modality, empty, nt, tier, tags, "write", 0)
|
||||
}
|
||||
|
||||
// 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.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p0-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
@@ -14,8 +18,8 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile (plain) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p0" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p0_emb.c" $RTSRC $SSLFLAGS \
|
||||
-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"
|
||||
"$WORK/p0" "$D" || { echo "FAIL: run"; fail=1; }
|
||||
@@ -69,8 +73,8 @@ PY
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p0_emb.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p0.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -20 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p0_emb.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/ds"; mkdir -p "$DS"
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
# Throwaway HOME + /tmp only. Never touches ~/.neuron or :8742.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p1-XXXXXX)"
|
||||
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
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p1" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p1_consol.c" $RTSRC $SSLFLAGS \
|
||||
-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 "== (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 "== ASan+UBSan (connect + perm + accrual-short) =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p1_consol.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p1.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p1_consol.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/san"; mkdir -p "$DS"
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
# Throwaway HOME + /tmp only. TC defaults to 3600s; we pin it for the math.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p2-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
@@ -15,8 +19,8 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p2" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p2_chrono.c" $RTSRC $SSLFLAGS \
|
||||
-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']))"; }
|
||||
|
||||
@@ -78,8 +82,8 @@ python3 -c "import sys; sys.exit(0 if abs($OFFWM-1.2)<1e-9 else 1)" \
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p2_chrono.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p2.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p2_chrono.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/san"; mkdir -p "$DS"
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
# Read-only pure primitive; no store, no flag. Throwaway /tmp only.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p3-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p3" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" $RTSRC $SSLFLAGS \
|
||||
-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; }
|
||||
cat "$WORK/out.txt" | sed 's/^/ /'
|
||||
@@ -52,8 +56,8 @@ PY
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p3.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p3_drift.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
"$WORK/p3.san" >/dev/null 2>"$WORK/san.log"
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
# M-INTEROCEPTION P4 gate: afferent input counters in act-stats (additive).
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p4-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
@@ -13,8 +17,8 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p4" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" $RTSRC $SSLFLAGS \
|
||||
-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; }
|
||||
grep -oE 'aff_[a-z_]+":[0-9]+' "$WORK/out.txt" | sed 's/^/ /' | head -30
|
||||
@@ -53,8 +57,8 @@ PY
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p4.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p4_afferent.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
"$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).
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
GEO="$HERE/../../lang/runtime/engram_geometry.c"
|
||||
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-p5-XXXXXX)"
|
||||
export HOME="$WORK/home"; mkdir -p "$HOME"
|
||||
@@ -13,8 +17,8 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compile =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p5" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" $RTSRC $SSLFLAGS \
|
||||
-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"
|
||||
"$WORK/p5" "$D" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
|
||||
@@ -57,8 +61,8 @@ PY
|
||||
echo
|
||||
echo "== ASan+UBSan =="
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \
|
||||
-lcurl -lm -o "$WORK/p5.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
|
||||
-I "$INC" "$HERE/test_interoception_p5_dreams.c" $RTSRC $SSLFLAGS \
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DS="$WORK/ds"; mkdir -p "$DS"
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-m35-XXXXXX)"
|
||||
BIN="$WORK/m35"
|
||||
@@ -17,7 +23,7 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m35_hebb_persist.c) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$BIN" 2>"$WORK/cc.log"
|
||||
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
|
||||
|
||||
echo
|
||||
@@ -139,7 +145,7 @@ echo
|
||||
echo "== 5) ASan+UBSan build, exercise the full persist+reboot flow (leaks off — harness intentionally leaks el_strdup) =="
|
||||
SANBIN="$WORK/m35.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DSAN="$WORK/san"; mkdir -p "$DSAN"
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME + ENGRAM_DATA_DIR.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-m3-XXXXXX)"
|
||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||
@@ -17,7 +23,7 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m3_parity.c) =="
|
||||
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m3_parity.c" "$RT" "$ST" -lcurl -o "$BIN" 2>"$WORK/cc.log"
|
||||
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
|
||||
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) =="
|
||||
SANBIN="$WORK/m3.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m3_parity.c" "$RT" "$ST" -lcurl -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
DATA2="$WORK/data2"; mkdir -p "$DATA2"
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
RT="$HERE/../../lang/runtime/el_runtime.c"
|
||||
ST="$HERE/../../lang/runtime/engram_store.c"
|
||||
RTSRC="$("$HERE/../../scripts/el-runtime-sources.sh" "$HERE/../../lang/runtime")"
|
||||
# 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"
|
||||
WORK="$(mktemp -d /tmp/engram-m7-XXXXXX)"
|
||||
DATA="$WORK/data"; mkdir -p "$DATA"
|
||||
@@ -22,7 +28,7 @@ unset ENGRAM_STORE
|
||||
fail=0
|
||||
|
||||
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m7_traversal.c) =="
|
||||
gcc -O2 -std=c11 -I "$INC" "$HERE/test_m7_traversal.c" "$RT" "$ST" -lcurl -lm -o "$BIN" 2>"$WORK/cc.log"
|
||||
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
|
||||
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) =="
|
||||
SANBIN="$WORK/m7.san"
|
||||
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
|
||||
-I "$INC" "$HERE/test_m7_traversal.c" "$RT" "$ST" -lcurl -lm -o "$SANBIN" 2>"$WORK/san_cc.log"
|
||||
-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
|
||||
export ASAN_OPTIONS=detect_leaks=0
|
||||
D2="$WORK/data2"; mkdir -p "$D2"
|
||||
|
||||
@@ -3,8 +3,17 @@
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
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" \
|
||||
"$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
|
||||
# Fail-loud data-dir check (must exit 1 with a FATAL line):
|
||||
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");
|
||||
engram_resolve_data_dir(); printf("REACHED\n"); return 0; }
|
||||
C
|
||||
cc -O2 -fbracket-depth=1024 -Wno-parentheses-equality -I"$REL" /tmp/test_failloud.c -lcurl -lpthread -o /tmp/test_failloud
|
||||
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
|
||||
|
||||
+23
-5
@@ -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.
|
||||
|
||||
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:
|
||||
- `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 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*.
|
||||
|
||||
**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.
|
||||
|
||||
#### 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):
|
||||
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`.
|
||||
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.
|
||||
@@ -129,7 +145,9 @@ diff elc-new.c elc-verify.c # must be identical
|
||||
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.
|
||||
|
||||
|
||||
+465
-602
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²)
|
||||
// memory growth caused by repeated `prefix = prefix + chunk` concatenation.
|
||||
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 already: String = state_get(seen_key)
|
||||
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.
|
||||
// Join once at the end — O(n) single pass.
|
||||
let prefix_chunks: [String] = native_list_empty()
|
||||
let prefix_paths: [String] = native_list_empty()
|
||||
let body_chunks: [String] = native_list_empty()
|
||||
let i: Int = 0
|
||||
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.
|
||||
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)
|
||||
// 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, "") {
|
||||
// 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.
|
||||
let seen_imp_key: String = "__elc_imp__:" + imp_path
|
||||
state_set(seen_imp_key, "1")
|
||||
let prefix_chunks = native_list_append(prefix_chunks, imp_elh)
|
||||
let prefix_paths = native_list_append(prefix_paths, imp_path)
|
||||
} else {
|
||||
let imp_body: String = resolve_imports(imp_path)
|
||||
let prefix_chunks = native_list_append(prefix_chunks, imp_body)
|
||||
let prefix_paths = native_list_append(prefix_paths, imp_path)
|
||||
}
|
||||
} else {
|
||||
let body_chunks = native_list_append(body_chunks, line + "\n")
|
||||
}
|
||||
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, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -138,13 +138,38 @@ fn lex_is_whitespace(ch: String) -> Bool {
|
||||
|
||||
// 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.
|
||||
// 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] {
|
||||
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_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 {
|
||||
if word == "let" { return "Let" }
|
||||
if word == "fn" { return "Fn" }
|
||||
@@ -161,14 +186,9 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "from" { return "From" }
|
||||
if word == "as" { return "As" }
|
||||
if word == "with" { return "With" }
|
||||
if word == "sealed" { return "Sealed" }
|
||||
if word == "activate" { return "Activate" }
|
||||
if word == "where" { return "Where" }
|
||||
if word == "test" { return "Test" }
|
||||
if word == "seed" { return "Seed" }
|
||||
if word == "assert" { return "Assert" }
|
||||
if word == "protocol" { return "Protocol" }
|
||||
if word == "impl" { return "Impl" }
|
||||
if word == "retry" { return "Retry" }
|
||||
if word == "times" { return "Times" }
|
||||
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
|
||||
// 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] {
|
||||
let src_len: Int = native_list_len(src)
|
||||
let j = 0
|
||||
@@ -531,9 +557,11 @@ fn interp_tokens_append_all(dst: [Any], src: [Any]) -> [Any] {
|
||||
let j = src_len
|
||||
} else {
|
||||
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, val)
|
||||
let j = j + 2
|
||||
let result = native_list_append(result, ln)
|
||||
let j = j + 3
|
||||
}
|
||||
}
|
||||
result
|
||||
@@ -764,8 +792,14 @@ fn lex(source: String) -> [Any] {
|
||||
let total: Int = str_len(source)
|
||||
let tokens: [Any] = native_list_empty()
|
||||
let i: Int = 0
|
||||
state_set("__lex_line", "1")
|
||||
let line_no: Int = 1
|
||||
|
||||
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)
|
||||
|
||||
// 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.
|
||||
|
||||
fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
let kind: String = native_list_get(tokens, pos * 2)
|
||||
let value: String = native_list_get(tokens, pos * 2 + 1)
|
||||
let kind: String = native_list_get(tokens, pos * 3)
|
||||
let value: String = native_list_get(tokens, pos * 3 + 1)
|
||||
{ "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),
|
||||
// which matches no delimiter, letting inner parse loops append AST nodes
|
||||
// forever on malformed input -> unbounded allocation -> OOM.
|
||||
let n: Int = native_list_len(tokens) / 2
|
||||
let n: Int = native_list_len(tokens) / 3
|
||||
if pos < 0 {
|
||||
return "Eof"
|
||||
}
|
||||
if pos >= n {
|
||||
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 {
|
||||
let n: Int = native_list_len(tokens) / 2
|
||||
let n: Int = native_list_len(tokens) / 3
|
||||
if pos < 0 {
|
||||
return ""
|
||||
}
|
||||
if pos >= n {
|
||||
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
|
||||
@@ -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
|
||||
// every iteration consumes >= 1 token. If we exceed that, the cursor has run
|
||||
// off the end without terminating (malformed input) -> fail fast, don't hang.
|
||||
let blk_total: Int = native_list_len(tokens) / 2
|
||||
let blk_total: Int = native_list_len(tokens) / 3
|
||||
let blk_iters: Int = 0
|
||||
while running {
|
||||
let blk_iters = blk_iters + 1
|
||||
@@ -1550,7 +1557,10 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
let p = r2["pos"]
|
||||
// r2 result map fully consumed — release to free peak heap.
|
||||
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, ... }`
|
||||
@@ -1842,6 +1852,7 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
"params": inner["params"],
|
||||
"body": inner["body"],
|
||||
"ret_type": inner["ret_type"],
|
||||
"line": inner["line"],
|
||||
"decorator": dec_name,
|
||||
"decorators": dlist
|
||||
}
|
||||
@@ -2158,7 +2169,7 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
|
||||
fn parse(tokens: [Any]) -> [Map<String, Any>] {
|
||||
// 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 pos: Int = 0
|
||||
let running = true
|
||||
@@ -2201,7 +2212,7 @@ fn parse_one(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
// On entry, pos must point at the LBrace token.
|
||||
// Returns the position of the token AFTER the matching RBrace.
|
||||
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 depth: Int = 1
|
||||
let going: Bool = true
|
||||
@@ -2253,7 +2264,7 @@ fn is_stmt_start_kind(k: String) -> Bool {
|
||||
// token that could start a new top-level statement, staying depth-aware
|
||||
// so that braces inside expressions don't fool us.
|
||||
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 depth: Int = 0
|
||||
let going: Bool = true
|
||||
@@ -2419,7 +2430,7 @@ fn scan_params_el(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
//
|
||||
// Peak memory: O(tokens) with no expression AST allocation.
|
||||
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(tokens) / 2
|
||||
let total: Int = native_list_len(tokens) / 3
|
||||
let sigs: [Map<String, Any>] = native_list_empty()
|
||||
let pos: Int = 0
|
||||
let going: Bool = true
|
||||
@@ -2561,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.
|
||||
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 pos: Int = 0
|
||||
let going: Bool = true
|
||||
|
||||
+41
-9
@@ -49,21 +49,49 @@ download() {
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
||||
download "${RELEASE_BASE}/el_runtime.c" "${TMP_DIR}/el_runtime.c"
|
||||
download "${RELEASE_BASE}/el_runtime.h" "${TMP_DIR}/el_runtime.h"
|
||||
# The runtime is MULTI-FILE. el_runtime.c #includes six engram headers and makes
|
||||
# hard cross-TU calls into all six sibling .c files, so installing el_runtime.c
|
||||
# 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 -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.c" "${LIB_DIR}/el_runtime.c"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.h" "${LIB_DIR}/el_runtime.h"
|
||||
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||
for f in "${RUNTIME_SOURCES[@]}" "${RUNTIME_HEADERS[@]}"; do
|
||||
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 "==> El SDK installed successfully"
|
||||
echo
|
||||
echo " elc binary : ${BIN_DIR}/elc"
|
||||
echo " runtime : ${LIB_DIR}/el_runtime.c"
|
||||
echo " header : ${LIB_DIR}/el_runtime.h"
|
||||
echo " runtime : ${LIB_DIR}/ (${#RUNTIME_SOURCES[@]} .c files, ${#RUNTIME_HEADERS[@]} headers)"
|
||||
echo " link set : ${LIB_DIR}/SOURCES"
|
||||
echo
|
||||
echo "Add the following to your Makefile to build El programs:"
|
||||
echo
|
||||
@@ -71,10 +99,14 @@ echo " EL_LIB := ${LIB_DIR}"
|
||||
echo " ELC := elc"
|
||||
echo " CC := cc"
|
||||
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 " dist/myapp.c: src/myapp.el"
|
||||
echo " \t\$(ELC) src/myapp.el > dist/myapp.c"
|
||||
echo
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,525 @@
|
||||
/* el_audio_darwin.m — the SPEAKER realizer. El's native audio output on Darwin.
|
||||
*
|
||||
* WHY THIS FILE EXISTS.
|
||||
*
|
||||
* Neuron could already turn meaning into samples — the render path in
|
||||
* elp/src/speech.el superposes formant resonances over a glottal source and
|
||||
* produces PCM. What it could not do was make a sound. Every path from those
|
||||
* samples to the air ran outside the language: a 939-line Swift program
|
||||
* (peripheral/src/periph.swift) that shelled out to /usr/bin/afplay. So the
|
||||
* voice was not a capability of El or of Neuron. It was a separate binary
|
||||
* standing next to them, and "speak" meant "ask that binary to speak."
|
||||
*
|
||||
* A speaker is not a language feature the way a string is, but it is exactly
|
||||
* the kind of thing a runtime owns: a device. El already owns the filesystem,
|
||||
* the network, the clock, and a graph. It should own the one output device that
|
||||
* makes it audible. After this file, `speak` is an El operation.
|
||||
*
|
||||
* WHY IT IS A REALIZER AND NOT PURE EL.
|
||||
*
|
||||
* This is the boundary the whole design turns on. Everything ABOVE the sample
|
||||
* buffer is arithmetic and belongs in El: formant geometry, superposition,
|
||||
* envelopes, WAV framing, the voice signature. Everything in this file is the
|
||||
* part that cannot be arithmetic — handing a buffer to CoreAudio and waiting
|
||||
* for the hardware to drain it. There is no way to express "the DAC has now
|
||||
* played these samples" in El, and there should not be. So the split is: El
|
||||
* computes the sound, the realizer emits it, and the realizer is as thin as it
|
||||
* can possibly be — it makes no decisions about content, it has no opinion
|
||||
* about audio, and it cannot synthesize anything.
|
||||
*
|
||||
* The precedent is eg_cosine_batch_strategy_metal_hand.m: a platform-bound
|
||||
* capability compiled as its OWN translation unit, declared in el_runtime.h,
|
||||
* and linked in where the platform supports it. Deliberately NOT a patch to
|
||||
* el_runtime.c — adding a device to El must not mean editing the core runtime,
|
||||
* for the same reason adding a modality must not (see el_runtime.c's realizer
|
||||
* registry: a realizer is resolved by name, so new organs never touch the
|
||||
* middle of the language). el_audio_null.c is the same two entry points for
|
||||
* every platform that is not Darwin, so El code that speaks still links
|
||||
* everywhere and simply reports that it has no speaker.
|
||||
*
|
||||
* WHY AudioQueue AND NOT afplay.
|
||||
*
|
||||
* afplay is a process. Using it means the sound Neuron makes is a file it wrote
|
||||
* and asked something else to open — which forces every utterance through the
|
||||
* disk, cannot start until the whole utterance exists, and puts a fork/exec
|
||||
* between the intent to speak and the sound. AudioQueue takes the samples
|
||||
* directly out of memory. Nothing is written, nothing is spawned, and a caller
|
||||
* that wants to stream can push buffers as it renders them.
|
||||
*
|
||||
* AudioToolbox ships with macOS, so this stays own-core: no cloud, no library
|
||||
* to install, no model. The output is the local speaker and nothing leaves the
|
||||
* machine — there is no network path in this file at all, by construction.
|
||||
*/
|
||||
|
||||
#import <AudioToolbox/AudioToolbox.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* Three buffers is the standard AudioQueue depth: one being played by the
|
||||
* hardware, one queued behind it, one being refilled. Fewer risks a gap on a
|
||||
* busy machine; more only adds latency before the first sound. */
|
||||
#define EL_AQ_NBUF 3
|
||||
#define EL_AQ_FRAMES 8192
|
||||
|
||||
typedef struct {
|
||||
const int16_t* pcm;
|
||||
int64_t frames;
|
||||
int64_t pos;
|
||||
volatile int inflight; /* buffers CoreAudio still owns */
|
||||
volatile int drained; /* set once the last buffer has been played */
|
||||
} ElAqState;
|
||||
|
||||
/* Called on an AudioQueue-internal thread each time a buffer finishes playing.
|
||||
* Refills and re-enqueues while samples remain; when the source is exhausted it
|
||||
* lets the buffer die and counts it out. `drained` flips only when the queue is
|
||||
* holding nothing, which is what makes the play call synchronous without
|
||||
* clipping the tail — the same reason periph.swift used .dataPlayedBack rather
|
||||
* than treating "consumed" as "heard". */
|
||||
static void el_aq_callback(void* userData, AudioQueueRef q, AudioQueueBufferRef buf) {
|
||||
ElAqState* st = (ElAqState*)userData;
|
||||
int64_t remain = st->frames - st->pos;
|
||||
if (remain <= 0) {
|
||||
if (--st->inflight <= 0) st->drained = 1;
|
||||
return;
|
||||
}
|
||||
int64_t n = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(buf->mAudioData, st->pcm + st->pos, (size_t)n * sizeof(int16_t));
|
||||
buf->mAudioDataByteSize = (UInt32)(n * (int64_t)sizeof(int16_t));
|
||||
st->pos += n;
|
||||
if (AudioQueueEnqueueBuffer(q, buf, 0, NULL) != noErr) {
|
||||
if (--st->inflight <= 0) st->drained = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Play a 16-bit mono PCM buffer out the default output device, blocking until
|
||||
* the hardware has actually finished. Returns 1 on success, 0 on any failure —
|
||||
* never throws, never hangs indefinitely. */
|
||||
static int el_audio_play_raw(const int16_t* pcm, int64_t frames, int32_t sample_rate) {
|
||||
if (!pcm || frames <= 0 || sample_rate <= 0) return 0;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sample_rate;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
ElAqState st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.pcm = pcm;
|
||||
st.frames = frames;
|
||||
|
||||
AudioQueueRef q = NULL;
|
||||
/* NULL run loop => callbacks arrive on an AudioQueue-internal thread, so
|
||||
* this function can simply wait rather than having to pump a run loop it
|
||||
* does not own. El programs are not required to have one. */
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, &st, NULL, NULL, 0, &q) != noErr || !q) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
AudioQueueBufferRef bufs[EL_AQ_NBUF];
|
||||
int prepared = 0;
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
if (AudioQueueAllocateBuffer(q, EL_AQ_FRAMES * sizeof(int16_t), &bufs[i]) != noErr) break;
|
||||
prepared++;
|
||||
}
|
||||
if (prepared == 0) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
/* Prime: fill what we can before starting, so playback begins immediately
|
||||
* rather than after the first underrun. */
|
||||
for (int i = 0; i < prepared; i++) {
|
||||
int64_t remain = st.frames - st.pos;
|
||||
if (remain <= 0) break;
|
||||
int64_t n = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(bufs[i]->mAudioData, st.pcm + st.pos, (size_t)n * sizeof(int16_t));
|
||||
bufs[i]->mAudioDataByteSize = (UInt32)(n * (int64_t)sizeof(int16_t));
|
||||
st.pos += n;
|
||||
if (AudioQueueEnqueueBuffer(q, bufs[i], 0, NULL) != noErr) break;
|
||||
st.inflight++;
|
||||
}
|
||||
if (st.inflight == 0) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
if (AudioQueueStart(q, NULL) != noErr) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
/* Bound the wait by the material's own duration plus a margin. A speaker
|
||||
* that wedges a program is worse than a speaker that gives up. */
|
||||
double seconds = (double)frames / (double)sample_rate;
|
||||
int64_t max_us = (int64_t)((seconds + 5.0) * 1000000.0);
|
||||
int64_t waited = 0;
|
||||
const int64_t tick = 5000; /* 5 ms */
|
||||
while (!st.drained && waited < max_us) {
|
||||
usleep((useconds_t)tick);
|
||||
waited += tick;
|
||||
}
|
||||
|
||||
AudioQueueStop(q, true);
|
||||
AudioQueueDispose(q, true);
|
||||
return st.drained ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── El entry points ────────────────────────────────────────────────────────
|
||||
* Declared in el_runtime.h; see there for the El-facing contract. */
|
||||
|
||||
/* 1 when this build has a real speaker behind it. El code should ask before
|
||||
* speaking so the no-speaker case is a reported condition, not a silence that
|
||||
* looks like success. */
|
||||
el_val_t speaker_available(void) {
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t speaker_name(void) {
|
||||
return EL_STR("coreaudio-audioqueue");
|
||||
}
|
||||
|
||||
/* Play an El [Int] of 16-bit samples. Values are clamped, not wrapped: a
|
||||
* render that overshoots should distort at the rails the way real clipping
|
||||
* does, rather than invert phase and produce a sound nothing in the signal
|
||||
* chain intended. */
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate) {
|
||||
int64_t n = (int64_t)el_list_len(samples);
|
||||
int32_t sr = (int32_t)sample_rate;
|
||||
if (n <= 0 || sr <= 0) return (el_val_t)0;
|
||||
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)n * sizeof(int16_t));
|
||||
if (!pcm) return (el_val_t)0;
|
||||
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
int64_t v = (int64_t)el_list_get(samples, (el_val_t)i);
|
||||
if (v > 32767) v = 32767;
|
||||
if (v < -32768) v = -32768;
|
||||
pcm[i] = (int16_t)v;
|
||||
}
|
||||
|
||||
int ok = el_audio_play_raw(pcm, n, sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(ok ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ── Asynchronous playback ───────────────────────────────────────────────────
|
||||
*
|
||||
* converse needs this and a blocking play cannot give it. Barge-in means
|
||||
* stopping ON THE SPOT when the user starts talking — not at the end of the
|
||||
* current buffer, and certainly not at the end of the utterance. So the async
|
||||
* path keeps one queue alive, reports how far the hardware actually got, and
|
||||
* can be halted mid-buffer.
|
||||
*
|
||||
* `played_frames` is what makes an interrupted utterance resumable at the
|
||||
* sample rather than at the segment: it is the position the DAC reached, not
|
||||
* the position we enqueued to, and those differ by up to the full queue depth.
|
||||
*
|
||||
* One utterance at a time. A second async play stops the first — a mouth that
|
||||
* can say two things at once is not a feature. */
|
||||
|
||||
static AudioQueueRef g_aq = NULL;
|
||||
static ElAqState* g_aq_state = NULL;
|
||||
static int16_t* g_aq_pcm = NULL;
|
||||
static int32_t g_aq_sr = 0;
|
||||
|
||||
static void el_audio_teardown(void) {
|
||||
if (g_aq) {
|
||||
AudioQueueStop(g_aq, true);
|
||||
AudioQueueDispose(g_aq, true);
|
||||
g_aq = NULL;
|
||||
}
|
||||
free(g_aq_pcm); g_aq_pcm = NULL;
|
||||
free(g_aq_state); g_aq_state = NULL;
|
||||
g_aq_sr = 0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate) {
|
||||
el_audio_teardown();
|
||||
|
||||
int64_t n = (int64_t)el_list_len(samples);
|
||||
int32_t sr = (int32_t)sample_rate;
|
||||
if (n <= 0 || sr <= 0) return (el_val_t)0;
|
||||
|
||||
g_aq_pcm = (int16_t*)malloc((size_t)n * sizeof(int16_t));
|
||||
if (!g_aq_pcm) return (el_val_t)0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
int64_t v = (int64_t)el_list_get(samples, (el_val_t)i);
|
||||
if (v > 32767) v = 32767;
|
||||
if (v < -32768) v = -32768;
|
||||
g_aq_pcm[i] = (int16_t)v;
|
||||
}
|
||||
|
||||
g_aq_state = (ElAqState*)calloc(1, sizeof(ElAqState));
|
||||
if (!g_aq_state) { el_audio_teardown(); return (el_val_t)0; }
|
||||
g_aq_state->pcm = g_aq_pcm;
|
||||
g_aq_state->frames = n;
|
||||
g_aq_sr = sr;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sr;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, g_aq_state, NULL, NULL, 0, &g_aq) != noErr || !g_aq) {
|
||||
el_audio_teardown();
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
int64_t remain = g_aq_state->frames - g_aq_state->pos;
|
||||
if (remain <= 0) break;
|
||||
AudioQueueBufferRef b = NULL;
|
||||
if (AudioQueueAllocateBuffer(g_aq, EL_AQ_FRAMES * sizeof(int16_t), &b) != noErr) break;
|
||||
int64_t k = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(b->mAudioData, g_aq_state->pcm + g_aq_state->pos, (size_t)k * sizeof(int16_t));
|
||||
b->mAudioDataByteSize = (UInt32)(k * (int64_t)sizeof(int16_t));
|
||||
g_aq_state->pos += k;
|
||||
if (AudioQueueEnqueueBuffer(g_aq, b, 0, NULL) != noErr) break;
|
||||
g_aq_state->inflight++;
|
||||
}
|
||||
if (g_aq_state->inflight == 0) { el_audio_teardown(); return (el_val_t)0; }
|
||||
|
||||
if (AudioQueueStart(g_aq, NULL) != noErr) { el_audio_teardown(); return (el_val_t)0; }
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t speaker_playing(void) {
|
||||
if (!g_aq || !g_aq_state) return (el_val_t)0;
|
||||
return (el_val_t)(g_aq_state->drained ? 0 : 1);
|
||||
}
|
||||
|
||||
/* Frames the DAC has actually rendered. AudioQueueGetCurrentTime's mSampleTime
|
||||
* is relative to queue start, which is exactly the "where was I really" figure
|
||||
* a resumable utterance needs. Falls back to the enqueued position if the
|
||||
* timeline is unavailable (it is, briefly, right after start). */
|
||||
el_val_t speaker_played_frames(void) {
|
||||
if (!g_aq || !g_aq_state) return (el_val_t)0;
|
||||
AudioTimeStamp ts;
|
||||
memset(&ts, 0, sizeof(ts));
|
||||
Boolean discontinuity = false;
|
||||
if (AudioQueueGetCurrentTime(g_aq, NULL, &ts, &discontinuity) == noErr &&
|
||||
(ts.mFlags & kAudioTimeStampSampleTimeValid)) {
|
||||
int64_t played = (int64_t)ts.mSampleTime;
|
||||
if (played < 0) played = 0;
|
||||
if (played > g_aq_state->frames) played = g_aq_state->frames;
|
||||
return (el_val_t)played;
|
||||
}
|
||||
return (el_val_t)g_aq_state->pos;
|
||||
}
|
||||
|
||||
/* Pause where we are, keeping the queue and its position intact.
|
||||
*
|
||||
* This is the difference between barge-in and "finish the buffer". The moment
|
||||
* the microphone hears speech, output must stop AT THAT SAMPLE — a listener
|
||||
* experiences even 200ms of continued talking as being talked over. Pause
|
||||
* rather than stop because the interruption might turn out to be a backchannel
|
||||
* ("mm-hm"), and the right response to a backchannel is to carry on as though
|
||||
* nothing happened, which requires the queue to still be exactly where it was.
|
||||
* A stop-and-restart would re-attack the buffer and be audible as a stutter. */
|
||||
el_val_t speaker_pause(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueuePause(g_aq) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_resume(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueueStart(g_aq, NULL) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_stop(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
/* immediate: do NOT let the queue finish what it is holding */
|
||||
AudioQueueStop(g_aq, true);
|
||||
el_audio_teardown();
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Decode a 16-bit RIFF/WAVE into a freshly malloc'd mono int16 buffer.
|
||||
* Returns frames, or 0 on any failure; *out is set only on success. Shared by
|
||||
* the blocking and async WAV paths. */
|
||||
static int64_t el_wav_load(const char* path, int16_t** out, int32_t* out_sr) {
|
||||
if (!path || !out) return 0;
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
||||
long size = ftell(f);
|
||||
if (size <= 44) { fclose(f); return 0; }
|
||||
rewind(f);
|
||||
unsigned char* d = (unsigned char*)malloc((size_t)size);
|
||||
if (!d) { fclose(f); return 0; }
|
||||
size_t got = fread(d, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(d); return 0; }
|
||||
if (memcmp(d, "RIFF", 4) != 0 || memcmp(d + 8, "WAVE", 4) != 0) { free(d); return 0; }
|
||||
|
||||
int32_t sr = 0, channels = 0, bits = 0;
|
||||
long dataOff = -1, dataLen = 0, o = 12;
|
||||
/* Chunk-walk rather than assuming fmt-then-data at fixed offsets: recorders
|
||||
* routinely interleave JUNK/FLLR padding, and a fixed-offset parser reads
|
||||
* padding as audio. */
|
||||
while (o + 8 <= size) {
|
||||
long sz = (long)d[o+4] | ((long)d[o+5] << 8) | ((long)d[o+6] << 16) | ((long)d[o+7] << 24);
|
||||
if (sz < 0) break;
|
||||
if (memcmp(d + o, "fmt ", 4) == 0 && o + 24 <= size) {
|
||||
channels = (int32_t)(d[o+10] | (d[o+11] << 8));
|
||||
sr = (int32_t)((long)d[o+12] | ((long)d[o+13] << 8) | ((long)d[o+14] << 16) | ((long)d[o+15] << 24));
|
||||
bits = (int32_t)(d[o+22] | (d[o+23] << 8));
|
||||
} else if (memcmp(d + o, "data", 4) == 0) {
|
||||
dataOff = o + 8;
|
||||
dataLen = sz;
|
||||
if (dataOff + dataLen > size) dataLen = size - dataOff;
|
||||
}
|
||||
o += 8 + sz + (sz & 1);
|
||||
}
|
||||
if (dataOff < 0 || sr <= 0 || bits != 16 || channels < 1 || dataLen <= 0) { free(d); return 0; }
|
||||
|
||||
long frames = dataLen / (2 * channels);
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)frames * sizeof(int16_t));
|
||||
if (!pcm) { free(d); return 0; }
|
||||
for (long i = 0; i < frames; i++) {
|
||||
long b = dataOff + i * 2 * channels;
|
||||
pcm[i] = (int16_t)((unsigned)d[b] | ((unsigned)d[b+1] << 8));
|
||||
}
|
||||
free(d);
|
||||
*out = pcm;
|
||||
if (out_sr) *out_sr = sr;
|
||||
return (int64_t)frames;
|
||||
}
|
||||
|
||||
/* Async WAV playback. converse speaks PRE-RENDERED segments and must keep
|
||||
* listening while it does, so it needs the file on the queue without blocking
|
||||
* and needs to be able to stop it mid-buffer. Going through the file rather
|
||||
* than an El [Int] also avoids marshalling a million-element list per segment
|
||||
* for audio the caller never intends to look at. */
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p) return (el_val_t)0;
|
||||
|
||||
el_audio_teardown();
|
||||
|
||||
int32_t sr = 0;
|
||||
int16_t* pcm = NULL;
|
||||
int64_t frames = el_wav_load(p, &pcm, &sr);
|
||||
if (frames <= 0 || !pcm) { free(pcm); return (el_val_t)0; }
|
||||
|
||||
g_aq_pcm = pcm;
|
||||
g_aq_sr = sr;
|
||||
g_aq_state = (ElAqState*)calloc(1, sizeof(ElAqState));
|
||||
if (!g_aq_state) { el_audio_teardown(); return (el_val_t)0; }
|
||||
g_aq_state->pcm = g_aq_pcm;
|
||||
g_aq_state->frames = frames;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sr;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, g_aq_state, NULL, NULL, 0, &g_aq) != noErr || !g_aq) {
|
||||
el_audio_teardown();
|
||||
return (el_val_t)0;
|
||||
}
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
int64_t remain = g_aq_state->frames - g_aq_state->pos;
|
||||
if (remain <= 0) break;
|
||||
AudioQueueBufferRef b = NULL;
|
||||
if (AudioQueueAllocateBuffer(g_aq, EL_AQ_FRAMES * sizeof(int16_t), &b) != noErr) break;
|
||||
int64_t k = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(b->mAudioData, g_aq_state->pcm + g_aq_state->pos, (size_t)k * sizeof(int16_t));
|
||||
b->mAudioDataByteSize = (UInt32)(k * (int64_t)sizeof(int16_t));
|
||||
g_aq_state->pos += k;
|
||||
if (AudioQueueEnqueueBuffer(g_aq, b, 0, NULL) != noErr) break;
|
||||
g_aq_state->inflight++;
|
||||
}
|
||||
if (g_aq_state->inflight == 0) { el_audio_teardown(); return (el_val_t)0; }
|
||||
if (AudioQueueStart(g_aq, NULL) != noErr) { el_audio_teardown(); return (el_val_t)0; }
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Total frames and sample rate of a WAV, without playing it — wav-info, and the
|
||||
* duration converse needs to compute progress through a segment. */
|
||||
el_val_t wav_frames(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)n;
|
||||
}
|
||||
|
||||
el_val_t wav_rate(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(n > 0 ? sr : 0);
|
||||
}
|
||||
|
||||
/* Play a 16-bit mono RIFF/WAVE file. Present because the render already knows
|
||||
* how to write a WAV and a caller may reasonably want to hear one back without
|
||||
* re-rendering it; the parse is deliberately minimal and chunk-walking, so the
|
||||
* JUNK/FLLR padding that recorders emit does not defeat it. */
|
||||
el_val_t speaker_play_wav(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p) return (el_val_t)0;
|
||||
FILE* f = fopen(p, "rb");
|
||||
if (!f) return (el_val_t)0;
|
||||
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return (el_val_t)0; }
|
||||
long size = ftell(f);
|
||||
if (size <= 44) { fclose(f); return (el_val_t)0; }
|
||||
rewind(f);
|
||||
|
||||
unsigned char* d = (unsigned char*)malloc((size_t)size);
|
||||
if (!d) { fclose(f); return (el_val_t)0; }
|
||||
size_t got = fread(d, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(d); return (el_val_t)0; }
|
||||
|
||||
if (memcmp(d, "RIFF", 4) != 0 || memcmp(d + 8, "WAVE", 4) != 0) { free(d); return (el_val_t)0; }
|
||||
|
||||
int32_t sr = 0, channels = 0, bits = 0;
|
||||
long dataOff = -1, dataLen = 0;
|
||||
long o = 12;
|
||||
while (o + 8 <= size) {
|
||||
long sz = (long)d[o+4] | ((long)d[o+5] << 8) | ((long)d[o+6] << 16) | ((long)d[o+7] << 24);
|
||||
if (sz < 0) break;
|
||||
if (memcmp(d + o, "fmt ", 4) == 0 && o + 24 <= size) {
|
||||
channels = (int32_t)(d[o+10] | (d[o+11] << 8));
|
||||
sr = (int32_t)((long)d[o+12] | ((long)d[o+13] << 8) | ((long)d[o+14] << 16) | ((long)d[o+15] << 24));
|
||||
bits = (int32_t)(d[o+22] | (d[o+23] << 8));
|
||||
} else if (memcmp(d + o, "data", 4) == 0) {
|
||||
dataOff = o + 8;
|
||||
dataLen = sz;
|
||||
if (dataOff + dataLen > size) dataLen = size - dataOff;
|
||||
}
|
||||
o += 8 + sz + (sz & 1);
|
||||
}
|
||||
if (dataOff < 0 || sr <= 0 || bits != 16 || channels < 1 || dataLen <= 0) { free(d); return (el_val_t)0; }
|
||||
|
||||
long frames = dataLen / (2 * channels);
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)frames * sizeof(int16_t));
|
||||
if (!pcm) { free(d); return (el_val_t)0; }
|
||||
/* Take channel 0; the organ is mono by design and downmixing would be an
|
||||
* opinion about content this layer is not entitled to have. */
|
||||
for (long i = 0; i < frames; i++) {
|
||||
long b = dataOff + i * 2 * channels;
|
||||
pcm[i] = (int16_t)((unsigned)d[b] | ((unsigned)d[b+1] << 8));
|
||||
}
|
||||
free(d);
|
||||
|
||||
int ok = el_audio_play_raw(pcm, frames, sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(ok ? 1 : 0);
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
/* el_capture_darwin.m — the MICROPHONE and CAMERA realizers. El's afferent
|
||||
* organ on Darwin: the two entry points through which the world gets in.
|
||||
*
|
||||
* WHY THIS FILE EXISTS, AND WHY IT IS A REALIZER RATHER THAN PURE EL.
|
||||
*
|
||||
* el_audio_darwin.m argued the efferent half of this: El can compute a sound
|
||||
* but it cannot make one, because "the DAC has now played these samples" is not
|
||||
* a fact any amount of arithmetic can produce. This file is the same argument
|
||||
* run backwards. El can compute *about* a sound — it can window it, take its
|
||||
* autocorrelation, run Levinson-Durbin over that, find the formant peaks in the
|
||||
* resulting all-pole envelope, and hand back a voiceprint — but it cannot ASK.
|
||||
* There is no expression in El, and there must not be, whose value is "the next
|
||||
* 1024 frames the microphone hears" or "what the camera is pointed at right
|
||||
* now." Those are not computed; they are *requested*, from an operating system
|
||||
* that owns the device, mediates consent for it, and delivers the answer on a
|
||||
* thread of its choosing whenever it feels like it. Asking is the one primitive
|
||||
* operation here. Everything else in this file is bookkeeping around the ask.
|
||||
*
|
||||
* So the line is drawn exactly where el_audio_darwin.m drew it, at the sample
|
||||
* buffer, and it is drawn on purpose:
|
||||
*
|
||||
* BELOW the line (here): open the device, honour the OS permission gate,
|
||||
* install a tap or a frame delegate, convert whatever the hardware happens to
|
||||
* emit into the one shape El asked for, and hand it up. No opinions about
|
||||
* content. No analysis. No decisions.
|
||||
*
|
||||
* ABOVE the line (El): energy, zero-crossing rate, spectral centroid, F0 by
|
||||
* autocorrelation, LPC, formants F1-F5, the compact descriptors, the
|
||||
* scene-geometry grid, the yield-or-hold turn-taking decision. All of it is
|
||||
* arithmetic over a buffer, all of it belongs in El, and none of it appears
|
||||
* below. The reference this file ports — peripheral/src/periph.swift — held
|
||||
* both halves, and that was the problem worth fixing: the descriptors were
|
||||
* trapped in a 939-line binary standing next to the language instead of being
|
||||
* written in it. Porting the *whole* of periph.swift down here would have
|
||||
* reproduced that mistake in C. Only the ask came down.
|
||||
*
|
||||
* The precedent for the file's SHAPE is eg_cosine_batch_strategy_metal_hand.m:
|
||||
* a platform-bound capability compiled as its own translation unit, declared in
|
||||
* el_runtime.h, linked in where the platform supports it, and deliberately NOT
|
||||
* a patch to the middle of el_runtime.c. Acquiring a device must not mean
|
||||
* editing the language, for the same reason acquiring a modality must not (see
|
||||
* the realizer registry: organs are resolved by name). el_peripheral_null.c is
|
||||
* the same entry points everywhere else, so El code that listens still links on
|
||||
* every platform and merely reports having no ear.
|
||||
*
|
||||
* FAIL CLOSED, ALWAYS.
|
||||
*
|
||||
* A capture path that returns plausible-looking zeros when it was denied is
|
||||
* worse than one that returns nothing, because the caller cannot tell the
|
||||
* difference between a silent room and a refused microphone. Every entry point
|
||||
* here checks AVCaptureDevice's authorization status BEFORE touching hardware
|
||||
* and returns the empty value — an empty list, a 0 map — on anything short of
|
||||
* .authorized. mic_available() and camera_available() report that state WITHOUT
|
||||
* prompting, so El can ask "may I?" without the act of asking being a prompt.
|
||||
*
|
||||
* NEVER HANG.
|
||||
*
|
||||
* Every wait in this file is bounded: 30s on a permission prompt (the user has
|
||||
* to walk to the dialog), seconds+5 on a capture of `seconds`, 10s on a camera
|
||||
* frame. An organ that wedges the program holding it is not an organ, it is a
|
||||
* fault. Every path also tears the device down on the way out, including the
|
||||
* failure paths, so a timed-out capture does not leave the mic light on.
|
||||
*
|
||||
* OWN-CORE AND LOCAL BY CONSTRUCTION.
|
||||
*
|
||||
* AVFoundation, CoreVideo, CoreGraphics and ImageIO ship with macOS. There is
|
||||
* no third-party library here, no model, and — the part that matters — no
|
||||
* network path of any kind. Samples and pixels move from local hardware into an
|
||||
* El value and stop. periph.swift had a URLSession in it; this file has no
|
||||
* socket, no URL, and nothing that could grow one without being obvious in
|
||||
* review. Consent is enforced above this layer in El and below it by the OS;
|
||||
* this layer's whole contribution to that is refusing to proceed.
|
||||
*
|
||||
* DISCLOSURE.
|
||||
*
|
||||
* Every actual device touch writes one line to stderr and flushes it, before
|
||||
* the device opens. stderr and not stdout: a program that announces "I am about
|
||||
* to open the microphone" on stdout has corrupted its own output, and the
|
||||
* caller must be able to separate the answer from how it was obtained. One line
|
||||
* per touch, no more — a disclosure rail that spams is a rail people learn to
|
||||
* ignore.
|
||||
*/
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Disclosure ──────────────────────────────────────────────────────────────
|
||||
* One flushed line per real device touch, on stderr. Flushed rather than
|
||||
* buffered so the line reaches the terminal BEFORE the mic light comes on
|
||||
* rather than whenever the buffer happens to drain. */
|
||||
static void el_cap_disclose(const char* what) {
|
||||
fprintf(stderr, " [peripheral] %s\n", what);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
/* ── Permission ──────────────────────────────────────────────────────────────
|
||||
* authorizationStatus is a pure read of the TCC database: it never prompts and
|
||||
* never blocks, which is what lets mic_available()/camera_available() answer
|
||||
* honestly without the question itself becoming an event. requestAccess DOES
|
||||
* prompt, so it lives behind its own entry point and nothing calls it
|
||||
* implicitly. */
|
||||
|
||||
static int el_cap_authorized(AVMediaType media) {
|
||||
@try {
|
||||
return [AVCaptureDevice authorizationStatusForMediaType:media]
|
||||
== AVAuthorizationStatusAuthorized ? 1 : 0;
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int el_cap_device_present(AVMediaType media) {
|
||||
@try {
|
||||
return [AVCaptureDevice defaultDeviceWithMediaType:media] != nil ? 1 : 0;
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prompt once and wait, bounded. 30 seconds is the same budget periph.swift
|
||||
* used: long enough for a human to notice a dialog and decide, short enough
|
||||
* that an unattended run fails rather than parks forever. A timeout is reported
|
||||
* as "not granted", which is the safe reading — we genuinely do not know that
|
||||
* it was. */
|
||||
static int el_cap_request(AVMediaType media) {
|
||||
__block int granted = 0;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
@try {
|
||||
[AVCaptureDevice requestAccessForMediaType:media
|
||||
completionHandler:^(BOOL ok) {
|
||||
granted = ok ? 1 : 0;
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
if (dispatch_semaphore_wait(sem,
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC))) != 0) {
|
||||
return 0; /* timed out — treat as refused */
|
||||
}
|
||||
return granted;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* MICROPHONE — one-shot capture
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* The sink the tap block writes into. An object rather than a static so two
|
||||
* captures can never share state, and so ARC keeps it alive for exactly as long
|
||||
* as the block that captured it. The lock is real, not decorative: the tap runs
|
||||
* on an AVAudioEngine-internal thread and the waiter runs on the caller's. */
|
||||
@interface ElCapMicSink : NSObject
|
||||
@property (nonatomic, strong) NSMutableData* pcm;
|
||||
@property (nonatomic, strong) NSLock* lock;
|
||||
@end
|
||||
|
||||
@implementation ElCapMicSink
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pcm = [NSMutableData data];
|
||||
_lock = [[NSLock alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
/* Capture `seconds` of mono 16-bit PCM at `sample_rate`.
|
||||
*
|
||||
* The hardware format is NOT assumed. A built-in mic will typically hand back
|
||||
* float32 at 44.1 or 48 kHz, an aggregate device may be 8 channels at 96 kHz,
|
||||
* and a caller asking for 16 kHz mono (which is what the formant path wants)
|
||||
* gets 16 kHz mono either way. AVAudioConverter does the rate conversion and
|
||||
* the downmix; doing it by hand would mean writing a resampler in the one file
|
||||
* that is supposed to contain no arithmetic.
|
||||
*
|
||||
* The converter is built ONCE, outside the tap, because a sample-rate converter
|
||||
* carries filter state across buffers — rebuilding it per callback would put a
|
||||
* discontinuity at every buffer boundary, which is audible and which would then
|
||||
* show up in El's spectral descriptors as energy that was never in the room. */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate) {
|
||||
el_val_t empty = el_list_empty();
|
||||
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return empty;
|
||||
|
||||
int64_t secs = (int64_t)seconds;
|
||||
int64_t sr = (int64_t)sample_rate;
|
||||
if (secs <= 0 || sr <= 0) return empty;
|
||||
/* Bound the ask. A caller that asks for a year of audio has made a mistake,
|
||||
* and honouring it would mean an unkillable capture and an OOM. */
|
||||
if (secs > 300) secs = 300;
|
||||
if (sr > 384000) sr = 384000;
|
||||
|
||||
__block AVAudioEngine* engine = nil;
|
||||
AVAudioInputNode* input = nil;
|
||||
AVAudioFormat* hwFmt = nil;
|
||||
int tapped = 0;
|
||||
|
||||
@try {
|
||||
engine = [[AVAudioEngine alloc] init];
|
||||
input = [engine inputNode];
|
||||
hwFmt = [input inputFormatForBus:0];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return empty;
|
||||
}
|
||||
if (!input || !hwFmt || hwFmt.sampleRate <= 0 || hwFmt.channelCount == 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
/* Preferred target: mono int16 at the requested rate. If the converter
|
||||
* refuses that pairing (some exotic input layouts will not downmix), fall
|
||||
* back to keeping the hardware's channel count and taking channel 0 on the
|
||||
* way out — the organ is mono by design and inventing a downmix here would
|
||||
* be an opinion about content this layer is not entitled to have. */
|
||||
AVAudioFormat* outFmt =
|
||||
[[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16
|
||||
sampleRate:(double)sr
|
||||
channels:1
|
||||
interleaved:YES];
|
||||
AVAudioConverter* conv = outFmt ? [[AVAudioConverter alloc] initFromFormat:hwFmt
|
||||
toFormat:outFmt] : nil;
|
||||
AVAudioChannelCount outCh = 1;
|
||||
if (!conv) {
|
||||
outFmt = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16
|
||||
sampleRate:(double)sr
|
||||
channels:hwFmt.channelCount
|
||||
interleaved:YES];
|
||||
conv = outFmt ? [[AVAudioConverter alloc] initFromFormat:hwFmt toFormat:outFmt] : nil;
|
||||
outCh = hwFmt.channelCount;
|
||||
}
|
||||
if (!conv || !outFmt) return empty;
|
||||
|
||||
ElCapMicSink* sink = [[ElCapMicSink alloc] init];
|
||||
const int64_t want = secs * sr; /* frames we are waiting for */
|
||||
|
||||
{
|
||||
char msg[192];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"MIC: opening the microphone for %llds -> %lld Hz mono PCM "
|
||||
"(local, never egresses).", (long long)secs, (long long)sr);
|
||||
el_cap_disclose(msg);
|
||||
}
|
||||
|
||||
const double ratio = (double)sr / hwFmt.sampleRate;
|
||||
|
||||
@try {
|
||||
[input installTapOnBus:0
|
||||
bufferSize:4096
|
||||
format:hwFmt
|
||||
block:^(AVAudioPCMBuffer* _Nonnull buf, AVAudioTime* _Nonnull when) {
|
||||
(void)when;
|
||||
if (!buf || buf.frameLength == 0) return;
|
||||
|
||||
AVAudioFrameCount cap =
|
||||
(AVAudioFrameCount)((double)buf.frameLength * ratio) + 1024;
|
||||
AVAudioPCMBuffer* out =
|
||||
[[AVAudioPCMBuffer alloc] initWithPCMFormat:outFmt frameCapacity:cap];
|
||||
if (!out) return;
|
||||
|
||||
__block BOOL fed = NO;
|
||||
AVAudioConverterInputBlock feed =
|
||||
^AVAudioBuffer* _Nullable (AVAudioPacketCount need,
|
||||
AVAudioConverterInputStatus* _Nonnull status) {
|
||||
(void)need;
|
||||
if (fed) { *status = AVAudioConverterInputStatus_NoDataNow; return nil; }
|
||||
fed = YES;
|
||||
*status = AVAudioConverterInputStatus_HaveData;
|
||||
return buf;
|
||||
};
|
||||
|
||||
NSError* err = nil;
|
||||
AVAudioConverterOutputStatus st =
|
||||
[conv convertToBuffer:out error:&err withInputFromBlock:feed];
|
||||
if (st == AVAudioConverterOutputStatus_Error || out.frameLength == 0) return;
|
||||
|
||||
const int16_t* src = out.int16ChannelData ? out.int16ChannelData[0] : NULL;
|
||||
if (!src) return;
|
||||
|
||||
NSUInteger n = (NSUInteger)out.frameLength;
|
||||
[sink.lock lock];
|
||||
if (outCh == 1) {
|
||||
[sink.pcm appendBytes:src length:n * sizeof(int16_t)];
|
||||
} else {
|
||||
/* Interleaved: stride to channel 0. */
|
||||
for (NSUInteger i = 0; i < n; i++) {
|
||||
int16_t v = src[i * outCh];
|
||||
[sink.pcm appendBytes:&v length:sizeof(int16_t)];
|
||||
}
|
||||
}
|
||||
[sink.lock unlock];
|
||||
}];
|
||||
tapped = 1;
|
||||
|
||||
[engine prepare];
|
||||
NSError* startErr = nil;
|
||||
if (![engine startAndReturnError:&startErr]) {
|
||||
[input removeTapOnBus:0];
|
||||
return empty;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { if (tapped) [input removeTapOnBus:0]; } @catch (NSException* e2) { (void)e2; }
|
||||
@try { [engine stop]; } @catch (NSException* e2) { (void)e2; }
|
||||
return empty;
|
||||
}
|
||||
|
||||
/* Wait for `want` frames, bounded by the material's own duration plus a
|
||||
* margin. A device that stops producing must not become a hang. */
|
||||
const int64_t deadline_us = (secs + 5) * 1000000;
|
||||
int64_t waited_us = 0;
|
||||
const int64_t tick_us = 5000;
|
||||
for (;;) {
|
||||
[sink.lock lock];
|
||||
int64_t have = (int64_t)([sink.pcm length] / sizeof(int16_t));
|
||||
[sink.lock unlock];
|
||||
if (have >= want || waited_us >= deadline_us) break;
|
||||
usleep((useconds_t)tick_us);
|
||||
waited_us += tick_us;
|
||||
}
|
||||
|
||||
@try { [input removeTapOnBus:0]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [engine stop]; } @catch (NSException* e) { (void)e; }
|
||||
engine = nil;
|
||||
|
||||
/* Hand up exactly what was asked for, or everything we got if the device
|
||||
* came up short. Never padded: silence we invented is indistinguishable
|
||||
* from silence we heard, and El has no way to tell them apart afterwards. */
|
||||
[sink.lock lock];
|
||||
int64_t have = (int64_t)([sink.pcm length] / sizeof(int16_t));
|
||||
int64_t n = have < want ? have : want;
|
||||
const int16_t* pcm = (const int16_t*)[sink.pcm bytes];
|
||||
el_val_t list = empty;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
list = el_list_append(list, (el_val_t)(int64_t)pcm[i]);
|
||||
}
|
||||
[sink.lock unlock];
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
el_val_t mic_available(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return (el_val_t)0;
|
||||
return (el_val_t)(el_cap_device_present(AVMediaTypeAudio) ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t mic_request_access(void) {
|
||||
return (el_val_t)(el_cap_request(AVMediaTypeAudio) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* MICROPHONE — live monitor (the full-duplex ear)
|
||||
*
|
||||
* converse needs to keep listening WHILE it speaks, which means the mic is open
|
||||
* at the same time as the speaker. In a real room that is a feedback path:
|
||||
* without cancellation Neuron hears its own voice, decides someone is talking,
|
||||
* and barges in on itself. setVoiceProcessingEnabled: hands the input node to
|
||||
* the OS voice-processing unit, which subtracts the known output signal from
|
||||
* the input — the single thing that makes barge-in work outside a headset.
|
||||
*
|
||||
* It is not always available (some aggregate and virtual devices refuse it), so
|
||||
* failure to enable it is reported as a DISTINCT return value (2) rather than
|
||||
* folded into success. The caller needs to know, because the correct response
|
||||
* is to raise the VAD floor, and a caller that thinks AEC is on will set that
|
||||
* floor far too low.
|
||||
*
|
||||
* The tap keeps only a running short-window RMS in a static behind a mutex.
|
||||
* Deliberately not a queue of samples: this path is polled at ~50 Hz by a loop
|
||||
* that only ever asks "is someone talking", and buffering audio nobody reads
|
||||
* would be an unbounded allocation in the middle of a conversation.
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static AVAudioEngine* g_mon_engine = nil;
|
||||
static int g_mon_running = 0;
|
||||
static int g_mon_code = 0; /* what the successful start reported */
|
||||
static double g_mon_rms = 0.0;
|
||||
static pthread_mutex_t g_mon_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t mic_monitor_start(void) {
|
||||
/* Idempotent, and it re-reports the ORIGINAL code rather than a bare 1: a
|
||||
* caller that starts twice must not be told AEC is on when the first start
|
||||
* already discovered it was not. */
|
||||
if (g_mon_running) return (el_val_t)g_mon_code;
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return (el_val_t)0;
|
||||
|
||||
AVAudioEngine* engine = nil;
|
||||
AVAudioInputNode* input = nil;
|
||||
AVAudioFormat* fmt = nil;
|
||||
int aec = 0;
|
||||
int tapped = 0;
|
||||
|
||||
@try {
|
||||
engine = [[AVAudioEngine alloc] init];
|
||||
input = [engine inputNode];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
if (!input) return (el_val_t)0;
|
||||
|
||||
/* Enable AEC BEFORE reading the format: the voice-processing unit imposes
|
||||
* its own input format, and a tap installed with the pre-VP format would be
|
||||
* rejected at start. */
|
||||
@try {
|
||||
NSError* vpErr = nil;
|
||||
if ([input respondsToSelector:@selector(setVoiceProcessingEnabled:error:)]) {
|
||||
aec = [input setVoiceProcessingEnabled:YES error:&vpErr] ? 1 : 0;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
aec = 0;
|
||||
}
|
||||
|
||||
@try {
|
||||
fmt = [input inputFormatForBus:0];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
if (!fmt || fmt.sampleRate <= 0 || fmt.channelCount == 0) return (el_val_t)0;
|
||||
|
||||
el_cap_disclose(aec
|
||||
? "MIC: opening the microphone for live monitoring, echo-cancelled (local)."
|
||||
: "MIC: opening the microphone for live monitoring, NO echo cancellation (local).");
|
||||
|
||||
@try {
|
||||
[input installTapOnBus:0
|
||||
bufferSize:1024
|
||||
format:fmt
|
||||
block:^(AVAudioPCMBuffer* _Nonnull buf, AVAudioTime* _Nonnull when) {
|
||||
(void)when;
|
||||
if (!buf) return;
|
||||
AVAudioFrameCount n = buf.frameLength;
|
||||
if (n == 0) return;
|
||||
|
||||
double sum = 0.0;
|
||||
/* Whatever the VP unit hands back — float32 is the norm, int16 and
|
||||
* int32 are possible on odd hardware — normalise to -1..1 so the
|
||||
* Float El sees means the same thing on every device. */
|
||||
if (buf.floatChannelData) {
|
||||
const float* ch = buf.floatChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) sum += (double)ch[i] * (double)ch[i];
|
||||
} else if (buf.int16ChannelData) {
|
||||
const int16_t* ch = buf.int16ChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) {
|
||||
double v = (double)ch[i] / 32768.0;
|
||||
sum += v * v;
|
||||
}
|
||||
} else if (buf.int32ChannelData) {
|
||||
const int32_t* ch = buf.int32ChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) {
|
||||
double v = (double)ch[i] / 2147483648.0;
|
||||
sum += v * v;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double rms = sqrt(sum / (double)n);
|
||||
if (rms < 0.0) rms = 0.0;
|
||||
if (rms > 1.0) rms = 1.0;
|
||||
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = rms;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
}];
|
||||
tapped = 1;
|
||||
|
||||
[engine prepare];
|
||||
NSError* startErr = nil;
|
||||
if (![engine startAndReturnError:&startErr]) {
|
||||
[input removeTapOnBus:0];
|
||||
return (el_val_t)0;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { if (tapped) [input removeTapOnBus:0]; } @catch (NSException* e2) { (void)e2; }
|
||||
@try { [engine stop]; } @catch (NSException* e2) { (void)e2; }
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = 0.0;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
|
||||
g_mon_engine = engine;
|
||||
g_mon_running = 1;
|
||||
g_mon_code = aec ? 1 : 2;
|
||||
return (el_val_t)g_mon_code;
|
||||
}
|
||||
|
||||
/* Float in 0..1. Reads the last window the tap computed; never blocks on the
|
||||
* audio thread beyond the mutex, because this is polled inside a turn-taking
|
||||
* loop where a stall IS a missed barge-in. */
|
||||
el_val_t mic_monitor_rms(void) {
|
||||
double rms = 0.0;
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
rms = g_mon_rms;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
return el_from_float(rms);
|
||||
}
|
||||
|
||||
el_val_t mic_monitor_stop(void) {
|
||||
AVAudioEngine* engine = g_mon_engine;
|
||||
g_mon_engine = nil;
|
||||
g_mon_running = 0;
|
||||
g_mon_code = 0;
|
||||
|
||||
if (engine) {
|
||||
@try { [[engine inputNode] removeTapOnBus:0]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [engine stop]; } @catch (NSException* e) { (void)e; }
|
||||
}
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = 0.0;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* CAMERA
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void el_cap_free_bitmap(void* info, const void* data, size_t size) {
|
||||
(void)info; (void)size;
|
||||
free((void*)data);
|
||||
}
|
||||
|
||||
/* CVPixelBuffer -> CGImage, own-core, no CoreImage.
|
||||
*
|
||||
* The output is pinned to 32BGRA at the AVCaptureVideoDataOutput (see below)
|
||||
* precisely so this conversion can be a memcpy and a CGImageCreate. The
|
||||
* alternative — accepting the camera's native 2vuy/420v and colour-converting
|
||||
* here — would mean either pulling in CoreImage or writing a YUV->RGB matrix in
|
||||
* the file that is supposed to contain no arithmetic. Asking the capture output
|
||||
* for BGRA moves that work into AVFoundation, where it is already written and
|
||||
* already hardware-accelerated.
|
||||
*
|
||||
* The rows are copied out rather than aliased because the CVPixelBuffer is
|
||||
* recycled by the capture session the moment the delegate returns; a CGImage
|
||||
* pointing at it would be pointing at the NEXT frame by the time anyone looked. */
|
||||
static CGImageRef el_cap_cgimage_from_pixelbuffer(CVPixelBufferRef pb) {
|
||||
if (!pb) return NULL;
|
||||
if (CVPixelBufferGetPixelFormatType(pb) != kCVPixelFormatType_32BGRA) return NULL;
|
||||
if (CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) return NULL;
|
||||
|
||||
size_t w = CVPixelBufferGetWidth(pb);
|
||||
size_t h = CVPixelBufferGetHeight(pb);
|
||||
size_t src_bpr = CVPixelBufferGetBytesPerRow(pb);
|
||||
const uint8_t* base = (const uint8_t*)CVPixelBufferGetBaseAddress(pb);
|
||||
|
||||
CGImageRef img = NULL;
|
||||
if (base && w > 0 && h > 0 && src_bpr >= w * 4) {
|
||||
size_t dst_bpr = w * 4;
|
||||
uint8_t* copy = (uint8_t*)malloc(dst_bpr * h);
|
||||
if (copy) {
|
||||
for (size_t y = 0; y < h; y++) {
|
||||
memcpy(copy + y * dst_bpr, base + y * src_bpr, dst_bpr);
|
||||
}
|
||||
CGDataProviderRef dp =
|
||||
CGDataProviderCreateWithData(NULL, copy, dst_bpr * h, el_cap_free_bitmap);
|
||||
if (dp) {
|
||||
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
||||
if (cs) {
|
||||
img = CGImageCreate(w, h, 8, 32, dst_bpr, cs,
|
||||
(CGBitmapInfo)(kCGBitmapByteOrder32Little |
|
||||
kCGImageAlphaNoneSkipFirst),
|
||||
dp, NULL, false, kCGRenderingIntentDefault);
|
||||
CGColorSpaceRelease(cs);
|
||||
}
|
||||
CGDataProviderRelease(dp); /* provider owns `copy` from here */
|
||||
} else {
|
||||
free(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly);
|
||||
return img;
|
||||
}
|
||||
|
||||
/* The frame delegate. AVCaptureVideoDataOutput is used rather than
|
||||
* AVCapturePhotoOutput for the same reason periph.swift used it: the photo path
|
||||
* wants KVO and a session owned by an app object, and this runs in a plain CLI
|
||||
* process with no run loop it can assume. A data output just calls back.
|
||||
*
|
||||
* The first frames are dropped on purpose. A camera that has just been powered
|
||||
* on is still converging exposure and white balance, and the first frame is
|
||||
* reliably darker and greener than the room. El's scene-geometry descriptors
|
||||
* are brightness and mean-colour statistics, so handing up an unsettled frame
|
||||
* would not produce a slightly worse answer, it would produce a confidently
|
||||
* wrong one. */
|
||||
@interface ElCapFrameGrabber : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate> {
|
||||
CGImageRef _img;
|
||||
int _seen;
|
||||
dispatch_semaphore_t _sem;
|
||||
}
|
||||
- (dispatch_semaphore_t)sem;
|
||||
- (CGImageRef)takeImage; /* transfers ownership to the caller */
|
||||
@end
|
||||
|
||||
@implementation ElCapFrameGrabber
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_img = NULL;
|
||||
_seen = 0;
|
||||
_sem = dispatch_semaphore_create(0);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (dispatch_semaphore_t)sem { return _sem; }
|
||||
|
||||
- (CGImageRef)takeImage {
|
||||
CGImageRef out = _img;
|
||||
_img = NULL;
|
||||
return out;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_img) { CGImageRelease(_img); _img = NULL; }
|
||||
}
|
||||
|
||||
/* Runs on the serial delegate queue, so no lock is needed among callbacks; the
|
||||
* waiter only reads _img after the semaphore has been signalled AND the session
|
||||
* has been stopped, which orders it after the last callback. */
|
||||
- (void)captureOutput:(AVCaptureOutput*)output
|
||||
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
fromConnection:(AVCaptureConnection*)connection {
|
||||
(void)output; (void)connection;
|
||||
_seen++;
|
||||
if (_img != NULL || _seen < 5) return; /* let exposure settle */
|
||||
|
||||
CVImageBufferRef pb = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
if (!pb) return;
|
||||
CGImageRef img = el_cap_cgimage_from_pixelbuffer(pb);
|
||||
if (!img) return;
|
||||
_img = img;
|
||||
dispatch_semaphore_signal(_sem);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/* Bring the camera up, take exactly one settled frame, put it back down.
|
||||
* Returns a +1 CGImageRef the caller releases, or NULL. Bounded at 10s: a
|
||||
* camera held by another process, or one whose TCC grant was revoked between
|
||||
* the check and the open, must fail rather than park. */
|
||||
static CGImageRef el_cap_grab_frame(void) {
|
||||
AVCaptureSession* session = nil;
|
||||
AVCaptureVideoDataOutput* output = nil;
|
||||
ElCapFrameGrabber* grabber = nil;
|
||||
CGImageRef img = NULL;
|
||||
|
||||
@try {
|
||||
AVCaptureDevice* dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!dev) return NULL;
|
||||
|
||||
NSError* err = nil;
|
||||
AVCaptureDeviceInput* in = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err];
|
||||
if (!in) return NULL;
|
||||
|
||||
session = [[AVCaptureSession alloc] init];
|
||||
session.sessionPreset = AVCaptureSessionPresetPhoto;
|
||||
if (![session canAddInput:in]) return NULL;
|
||||
[session addInput:in];
|
||||
|
||||
output = [[AVCaptureVideoDataOutput alloc] init];
|
||||
output.alwaysDiscardsLateVideoFrames = YES;
|
||||
/* Pin the pixel format so the CGImage conversion above stays a memcpy.
|
||||
* Every macOS capture device advertises 32BGRA. */
|
||||
output.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey :
|
||||
@(kCVPixelFormatType_32BGRA) };
|
||||
|
||||
grabber = [[ElCapFrameGrabber alloc] init];
|
||||
dispatch_queue_t q = dispatch_queue_create("el.capture.camera", DISPATCH_QUEUE_SERIAL);
|
||||
[output setSampleBufferDelegate:grabber queue:q];
|
||||
|
||||
if (![session canAddOutput:output]) return NULL;
|
||||
[session addOutput:output];
|
||||
|
||||
el_cap_disclose("CAMERA: opening the camera for one frame (local, never egresses).");
|
||||
[session startRunning];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { [session stopRunning]; } @catch (NSException* e2) { (void)e2; }
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long timed_out = dispatch_semaphore_wait([grabber sem],
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)));
|
||||
|
||||
/* Stop first, then detach the delegate, then read. In that order the last
|
||||
* callback has already returned by the time anyone touches the image. */
|
||||
@try { [session stopRunning]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [output setSampleBufferDelegate:nil queue:NULL]; } @catch (NSException* e) { (void)e; }
|
||||
|
||||
if (timed_out == 0) img = [grabber takeImage];
|
||||
return img;
|
||||
}
|
||||
|
||||
el_val_t camera_available(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
return (el_val_t)(el_cap_device_present(AVMediaTypeVideo) ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t camera_request_access(void) {
|
||||
return (el_val_t)(el_cap_request(AVMediaTypeVideo) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* Longest edge of the grid handed to El. 64 is not a resolution, it is a budget:
|
||||
* a 1920x1080 frame is 6.2 MILLION packed RGB ints, and building that as an El
|
||||
* list would cost more time and memory than everything El then does with it.
|
||||
* The descriptors El computes over this — mean colour, brightness, a 3x3
|
||||
* luminance grid — are region statistics, and region statistics do not get
|
||||
* meaningfully better above a 64-wide grid. The TRUE frame dimensions are
|
||||
* reported separately so nothing downstream has to guess what was thrown away. */
|
||||
#define EL_CAP_GRID_MAX 64
|
||||
|
||||
/* One frame as Map{width, height, grid_w, grid_h, pixels:[Int]}.
|
||||
*
|
||||
* "pixels" is packed R,G,B with NO alpha — three ints per grid cell, row-major
|
||||
* from the TOP-LEFT. (CGBitmapContext lays its buffer out top row first and
|
||||
* CGContextDrawImage does the flip, so row 0 here is the top of the frame, the
|
||||
* same convention periph.swift's grid indexing assumed.) Alpha is dropped
|
||||
* because a camera frame has none worth carrying and it would inflate the list
|
||||
* by a third to say "opaque" six thousand times. */
|
||||
el_val_t camera_capture_rgb(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
|
||||
CGImageRef img = el_cap_grab_frame();
|
||||
if (!img) return (el_val_t)0;
|
||||
|
||||
size_t w = CGImageGetWidth(img);
|
||||
size_t h = CGImageGetHeight(img);
|
||||
if (w == 0 || h == 0) { CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
/* Preserve aspect ratio, longest edge capped. */
|
||||
size_t gw = w, gh = h;
|
||||
size_t longest = w > h ? w : h;
|
||||
if (longest > EL_CAP_GRID_MAX) {
|
||||
double s = (double)EL_CAP_GRID_MAX / (double)longest;
|
||||
gw = (size_t)((double)w * s + 0.5);
|
||||
gh = (size_t)((double)h * s + 0.5);
|
||||
if (gw == 0) gw = 1;
|
||||
if (gh == 0) gh = 1;
|
||||
}
|
||||
|
||||
size_t bpr = gw * 4;
|
||||
uint8_t* buf = (uint8_t*)calloc(1, bpr * gh);
|
||||
if (!buf) { CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef ctx = cs ? CGBitmapContextCreate(buf, gw, gh, 8, bpr, cs,
|
||||
(CGBitmapInfo)kCGImageAlphaPremultipliedLast)
|
||||
: NULL;
|
||||
if (cs) CGColorSpaceRelease(cs);
|
||||
if (!ctx) { free(buf); CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
/* Nearest-neighbour. This is a decimation for statistics, not a thumbnail
|
||||
* for a human to look at; smoothing would only cost time and blur the very
|
||||
* region boundaries the grid exists to measure. */
|
||||
CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);
|
||||
CGContextDrawImage(ctx, CGRectMake(0, 0, (CGFloat)gw, (CGFloat)gh), img);
|
||||
CGContextRelease(ctx);
|
||||
CGImageRelease(img);
|
||||
|
||||
el_val_t pixels = el_list_empty();
|
||||
for (size_t y = 0; y < gh; y++) {
|
||||
const uint8_t* row = buf + y * bpr;
|
||||
for (size_t x = 0; x < gw; x++) {
|
||||
const uint8_t* p = row + x * 4; /* RGBA8, premultiplied-last */
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[0]);
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[1]);
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[2]);
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
|
||||
el_val_t m = el_map_new((el_val_t)0);
|
||||
if (!m) return (el_val_t)0;
|
||||
m = el_map_set(m, EL_STR("width"), (el_val_t)(int64_t)w);
|
||||
m = el_map_set(m, EL_STR("height"), (el_val_t)(int64_t)h);
|
||||
m = el_map_set(m, EL_STR("grid_w"), (el_val_t)(int64_t)gw);
|
||||
m = el_map_set(m, EL_STR("grid_h"), (el_val_t)(int64_t)gh);
|
||||
m = el_map_set(m, EL_STR("pixels"), pixels);
|
||||
return m;
|
||||
}
|
||||
|
||||
/* One frame to disk as JPEG, at FULL resolution — the opposite budget from
|
||||
* camera_capture_rgb, and for the opposite reason. A file is not being walked
|
||||
* element-by-element by an interpreter; it costs one ImageIO call and it is the
|
||||
* artefact a human or a later pass will actually look at. The encoder is
|
||||
* ImageIO's because a JPEG encoder is a codec, and re-implementing one in El
|
||||
* would be a large amount of arithmetic that buys nothing: the point of keeping
|
||||
* work in El is the reasoning, not the entropy coding. */
|
||||
el_val_t camera_capture_jpeg(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p || !*p) return (el_val_t)0;
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
|
||||
CGImageRef img = el_cap_grab_frame();
|
||||
if (!img) return (el_val_t)0;
|
||||
|
||||
int ok = 0;
|
||||
@autoreleasepool {
|
||||
NSString* ns = [NSString stringWithUTF8String:p];
|
||||
NSURL* url = ns ? [NSURL fileURLWithPath:ns] : nil;
|
||||
if (url) {
|
||||
CGImageDestinationRef dst =
|
||||
CGImageDestinationCreateWithURL((__bridge CFURLRef)url, CFSTR("public.jpeg"), 1, NULL);
|
||||
if (dst) {
|
||||
CGImageDestinationAddImage(dst, img, NULL);
|
||||
ok = CGImageDestinationFinalize(dst) ? 1 : 0;
|
||||
CFRelease(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
CGImageRelease(img);
|
||||
return (el_val_t)ok;
|
||||
}
|
||||
|
||||
#endif /* __APPLE__ */
|
||||
@@ -0,0 +1,89 @@
|
||||
/* el_peripheral_null.c — the no-device build of El's I/O organ.
|
||||
*
|
||||
* Every entry point declared in el_runtime.h's "Peripheral" block, implemented
|
||||
* as an honest refusal. This is what a platform without an El audio/capture
|
||||
* realizer links instead of el_audio_darwin.m + el_capture_darwin.m, so an El
|
||||
* program that speaks or listens still COMPILES AND LINKS everywhere.
|
||||
*
|
||||
* The distinction that matters: these do not pretend. speaker_available() and
|
||||
* mic_available() return 0, and every operation returns its failure sentinel.
|
||||
* A program asking "can I speak here?" gets a truthful no, rather than a
|
||||
* silence it would have to infer something from. Silent success is the failure
|
||||
* mode this whole change exists to eliminate — El spent this entire codebase's
|
||||
* history writing WAV files full of zeros and reporting ok=true, and nobody
|
||||
* caught it because nothing ever said "there is no sound here".
|
||||
*
|
||||
* Compiled INSTEAD OF the Darwin realizers, never alongside them — the symbols
|
||||
* are the same by design, which is the point: the El side never branches on
|
||||
* platform, it branches on speaker_available().
|
||||
*/
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
/* ── Speaker ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t speaker_available(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_name(void) { return EL_STR("none"); }
|
||||
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate) {
|
||||
(void)samples; (void)sample_rate;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_wav(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate) {
|
||||
(void)samples; (void)sample_rate;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_pause(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_resume(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_playing(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_stop(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_played_frames(void) { return (el_val_t)0; }
|
||||
|
||||
/* WAV geometry is pure parsing and would work fine here, but reporting a
|
||||
* duration for audio this build cannot play would invite a caller to sequence
|
||||
* around a silence. Refuse consistently with the rest of the file. */
|
||||
el_val_t wav_frames(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
el_val_t wav_rate(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
|
||||
/* ── Microphone ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t mic_available(void) { return (el_val_t)0; }
|
||||
el_val_t mic_request_access(void) { return (el_val_t)0; }
|
||||
|
||||
/* Empty list, not 0: the contract says capture returns samples, and a caller
|
||||
* iterating the result must find nothing rather than dereference a non-list. */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate) {
|
||||
(void)seconds; (void)sample_rate;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t mic_monitor_start(void) { return (el_val_t)0; }
|
||||
el_val_t mic_monitor_rms(void) { return el_from_float(0.0); }
|
||||
el_val_t mic_monitor_stop(void) { return (el_val_t)0; }
|
||||
|
||||
/* ── Camera ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t camera_available(void) { return (el_val_t)0; }
|
||||
el_val_t camera_request_access(void) { return (el_val_t)0; }
|
||||
el_val_t camera_capture_rgb(void) { return (el_val_t)0; }
|
||||
|
||||
el_val_t camera_capture_jpeg(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
#endif /* !__APPLE__ */
|
||||
+220
-116
@@ -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
|
||||
* 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) {
|
||||
if (v == 0) return 0;
|
||||
int64_t s = (int64_t)v;
|
||||
@@ -722,6 +736,12 @@ static int looks_like_heap_obj(el_val_t v) {
|
||||
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) {
|
||||
if (!looks_like_heap_obj(v)) return;
|
||||
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
|
||||
* 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_vindex.h" /* M8: ANN (HNSW) index for activation seed selection */
|
||||
#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
|
||||
* would have caught this in a day instead of two months.
|
||||
*
|
||||
* 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. */
|
||||
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;
|
||||
}
|
||||
* The SIGNATURE itself (eg_text_loss_signature) moved to engram_text.c on
|
||||
* 2026-08-16 — it is plain C over <ctype.h> and touches nothing in here. The
|
||||
* stock/flow gauges below stay, because they touch store and EL value types. */
|
||||
|
||||
/* Damaged-node creations since process start. See the block comment above. */
|
||||
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
|
||||
* binary actually builds against.) */
|
||||
#define ENGRAM_MAX_QTOKENS 32
|
||||
#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. */
|
||||
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;
|
||||
}
|
||||
/* ENGRAM_QTOK_LEN and engram_tokenize_query moved to engram_text.h/.c. */
|
||||
|
||||
/* Count how many of the ntok distinct query tokens appear (case-insensitive)
|
||||
* 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);
|
||||
/* 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);
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -16389,29 +16418,6 @@ el_val_t engram_label_df(el_val_t term) {
|
||||
#define ENGRAM_ST_TOKLEN 64
|
||||
#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
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* 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);
|
||||
}
|
||||
|
||||
/* engram_boundary_beat(op_name) — the decorated-fn boundary AUTO-EMIT (VBD seam).
|
||||
* codegen injects a single call to this at the entry of every @manager/@accessor
|
||||
* decorated fn, so a decorated op self-reports with ZERO hand-written
|
||||
* instrumentation in its body:
|
||||
/* engram_boundary_beat(op_name, construct) — decorated-fn boundary AUTO-EMIT
|
||||
* (VBD seam). codegen injects a single call to this at the entry of every
|
||||
* @manager/@accessor decorated fn, so a decorated op self-reports with ZERO
|
||||
* hand-written instrumentation in its body:
|
||||
* (1) afferent counter++ — the boundary was crossed
|
||||
* (2) engram_chrono_tick() — interoception: the mind senses its own op firing
|
||||
* (3) engram_strengthen(self-anchor)— reinforce the self-activity anchor (an
|
||||
* activation-count/salience bump, NOT a content/edge write — identity
|
||||
* write-protection is untouched)
|
||||
* (4) dharma_emit(neuron.op.<name>) — provenance on the shared bus transport
|
||||
* (same bus the swarm peers field on); bumps _eg_dharma_emits. */
|
||||
el_val_t engram_boundary_beat(el_val_t op_name) {
|
||||
* (same bus the swarm peers field on); bumps _eg_dharma_emits.
|
||||
*
|
||||
* `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++;
|
||||
engram_chrono_tick();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* passed in as int-cast values). 4096 is a safe lower bound on any
|
||||
* platform we target. */
|
||||
if ((uintptr_t)p < 4096) return 0;
|
||||
const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t));
|
||||
/* Reads BACKWARD, so the HEADER address is what must be validated -- and a
|
||||
* 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;
|
||||
*out_len = hdr->length;
|
||||
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) {
|
||||
size_t 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) ──────────────────────────────── */
|
||||
|
||||
@@ -80,6 +80,92 @@ void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* stderr counterpart of println (defined in el_seed.c). El could write to
|
||||
* stdout and nowhere else, which is right for a program's RESULT and wrong for
|
||||
* everything about how that result was produced. Disclosure especially has to
|
||||
* leave on a stream the caller can separate from the answer: a program that
|
||||
* announces "I am about to open the microphone" on stdout has corrupted its own
|
||||
* output. Flushed on every call, so a disclosure reaches the terminal BEFORE
|
||||
* the device it describes is touched rather than whenever the buffer drains. */
|
||||
void eprintln(el_val_t s);
|
||||
|
||||
/* ── Peripheral: the speaker, the microphone, the camera ─────────────────────
|
||||
*
|
||||
* El's I/O organ. Implemented per platform in its OWN translation unit —
|
||||
* el_audio_darwin.m / el_capture_darwin.m on Darwin, el_peripheral_null.c
|
||||
* everywhere else — so El code that speaks or listens links on every platform
|
||||
* and merely reports having no device where there isn't one. Declared here and
|
||||
* deliberately NOT implemented in el_runtime.c: acquiring a device must not
|
||||
* mean editing the middle of the language, the same rule the realizer registry
|
||||
* follows for modalities.
|
||||
*
|
||||
* These are the ONLY parts of the organ that are not El. Everything above the
|
||||
* sample buffer — WAV encode/decode, LPC autocorrelation, Levinson-Durbin,
|
||||
* formant extraction, source-filter resynthesis, the compact descriptors, the
|
||||
* converse decision loop — is arithmetic, and arithmetic belongs in El. What
|
||||
* remains here is what El cannot express: handing a buffer to the DAC and
|
||||
* waiting for it to drain, and asking the OS for frames off a capture device.
|
||||
*
|
||||
* Local by construction: none of these entry points has a network path. Samples
|
||||
* and pixels go to and from local hardware and nowhere else. Consent is
|
||||
* enforced ABOVE this layer in El (peripheral/src/organ.el) for the Neuron-level
|
||||
* grant, and BELOW it by the OS for TCC; capture fails closed on either. */
|
||||
|
||||
/* Speaker (efferent). speaker_play_pcm16 BLOCKS until the audio has actually
|
||||
* been played rather than merely queued, so a caller can sequence utterances
|
||||
* without guessing durations and without clipping each tail. */
|
||||
el_val_t speaker_available(void); /* 1 if a real speaker backs this build */
|
||||
el_val_t speaker_name(void); /* backend id, e.g. "coreaudio-audioqueue" */
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate); /* [Int] 16-bit mono; 1 ok */
|
||||
el_val_t speaker_play_wav(el_val_t path); /* 16-bit mono RIFF/WAVE; 1 ok */
|
||||
|
||||
/* Asynchronous playback — required by converse, which must keep listening while
|
||||
* it speaks and must be able to stop ON THE SPOT mid-buffer. A blocking play
|
||||
* cannot be interrupted, and "finish the current buffer" is not barge-in.
|
||||
* speaker_stop() halts output immediately; speaker_playing() reports whether
|
||||
* the hardware is still going; speaker_played_frames() is how far it actually
|
||||
* got, which is what makes an interrupted utterance resumable at the sample. */
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate);
|
||||
el_val_t speaker_play_wav_async(el_val_t path);
|
||||
el_val_t speaker_pause(void); /* stop AT THIS SAMPLE, keep position */
|
||||
el_val_t speaker_resume(void); /* carry on from exactly there */
|
||||
el_val_t speaker_playing(void);
|
||||
el_val_t speaker_stop(void);
|
||||
el_val_t speaker_played_frames(void);
|
||||
|
||||
/* WAV geometry without playing — wav-info, and the segment duration converse
|
||||
* needs to turn elapsed time into progress. */
|
||||
el_val_t wav_frames(el_val_t path);
|
||||
el_val_t wav_rate(el_val_t path);
|
||||
|
||||
/* Microphone (afferent). Fails CLOSED: returns 0 unless the OS has granted
|
||||
* capture access. mic_capture_pcm16 blocks for `seconds` and returns an [Int]
|
||||
* of 16-bit mono samples at `sample_rate` — the raw stream is handed to El and
|
||||
* never written anywhere by this layer. mic_available() reports device +
|
||||
* permission state without prompting. */
|
||||
el_val_t mic_available(void); /* 1 device present AND OS-authorized */
|
||||
el_val_t mic_request_access(void); /* prompt once; 1 if granted */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate); /* [Int], empty on refusal */
|
||||
|
||||
/* Live monitoring for full-duplex converse. mic_monitor_start enables the OS
|
||||
* voice-processing unit (acoustic echo cancellation) so the microphone does not
|
||||
* hear the speaker — without AEC, Neuron barges in on its own voice and
|
||||
* turn-taking is unusable in a real room. mic_monitor_rms returns the current
|
||||
* short-window RMS as a Float in 0..1. */
|
||||
el_val_t mic_monitor_start(void); /* 1 ok; 2 = started but AEC unavailable */
|
||||
el_val_t mic_monitor_rms(void); /* Float */
|
||||
el_val_t mic_monitor_stop(void);
|
||||
|
||||
/* Camera (afferent). Fails CLOSED like the microphone. camera_capture_rgb
|
||||
* returns a Map with width/height and the frame as an [Int] of packed RGB
|
||||
* bytes, so the descriptor arithmetic can happen in El rather than here.
|
||||
* camera_capture_jpeg writes an encoded frame via ImageIO, which is a codec and
|
||||
* not something El should re-implement. */
|
||||
el_val_t camera_available(void);
|
||||
el_val_t camera_request_access(void);
|
||||
el_val_t camera_capture_rgb(void); /* Map{width,height,pixels:[Int]} or 0 */
|
||||
el_val_t camera_capture_jpeg(el_val_t path); /* 1 ok */
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
@@ -801,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_chrono_persist_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_drift_json(void);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
|
||||
+130
-3
@@ -154,9 +154,18 @@ static void seed_request_start(void) {
|
||||
* file still links on its own. */
|
||||
__attribute__((weak)) void el_str_cache_flush(void);
|
||||
|
||||
/* Byte-buffer capacity registry (defined below, next to the string
|
||||
* primitives). The arena frees the pointers it tracked, so any capacity
|
||||
* entry for those addresses must go with them — otherwise a later malloc
|
||||
* reusing the address would inherit a stale width. */
|
||||
static void seed_cap_drop(const char* p);
|
||||
|
||||
static void seed_request_end(void) {
|
||||
_seed_arena_on = 0;
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]);
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) {
|
||||
seed_cap_drop(_seed_arena.ptrs[i]);
|
||||
free(_seed_arena.ptrs[i]);
|
||||
}
|
||||
_seed_arena.count = 0;
|
||||
if (el_str_cache_flush) el_str_cache_flush(); /* freed pointers may be reused */
|
||||
}
|
||||
@@ -188,6 +197,114 @@ static char* seed_strbuf(size_t n) {
|
||||
|
||||
static el_val_t seed_wrap_str(char* s) { return EL_STR(s); }
|
||||
|
||||
/* ── Byte-buffer capacity registry ────────────────────────────────────────────
|
||||
* A String produced by __str_alloc is a fixed-size BYTE BUFFER, not text. Its
|
||||
* length is the capacity it was asked for; strlen() is meaningless on it,
|
||||
* because the buffer is zero-filled and binary content (PCM audio, RIFF
|
||||
* headers, image rasters) contains NUL bytes by nature.
|
||||
*
|
||||
* Before this registry existed, __str_set_char bounds-checked the write index
|
||||
* against strlen(p). For a freshly __str_alloc'd buffer strlen(p) == 0, so the
|
||||
* check `idx >= len` rejected EVERY index and the function was a total no-op:
|
||||
* every El program that built bytes this way wrote a file of pure zeros and
|
||||
* still saw a success return. That is why El's own-core WAV writer emitted
|
||||
* 55,244 silent bytes with a correct-looking header length and no header.
|
||||
*
|
||||
* The fix cannot be "trust the index", because that removes the bound. It also
|
||||
* cannot be a length header stored behind the pointer, because __str_set_char
|
||||
* accepts any String — including a string literal in .rodata, where reading the
|
||||
* bytes preceding the pointer is undefined and may fault. So capacity is kept
|
||||
* in a side table keyed by the pointer itself: allocation registers, the arena
|
||||
* sweep unregisters, and anything not registered keeps the exact strlen
|
||||
* behaviour it had before. Text semantics are unchanged; byte buffers gain the
|
||||
* bound they always should have had. */
|
||||
|
||||
typedef struct {
|
||||
char* ptr; /* NULL = empty slot, (char*)1 = tombstone */
|
||||
size_t cap;
|
||||
} SeedCapEntry;
|
||||
|
||||
#define SEED_CAP_TOMB ((char*)1)
|
||||
|
||||
static _Thread_local SeedCapEntry* _seed_cap = NULL;
|
||||
static _Thread_local size_t _seed_cap_mask = 0; /* table size - 1 */
|
||||
static _Thread_local size_t _seed_cap_used = 0; /* live + tombstoned */
|
||||
|
||||
static size_t seed_cap_hash(const char* p) {
|
||||
uintptr_t h = (uintptr_t)p >> 4; /* malloc alignment: low bits are dead */
|
||||
h *= (uintptr_t)0x9E3779B97F4A7C15ull;
|
||||
return (size_t)(h >> 32);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap);
|
||||
|
||||
static void seed_cap_grow(void) {
|
||||
size_t old_size = _seed_cap_mask ? _seed_cap_mask + 1 : 0;
|
||||
SeedCapEntry* old = _seed_cap;
|
||||
size_t new_size = old_size ? old_size * 2 : 256;
|
||||
SeedCapEntry* fresh = calloc(new_size, sizeof(SeedCapEntry));
|
||||
if (!fresh) return; /* out of memory: keep old table */
|
||||
_seed_cap = fresh;
|
||||
_seed_cap_mask = new_size - 1;
|
||||
_seed_cap_used = 0;
|
||||
for (size_t i = 0; i < old_size; i++) {
|
||||
if (old[i].ptr && old[i].ptr != SEED_CAP_TOMB) seed_cap_put(old[i].ptr, old[i].cap);
|
||||
}
|
||||
free(old);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap) {
|
||||
if (!p) return;
|
||||
if (!_seed_cap || (_seed_cap_used + 1) * 4 >= (_seed_cap_mask + 1) * 3) {
|
||||
seed_cap_grow();
|
||||
if (!_seed_cap) return;
|
||||
}
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
size_t first_free = (size_t)-1;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (e == p) { _seed_cap[i].cap = cap; return; } /* address reused */
|
||||
if (e == SEED_CAP_TOMB && first_free == (size_t)-1) first_free = i;
|
||||
if (!e) {
|
||||
if (first_free != (size_t)-1) i = first_free; else _seed_cap_used++;
|
||||
_seed_cap[i].ptr = p;
|
||||
_seed_cap[i].cap = cap;
|
||||
return;
|
||||
}
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Capacity of a registered byte buffer, or -1 when the pointer is not one. */
|
||||
static int64_t seed_cap_get(const char* p) {
|
||||
if (!p || !_seed_cap) return -1;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return -1;
|
||||
if (e == (char*)p) return (int64_t)_seed_cap[i].cap;
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
static void seed_cap_drop(const char* p) {
|
||||
if (!p || !_seed_cap) return;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return;
|
||||
if (e == (char*)p) { _seed_cap[i].ptr = SEED_CAP_TOMB; return; }
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Effective addressable length of a String: its buffer capacity when it is a
|
||||
* byte buffer, otherwise strlen. */
|
||||
static int64_t seed_addressable_len(const char* p) {
|
||||
int64_t cap = seed_cap_get(p);
|
||||
return cap >= 0 ? cap : (int64_t)strlen(p);
|
||||
}
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s) {
|
||||
@@ -199,7 +316,7 @@ el_val_t __str_len(el_val_t s) {
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i) {
|
||||
const char* p = EL_CSTR(s);
|
||||
if (!p) return 0;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return 0;
|
||||
return (el_val_t)(unsigned char)p[idx];
|
||||
@@ -210,13 +327,14 @@ el_val_t __str_alloc(el_val_t n) {
|
||||
if (sz < 0) sz = 0;
|
||||
char* buf = seed_strbuf((size_t)sz);
|
||||
memset(buf, 0, (size_t)sz + 1);
|
||||
seed_cap_put(buf, (size_t)sz); /* this is a byte buffer of width sz */
|
||||
return seed_wrap_str(buf);
|
||||
}
|
||||
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
|
||||
char* p = (char*)(uintptr_t)s;
|
||||
if (!p) return s;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return s;
|
||||
p[idx] = (char)(unsigned char)(int64_t)c;
|
||||
@@ -406,6 +524,15 @@ el_val_t __fs_mkdir(el_val_t path) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* stderr counterpart of println. Flushed immediately: a disclosure line is only
|
||||
* worth anything if it lands before the thing it discloses happens. */
|
||||
void eprintln(el_val_t s) {
|
||||
const char* p = EL_CSTR(s);
|
||||
fputs(p ? p : "", stderr);
|
||||
fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) {
|
||||
const char* p = EL_CSTR(path);
|
||||
const char* b = EL_CSTR(bytes);
|
||||
|
||||
@@ -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 */
|
||||
+13
-2
@@ -460,7 +460,8 @@ The `@` token followed by an identifier attaches a decorator to the next `FnDef`
|
||||
| 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`, `@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. |
|
||||
|
||||
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.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:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```c
|
||||
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
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 ─────────────────────
|
||||
@@ -259,22 +261,28 @@ test "lex-multiline-source" {
|
||||
assert tok_kind(tokens, 0) == "Let", "first token is Let"
|
||||
}
|
||||
|
||||
test "lex-flat-stride-2-layout" {
|
||||
// Verify that the flat stride-2 layout: token i has kind at index 2*i, value at 2*i+1
|
||||
test "lex-flat-stride-3-layout" {
|
||||
// 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")
|
||||
// tokens[0] = "Fn", tokens[1] = "fn", tokens[2] = "Ident", tokens[3] = "foo", ...
|
||||
let raw_len: Int = native_list_len(tokens)
|
||||
assert raw_len == 6, "fn + foo + Eof = 3 tokens = 6 raw entries"
|
||||
let kind0: String = native_list_get(tokens, 0)
|
||||
let val0: String = native_list_get(tokens, 1)
|
||||
let kind1: String = native_list_get(tokens, 2)
|
||||
let val1: String = native_list_get(tokens, 3)
|
||||
assert kind0 == "Fn", "raw[0] is Fn kind"
|
||||
assert val0 == "fn", "raw[1] is fn value"
|
||||
assert kind1 == "Ident", "raw[2] is Ident kind"
|
||||
assert val1 == "foo", "raw[3] is foo value"
|
||||
assert raw_len == 9, "fn + foo + Eof = 3 tokens = 9 raw entries"
|
||||
assert native_list_get(tokens, 0) == "Fn", "raw[0] is the kind"
|
||||
assert native_list_get(tokens, 1) == "fn", "raw[1] is the value"
|
||||
assert native_list_get(tokens, 2) == "1", "raw[2] is the line"
|
||||
assert native_list_get(tokens, 3) == "Ident", "raw[3] is the next kind"
|
||||
assert native_list_get(tokens, 5) == "1", "still line 1"
|
||||
}
|
||||
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_first_stmt_kind(src: String) -> String {
|
||||
@@ -726,3 +734,264 @@ test "compiler-stdint-include" {
|
||||
let out: String = compile_capture(src)
|
||||
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"
|
||||
+165
-61
@@ -1,80 +1,184 @@
|
||||
# peripheral — Neuron's I/O organ (own-core, local, consent-gated)
|
||||
# peripheral — Neuron's I/O organ, in El
|
||||
|
||||
The interface made physical. Two afferent senses in, one efferent voice out —
|
||||
all reached the way the agentic surface reaches any tool.
|
||||
**El speaks.** The engram stores geometry and does not speak; the speaking
|
||||
belongs to the language and its runtime.
|
||||
|
||||
Until this landed, the organ was a 939-line Swift program (`src/periph.swift`)
|
||||
that shelled out to `afplay`. Neuron's mouth and ears were a separate binary
|
||||
standing next to the language, and "speak" meant "ask that binary to speak."
|
||||
That program is now **reference material, not the implementation.**
|
||||
|
||||
```
|
||||
MIC (hear) afferent device -> capture -> descriptor -> ingest -> geometry
|
||||
CAMERA (see) afferent device -> capture -> descriptor -> ingest -> scene-geometry
|
||||
SPEAKER(speak) efferent render WAV -> PLAY ALOUD out the speaker
|
||||
SPEAKER (speak) efferent samples ──────────────► CoreAudio ──► the room
|
||||
MIC (hear) afferent device ──► samples ──► descriptor ──► engram
|
||||
CAMERA (see) afferent device ──► frame ──► descriptor ──► engram
|
||||
```
|
||||
|
||||
Closes the conversational loop: **hear (mic) -> understand (engram) -> speak (speaker)**.
|
||||
## The split, and why it falls where it does
|
||||
|
||||
Exactly **two** things here are not El, and they are the two things El cannot
|
||||
express as arithmetic:
|
||||
|
||||
| Not El (realizers) | Why |
|
||||
|---|---|
|
||||
| `lang/runtime/el_audio_darwin.m` | Handing a buffer to the DAC and waiting for it to drain. There is no way to say "the hardware has now played these samples" in El, and there should not be. |
|
||||
| `lang/runtime/el_capture_darwin.m` | Asking the OS for samples off a microphone or frames off a camera, plus the TCC permission dance. |
|
||||
|
||||
**Everything else is El**, because everything else is arithmetic:
|
||||
|
||||
| In El | Where |
|
||||
|---|---|
|
||||
| WAV encode / decode (chunk-walking, JUNK/FLLR tolerant) | `src/organ_dsp.el`, `elp/src/speech.el` |
|
||||
| LPC autocorrelation + Levinson-Durbin (order 16 @ 16 kHz) | `src/organ_dsp.el` |
|
||||
| Formant extraction off the all-pole spectral envelope | `src/organ_dsp.el` |
|
||||
| Source-filter resynthesis (glottal impulse train through the filter) | `src/organ_dsp.el` |
|
||||
| Audio descriptor `[seconds, sr, ch, rms, peak, zcr, centroid, F0]` | `src/organ_dsp.el` |
|
||||
| Voice descriptor `[F0, F1..F5, bandwidths]` | `src/organ_dsp.el` |
|
||||
| Scene descriptor `[w, h, meanRGB, brightness, 3×3 luminance grid]` | `src/organ.el` |
|
||||
| Consent, disclosure, the voice-from-engram fetch | `src/organ.el` |
|
||||
| Barge-in, yield-or-hold, backchannel, resume | `src/organ_converse.el` |
|
||||
| The command surface | `src/organ_cli.el` |
|
||||
|
||||
Both realizers are their **own translation units**, declared in
|
||||
`lang/runtime/el_runtime.h`, and deliberately **not** patches to
|
||||
`el_runtime.c`. Acquiring a device must not mean editing the middle of the
|
||||
language — the same rule the realizer registry follows for modalities.
|
||||
`lang/runtime/el_peripheral_null.c` provides the identical entry points
|
||||
everywhere else, so El that speaks links on any platform and truthfully reports
|
||||
having no speaker rather than going quietly silent.
|
||||
|
||||
## The voice comes from the engram
|
||||
|
||||
A voice is **geometry in the engram**, not a JSON file next to the code and
|
||||
certainly not constants in a source file. The organ fetches it the way anything
|
||||
retrieves a memory — it asks:
|
||||
|
||||
```el
|
||||
let g: [Int] = organ_voice_fetch("will")
|
||||
// [peripheral] VOICE: fetched 'will' FROM THE ENGRAM —
|
||||
// f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531
|
||||
```
|
||||
|
||||
`organ_voice_fetch` issues an engram query and reads the geometry off the node
|
||||
that comes back. Nothing opens a file. If the region is not in the graph it
|
||||
returns **empty**, not a plausible default — a caller has to be able to tell
|
||||
"this is how they sound" from "I never heard them."
|
||||
|
||||
The reverse direction is `ingest-voice`: an LPC voiceprint becomes a node, and
|
||||
from then on the voice is a memory rather than a measurement someone wrote down.
|
||||
|
||||
## What the organ never does
|
||||
|
||||
**It never learns a word.** Pronunciation, vocabulary and phonemes belong to the
|
||||
language faculty and are already built as ingested geometry — *the engram knows
|
||||
how to pronounce*. The seam is `synth_codes(codes, voice, pmap)`: the codes and
|
||||
the phoneme map arrive from the language side as geometry, and the organ's whole
|
||||
job is turning them into samples and getting the samples out the speaker, plus
|
||||
the same trip in reverse for the senses. There is no lexicon here and no
|
||||
grapheme-to-phoneme rule, by design.
|
||||
|
||||
## Rails
|
||||
- **Own-core.** macOS-native only: AVFoundation (camera/mic), CoreAudio voice-
|
||||
processing (AEC), afplay (speaker), ImageIO/CoreGraphics (frames), hand-rolled
|
||||
DSP (WAV, LPC, formant synthesis). No cloud, no heavy deps.
|
||||
- **Local-only.** Raw streams are written to `out/` and never egress. `.gitignore`
|
||||
keeps captured media out of git.
|
||||
- **Consent-gated (two locks).** A Neuron-level grant (`grant`/`revoke`) *and* the
|
||||
OS TCC permission. Sensitive senses (camera/mic) fail closed without both.
|
||||
- **Disclosed.** Every device touch prints a `[peripheral]` line on stderr.
|
||||
|
||||
- **Own-core.** CoreAudio / AVFoundation / ImageIO — all ship with macOS. No
|
||||
cloud, no model, no heavy dependency. There is **no network code in the organ
|
||||
at all**, by construction.
|
||||
- **Local-only.** Raw streams stay on the machine. What leaves a capture is a
|
||||
descriptor of a few dozen numbers. A 1920×1080 frame becomes 15 integers
|
||||
(~414,000× smaller); three seconds of audio becomes 8.
|
||||
- **Consent, two locks.** A Neuron-level grant **and** the OS TCC permission.
|
||||
Camera and mic **fail closed** without both. The speaker is disclosed but not
|
||||
gated — you cannot secretly speak aloud, and gating it would mean Neuron needs
|
||||
permission to answer.
|
||||
- **Disclosed.** Every device touch prints a `[peripheral]` line on **stderr**
|
||||
(via `eprintln`, flushed immediately), so a disclosure lands before the device
|
||||
is touched and never contaminates the program's stdout.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./peripheral/build.sh /tmp/organ
|
||||
```
|
||||
swiftc -O -o bin/periph src/periph.swift \
|
||||
-framework AVFoundation -framework CoreMedia -framework Foundation \
|
||||
-framework CoreGraphics -framework ImageIO -framework CoreImage
|
||||
```
|
||||
|
||||
Concatenates the El modules, compiles with `elc`, links the two realizers.
|
||||
Run it **from the repo root** or the `.psv` phoneme data will not resolve.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
periph grant|revoke <camera|mic> # Neuron-level consent
|
||||
periph status
|
||||
periph speak <file.wav> # SPEAK ALOUD (efferent)
|
||||
periph tone <out.wav> [hz] [sec] # own-core WAV synth
|
||||
periph listen <sec> <out.wav> # MIC capture (afferent), 16k mono
|
||||
periph see <out.jpg> # CAMERA one frame (afferent)
|
||||
periph feat-audio <wav> | feat-image <jpg> # capture -> compact descriptor
|
||||
periph ingest-audio|ingest-image <file> <engramURL> # descriptor -> engram node (geometry)
|
||||
periph voiceprint <voice.wav> # extract F0 + formants F1-F5
|
||||
periph imitate <voice.wav> <out.wav> # speak back in that voice (LPC resynthesis)
|
||||
periph hear-imitate <sec> <out.wav> # MIC -> signature -> imitate -> SPEAK ALOUD
|
||||
periph converse <manifest.json> [--authority F] [--barge-at S[:backchannel|:bargein]] [--resume] [--live-mic]
|
||||
organ grant|revoke <camera|mic> Neuron-level consent
|
||||
organ status consent + device state
|
||||
organ speak <file.wav> play a WAV aloud (efferent)
|
||||
organ tone [hz] [ms] synthesize and play — no file at all
|
||||
organ say <voice> <CODE> [CODE...] fetch voice FROM THE ENGRAM, render, speak
|
||||
organ listen <sec> <out.wav> mic capture 16k mono (afferent)
|
||||
organ see <out.jpg> one camera frame (afferent)
|
||||
organ wav-info <file.wav> WAV geometry
|
||||
organ feat-audio <file.wav> compact audio descriptor (8 numbers)
|
||||
organ feat-image compact scene-geometry from the camera
|
||||
organ voiceprint <voice.wav> F0 + formants F1-F5 (LPC)
|
||||
organ imitate <in.wav> <out.wav> LPC analysis-resynthesis
|
||||
organ hear-imitate <sec> <out.wav> mic -> signature -> imitate -> speak aloud
|
||||
organ ingest-audio <file.wav> descriptor -> engram node (geometry)
|
||||
organ ingest-voice <voice.wav> <n> voiceprint -> engram voice region
|
||||
organ converse <manifest.json> [--authority PM] [--barge-at MS[:kind]] [--live-mic] [--resume]
|
||||
```
|
||||
|
||||
## The afferent metabolism
|
||||
A capture is never shipped raw. It becomes a **compact descriptor** — the afferent
|
||||
twin of the music instrument-signature:
|
||||
- audio -> `[seconds, sr, ch, rms, peak, zcr, centroid, F0]` (~2400-6000x smaller)
|
||||
- image -> `[w, h, meanRGB, brightness, 3x3 luminance grid]` (~400000x smaller)
|
||||
- voice -> `[F0, F1..F5, bandwidths]` (11 numbers)
|
||||
## Interruptibility
|
||||
|
||||
That descriptor is what the ingest organ (engram `POST /api/nodes`) turns into an
|
||||
embedded node = geometry.
|
||||
`converse` speaks an ordered, salience-tagged **meaning-plan** while listening:
|
||||
|
||||
## Voice by imitation
|
||||
`voiceprint`/`imitate` are own-core LPC (autocorrelation + Levinson-Durbin, order
|
||||
16 @ 16 kHz), formant extraction from the LPC spectral envelope, and source-filter
|
||||
resynthesis (glottal impulse train at F0 through the all-pole formant filter). A
|
||||
voice is grabbed by ear as ~a dozen numbers and spoken back — **no training, no
|
||||
stolen voice.** Measured fidelity on real speech: resynthesized formants match the
|
||||
source within 2-3%. The full phoneme->formant path for *novel* sentences is the
|
||||
speech faculty's seam (`elp` audio surface profile); this engine provides the
|
||||
formant synthesis primitive it renders through.
|
||||
- **barge-in** — output stops at the sample, not at the end of the buffer. The
|
||||
realizer exposes `pause`/`resume` and reports `played_frames` (the real DAC
|
||||
position) precisely so this is possible.
|
||||
- **yield-or-hold** — a decision, not a rule: `hold = salience·0.6 +
|
||||
progress·0.4`, and holding also requires that the interrupter not be
|
||||
high-authority. Otherwise yield, because the polite default is the right one.
|
||||
- **backchannel** — "mm-hm" is brief and low-energy; resume seamlessly.
|
||||
- **resumable** — on yield the remaining plan persists to `.resume.json`;
|
||||
`--resume` picks the thread back up. An interruption should cost a turn, not
|
||||
the content.
|
||||
|
||||
## Interruptibility (native turn-taking)
|
||||
`converse` plays the utterance as an ordered, salience-tagged **meaning-plan**
|
||||
while the mic listens (full-duplex, AEC on so it never barges in on its own voice):
|
||||
- **barge-in**: user speech -> pause on the spot (sample-accurate), not "finish the buffer."
|
||||
- **yield-or-hold**: a decision grounded in the current segment's salience + progress
|
||||
+ the interrupter's authority — YIELD (stop) or HOLD ("hang on, let me finish").
|
||||
- **backchannel** ("mm-hm"): brief/low -> keep going, resume seamlessly.
|
||||
- **resumable**: on yield the remaining plan persists (`.resume.json`); `--resume`
|
||||
picks the thread back up ("as I was saying").
|
||||
Live full-duplex uses `--live-mic` with the OS voice-processing unit (AEC) so
|
||||
Neuron does not barge in on its own voice. `--barge-at` injects the event
|
||||
deterministically for testing.
|
||||
|
||||
Live full-duplex uses `--live-mic` (OS AEC). Injected `--barge-at` drives the
|
||||
decision loop deterministically for testing.
|
||||
```
|
||||
```
|
||||
## Measured against the Swift original
|
||||
|
||||
Same input (`out/mic_room.wav`, 16 kHz mono, 48121 samples), Swift `periph`
|
||||
vs the El organ:
|
||||
|
||||
| | Swift | El |
|
||||
|---|---|---|
|
||||
| seconds | 3.0075625 | 3.0076 |
|
||||
| rms | 0.0047766496761 | 0.004777 |
|
||||
| peak | 0.01806640625 | 0.018066 |
|
||||
| zcr_hz | 416.28395087 | 416.2840 |
|
||||
| centroid_hz | 727.60529169 | 727.6053 |
|
||||
| f0_hz | 400 | 400.0000 |
|
||||
| formants F1–F5 | 1734.375 / 3343.75 / 3875 / 4359.375 / 4468.75 | identical |
|
||||
| bandwidths B1–B5 | 2000 / 2968.75 / 4203.125 / 4687.5 / 5000 | identical |
|
||||
|
||||
Agreement to every printed digit. `imitate` cannot match bit-for-bit because the
|
||||
Swift excites unvoiced frames with `Double.random` — two Swift runs correlate
|
||||
0.957 with **each other**; El correlates **0.958** with Swift. The port is as
|
||||
close to the original as the original is to itself, and the deterministic prefix
|
||||
is bit-identical.
|
||||
|
||||
## Honest status
|
||||
|
||||
- **Works:** speaker (CoreAudio, no `afplay`, no subprocess — verified: zero
|
||||
`afplay`/Swift strings in the binary, no child process during playback), mic
|
||||
capture, camera capture, all descriptors, LPC voiceprint, imitate,
|
||||
hear-imitate, voice fetch/ingest against the engram, converse (yield, hold,
|
||||
yield-to-authority, backchannel, resume — all exercised with real audio).
|
||||
- **Coarse, and labelled so:** a fetched voice is one formant triple with no
|
||||
coarticulation and no prosody. It is an impression, explicitly **not a
|
||||
clone**, and `prov=COARSE` says so on the node.
|
||||
- **Not verified here:** live `--live-mic` barge-in in a real room with a real
|
||||
interrupter. The AEC path is implemented and the deterministic path is proven;
|
||||
the acoustic behaviour is not something a headless run can establish.
|
||||
- **Not in the engram yet:** the structured `Voice` / `VowelTarget` geometry
|
||||
nodes live in the organ's own store and in snapshot files from earlier work,
|
||||
but the **production engram does not carry them**. Getting them there is an
|
||||
ingest, not a code change.
|
||||
- `src/periph.swift` is kept as the reference the port was measured against.
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build the El organ.
|
||||
#
|
||||
# El has no import system on this path, so the modules are concatenated in
|
||||
# dependency order (the same thing elp/tests/run.sh does) and handed to elc as
|
||||
# one unit. The two device realizers are then linked in.
|
||||
#
|
||||
# MUST be run from the repo root, or the .psv phoneme geometry will not resolve
|
||||
# and the render silently degrades.
|
||||
set -uo pipefail
|
||||
|
||||
OUT="${1:-./peripheral/organ}"
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# Dependency order. The elp modules supply the render (synth_codes) and the
|
||||
# phoneme-geometry read; the organ supplies everything else.
|
||||
cat elp/src/voice-profile.el \
|
||||
elp/src/accent.el \
|
||||
elp/src/voice-ingest.el \
|
||||
elp/src/speech-ingest.el \
|
||||
elp/src/speech.el \
|
||||
peripheral/src/organ.el \
|
||||
peripheral/src/organ_dsp.el \
|
||||
peripheral/src/organ_converse.el \
|
||||
peripheral/src/organ_cli.el \
|
||||
| grep -v '^import ' > "$WORK/organ.el"
|
||||
|
||||
cd "$REPO/lang"
|
||||
./dist/platform/elc "$WORK/organ.el" > "$WORK/organ.c" || { echo "elc failed" >&2; exit 1; }
|
||||
|
||||
SSL_PREFIX="$(brew --prefix openssl@3 2>/dev/null || echo /usr/local)"
|
||||
|
||||
# The peripheral realizers are per-platform: Darwin gets the real devices,
|
||||
# anything else gets el_peripheral_null.c and honestly reports having none.
|
||||
case "$(uname)" in
|
||||
Darwin)
|
||||
# The Objective-C realizers are compiled SEPARATELY, with -fobjc-arc. The
|
||||
# capture realizer is written against ARC (it holds AVFoundation objects);
|
||||
# compiling it MRR silently changes its memory semantics, which on a device
|
||||
# path shows up as a use-after-free under load rather than as an error here.
|
||||
cc -std=c11 -fobjc-arc -O1 -I runtime -c runtime/el_audio_darwin.m -o "$WORK/el_audio.o" || exit 1
|
||||
cc -std=c11 -fobjc-arc -O1 -I runtime -c runtime/el_capture_darwin.m -o "$WORK/el_capture.o" || exit 1
|
||||
PERIPH_SRC="$WORK/el_audio.o $WORK/el_capture.o"
|
||||
PERIPH_LIBS="-framework AudioToolbox -framework AVFoundation -framework CoreMedia
|
||||
-framework CoreVideo -framework CoreGraphics -framework ImageIO
|
||||
-framework Foundation"
|
||||
;;
|
||||
*)
|
||||
PERIPH_SRC="runtime/el_peripheral_null.c"
|
||||
PERIPH_LIBS=""
|
||||
;;
|
||||
esac
|
||||
|
||||
cc -O1 -I runtime -I"$SSL_PREFIX/include" -L"$SSL_PREFIX/lib" \
|
||||
-o "$OUT" "$WORK/organ.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 \
|
||||
$PERIPH_SRC $PERIPH_LIBS \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm || { echo "link failed" >&2; exit 1; }
|
||||
|
||||
echo "built: $OUT"
|
||||
@@ -0,0 +1,570 @@
|
||||
// organ.el — Neuron's I/O organ, in El.
|
||||
//
|
||||
// THE PRINCIPLE. El speaks. The engram stores geometry and does not speak.
|
||||
// Before this file the organ was a 939-line Swift program standing next to the
|
||||
// language (peripheral/src/periph.swift): Neuron's mouth and ears were a
|
||||
// separate binary, and "speak" meant "shell out to that binary, which shells
|
||||
// out to afplay." That is not a voice, it is a subprocess. The voice belongs to
|
||||
// the language and its runtime.
|
||||
//
|
||||
// THE SPLIT. Exactly two things here are not El, and they are the two things El
|
||||
// cannot express as arithmetic:
|
||||
//
|
||||
// the speaker — handing a buffer to the DAC and waiting for it to drain
|
||||
// the capture — asking the OS for samples off a mic or frames off a camera
|
||||
//
|
||||
// Those live in lang/runtime/el_audio_darwin.m and el_capture_darwin.m, as
|
||||
// their own translation units, declared in el_runtime.h. Everything ELSE that
|
||||
// the Swift did — WAV encode and decode, LPC autocorrelation, Levinson-Durbin,
|
||||
// formant extraction off the all-pole envelope, source-filter resynthesis, the
|
||||
// compact descriptors, the converse yield-or-hold decision — is arithmetic, and
|
||||
// arithmetic is El's. See organ_dsp.el for that half.
|
||||
//
|
||||
// WHERE THE VOICE COMES FROM. Not from this file, and not from a JSON manifest
|
||||
// on disk. A voice is GEOMETRY IN THE ENGRAM, and the organ goes and gets it by
|
||||
// asking the engram, the same way anything else asks the engram for anything:
|
||||
// a query against the graph, then read the numbers off the node that comes
|
||||
// back. organ_voice_fetch is that. The previous path, load_voice("...json"),
|
||||
// parsed a file — which quietly made the voice a build artifact instead of a
|
||||
// memory. If the region is not in the graph, the honest answer is an empty
|
||||
// result, not a default voice.
|
||||
//
|
||||
// WHAT THE ORGAN NEVER DOES. It never learns a word. Pronunciation, vocabulary
|
||||
// and phonemes are the language faculty's, already built as ingested geometry —
|
||||
// "the engram knows how to pronounce." The seam is synth_codes(codes, voice,
|
||||
// pmap): the codes and the phoneme map arrive from the language side as
|
||||
// geometry, and the organ's whole job is turning them into samples and getting
|
||||
// the samples out the speaker, plus the same trip in reverse for the senses.
|
||||
//
|
||||
// RAILS, all non-negotiable:
|
||||
// own-core — CoreAudio / AVFoundation / ImageIO, all shipped with the OS.
|
||||
// No cloud, no model, no heavy dependency. There is no network
|
||||
// call anywhere in the organ, by construction.
|
||||
// local-only — raw streams stay on the machine. What leaves a capture is a
|
||||
// DESCRIPTOR of a few dozen numbers, never the stream.
|
||||
// consent — two locks on the sensitive senses: a Neuron-level grant AND
|
||||
// the OS TCC permission. Camera and mic FAIL CLOSED without
|
||||
// both. The speaker is disclosed but not gated (see below).
|
||||
// disclosed — every device touch prints a [peripheral] line on stderr.
|
||||
// Nothing here is ever silent about being a device.
|
||||
|
||||
// ── Disclosure ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// stderr, not stdout: a program that announces "I am opening the microphone" on
|
||||
// stdout has corrupted its own output. And flushed immediately, so the line is
|
||||
// on the terminal BEFORE the device is touched — a disclosure that arrives
|
||||
// after the fact is a log, not a disclosure.
|
||||
|
||||
fn organ_disclose(msg: String) -> Bool {
|
||||
eprintln(" [peripheral] " + msg)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Consent, the Neuron-level lock ───────────────────────────────────────────
|
||||
//
|
||||
// The OS has its own lock (TCC) and it is not enough on its own: TCC grants the
|
||||
// TERMINAL access to the microphone, once, more or less forever. That says the
|
||||
// user trusts the app. It does not say the user consents to THIS program
|
||||
// listening THIS time. So Neuron keeps its own grant, revocable, on the same
|
||||
// footing — and both must be open for a sensitive sense to work.
|
||||
//
|
||||
// Stored next to the organ rather than in the engram deliberately: consent must
|
||||
// be inspectable and revocable without a running graph, and a permission that
|
||||
// can only be revoked by the system it governs is not a permission.
|
||||
|
||||
fn organ_consent_path() -> String {
|
||||
let home: String = env("PERIPH_HOME")
|
||||
if str_eq(home, "") {
|
||||
return "peripheral/.consent.json"
|
||||
}
|
||||
return home + "/.consent.json"
|
||||
}
|
||||
|
||||
fn organ_consent_granted(device: String) -> Bool {
|
||||
let raw: String = fs_read(organ_consent_path())
|
||||
if str_eq(raw, "") {
|
||||
return false
|
||||
}
|
||||
// A device is granted only on an explicit true. Anything unparseable,
|
||||
// missing or malformed reads as NOT granted — the failure direction for a
|
||||
// permission file is always closed.
|
||||
let key: String = "\"" + device + "\""
|
||||
let at: Int = str_index_of(raw, key)
|
||||
if at < 0 {
|
||||
return false
|
||||
}
|
||||
let tail: String = str_slice(raw, at, str_len(raw))
|
||||
let t: Int = str_index_of(tail, "true")
|
||||
let f: Int = str_index_of(tail, "false")
|
||||
if t < 0 {
|
||||
return false
|
||||
}
|
||||
if f < 0 {
|
||||
return true
|
||||
}
|
||||
// whichever token appears first after the key is this device's value
|
||||
if t < f {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_consent_write(camera: Bool, mic: Bool) -> Bool {
|
||||
let c: String = "false"
|
||||
if camera {
|
||||
c = "true"
|
||||
}
|
||||
let m: String = "false"
|
||||
if mic {
|
||||
m = "true"
|
||||
}
|
||||
return fs_write(organ_consent_path(), "{\"camera\": " + c + ", \"mic\": " + m + "}\n")
|
||||
}
|
||||
|
||||
fn organ_grant(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = true
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = true
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("granted '" + device + "' (Neuron-level) — raw stream stays local, never egresses.")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_revoke(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = false
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = false
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("revoked '" + device + "' (Neuron-level).")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_consent_status() -> String {
|
||||
let cam: String = "denied"
|
||||
if organ_consent_granted("camera") {
|
||||
cam = "granted"
|
||||
}
|
||||
let mic: String = "denied"
|
||||
if organ_consent_granted("mic") {
|
||||
mic = "granted"
|
||||
}
|
||||
return "camera=" + cam + " mic=" + mic
|
||||
}
|
||||
|
||||
// Both locks, in order, with a disclosure for each outcome. Returns false and
|
||||
// says exactly which lock is shut — a refusal that does not say why is
|
||||
// indistinguishable from a bug.
|
||||
fn organ_may_listen() -> Bool {
|
||||
if organ_consent_granted("mic") == false {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (Neuron-level). Run: organ grant mic")
|
||||
return false
|
||||
}
|
||||
if mic_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (OS/TCC), or no input device. Grant microphone access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'mic' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
fn organ_may_see() -> Bool {
|
||||
if organ_consent_granted("camera") == false {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (Neuron-level). Run: organ grant camera")
|
||||
return false
|
||||
}
|
||||
if camera_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (OS/TCC), or no capture device. Grant camera access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'camera' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── SPEAKER (efferent) ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Not consent-gated, and that is a deliberate asymmetry rather than an
|
||||
// oversight. The microphone and camera take information OFF the user without
|
||||
// them necessarily knowing; the speaker puts information INTO a room the user
|
||||
// is in, audibly, which is self-disclosing by its nature — you cannot secretly
|
||||
// speak aloud. So the speaker is DISCLOSED (every utterance announces itself on
|
||||
// stderr) but not gated. Gating it would mean Neuron needs permission to answer.
|
||||
|
||||
fn organ_speak_samples(samples: [Int], sr: Int) -> Bool {
|
||||
let n: Int = native_list_len(samples)
|
||||
if n <= 0 {
|
||||
organ_disclose("SPEAKER: nothing to say (0 samples) — not touching the device.")
|
||||
return false
|
||||
}
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build (" + speaker_name() + ") — cannot speak.")
|
||||
return false
|
||||
}
|
||||
let secs: Int = n * 1000 / sr
|
||||
organ_disclose("SPEAKER: playing " + int_to_str(n) + " samples (" + int_to_str(secs) + " ms @ " + int_to_str(sr) + " Hz) ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_pcm16(samples, sr)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_speak_wav(path: String) -> Bool {
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build — cannot speak.")
|
||||
return false
|
||||
}
|
||||
if fs_exists(path) == false {
|
||||
organ_disclose("SPEAKER: no such file: " + path)
|
||||
return false
|
||||
}
|
||||
organ_disclose("SPEAKER: playing '" + path + "' ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_wav(path)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
// ── The voice, fetched FROM THE ENGRAM ───────────────────────────────────────
|
||||
//
|
||||
// This is the part that matters most and is easiest to get subtly wrong. A
|
||||
// voice is not a constant in code and it is not a JSON file next to the code —
|
||||
// it is a region of the graph, put there by having heard someone, and the organ
|
||||
// retrieves it the way anything retrieves a memory: by asking.
|
||||
//
|
||||
// The node content is the geometry, in the engram's own flat key=value form:
|
||||
// voice will | f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531 ...
|
||||
// so the read is: query the graph, take the returned node, pull the numbers off
|
||||
// it. Nothing here opens a file.
|
||||
//
|
||||
// Returns [f0, f0_end, kf, f1, f2, f3], or an EMPTY list when the region is not
|
||||
// in the graph. Empty is the honest answer — a caller that gets no voice must
|
||||
// not be handed a plausible default and left unable to tell the difference
|
||||
// between "this is how they sound" and "I never heard them."
|
||||
|
||||
// Read an unsigned integer that follows `key` in `s`. Stops at the first
|
||||
// non-digit, returns 0 when the key is absent.
|
||||
fn organ_int_after(s: String, key: String) -> Int {
|
||||
let at: Int = str_index_of(s, key)
|
||||
if at < 0 {
|
||||
return 0
|
||||
}
|
||||
let i: Int = at + str_len(key)
|
||||
let n: Int = str_len(s)
|
||||
let v: Int = 0
|
||||
let seen: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c < 48 {
|
||||
i = n
|
||||
} else {
|
||||
if c > 57 {
|
||||
i = n
|
||||
} else {
|
||||
v = v * 10 + (c - 48)
|
||||
seen = seen + 1
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if seen == 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Ask the engram for a named voice region and read its geometry back.
|
||||
fn organ_voice_fetch(name: String) -> [Int] {
|
||||
let out: [Int] = native_list_empty()
|
||||
let marker: String = "voice " + name + " |"
|
||||
// The graph is asked by MEANING, not by id or by path.
|
||||
let hits: String = engram_search_json("voice " + name + " f0 formants", 12)
|
||||
let at: Int = str_index_of(hits, marker)
|
||||
if at < 0 {
|
||||
// Fall back to a scan of the resident graph before giving up: search is
|
||||
// geometric and a small graph may not rank the region first.
|
||||
let scan: String = engram_scan_nodes_json(500, 0)
|
||||
at = str_index_of(scan, marker)
|
||||
if at < 0 {
|
||||
organ_disclose("VOICE: no region for '" + name + "' in the engram — nothing to speak with.")
|
||||
return out
|
||||
}
|
||||
hits = scan
|
||||
}
|
||||
let win: String = str_slice(hits, at, at + 240)
|
||||
out = native_list_append(out, organ_int_after(win, "f0="))
|
||||
out = native_list_append(out, organ_int_after(win, "f0_end="))
|
||||
out = native_list_append(out, organ_int_after(win, "kf="))
|
||||
out = native_list_append(out, organ_int_after(win, "f1="))
|
||||
out = native_list_append(out, organ_int_after(win, "f2="))
|
||||
out = native_list_append(out, organ_int_after(win, "f3="))
|
||||
organ_disclose("VOICE: fetched '" + name + "' FROM THE ENGRAM — f0=" + int_to_str(native_list_get(out, 0)) + " f0_end=" + int_to_str(native_list_get(out, 1)) + " kf=" + int_to_str(native_list_get(out, 2)) + " f1=" + int_to_str(native_list_get(out, 3)) + " f2=" + int_to_str(native_list_get(out, 4)) + " f3=" + int_to_str(native_list_get(out, 5)))
|
||||
return out
|
||||
}
|
||||
|
||||
// Put a measured voice INTO the engram as geometry. This is the afferent end of
|
||||
// the same wire: a voiceprint (organ_dsp.el's LPC analysis) becomes a node, and
|
||||
// from then on the voice is a memory rather than a measurement someone happened
|
||||
// to write down. `prov` carries the honesty: COARSE means one formant triple, no
|
||||
// coarticulation, no prosody — an impression, explicitly not a clone.
|
||||
fn organ_voice_ingest(name: String, f0: Int, f0_end: Int, kf: Int, f1: Int, f2: Int, f3: Int, src: String, prov: String) -> String {
|
||||
let hub: String = engram_node("voice-signature-set " + name + " grounding=measured src=" + src, "VoiceSet", 90)
|
||||
let body: String = "voice " + name + " | f0=" + int_to_str(f0) + " f0_end=" + int_to_str(f0_end) + " kf=" + int_to_str(kf) + " f1=" + int_to_str(f1) + " f2=" + int_to_str(f2) + " f3=" + int_to_str(f3) + " grounding=measured src=" + src + " prov=" + prov
|
||||
let vid: String = engram_node(body, "Voice", 90)
|
||||
engram_connect(hub, vid, 90, "has-signature")
|
||||
organ_disclose("VOICE: ingested '" + name + "' into the engram as geometry (node " + vid + ").")
|
||||
return vid
|
||||
}
|
||||
|
||||
// Turn the fetched geometry into the voice slot-map the render consumes. Kept
|
||||
// separate from the fetch so the organ never invents a voice: if the fetch came
|
||||
// back empty this returns empty too, and the caller has to deal with it.
|
||||
//
|
||||
// The slot-map is built here rather than by calling the render's own
|
||||
// constructor, so the organ carries NO dependency on the language faculty's
|
||||
// modules — it only has to agree with them about a wire format, which is the
|
||||
// looser and more honest coupling. (The layout is the same key/value [String]
|
||||
// convention lang_get / surface_get / voice_get all read.)
|
||||
fn organ_voice_profile(name: String, g: [Int]) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
if native_list_len(g) < 6 {
|
||||
return r
|
||||
}
|
||||
r = native_list_append(r, "name")
|
||||
r = native_list_append(r, name)
|
||||
r = native_list_append(r, "f0")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 0)))
|
||||
r = native_list_append(r, "f0_end")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 1)))
|
||||
r = native_list_append(r, "kf")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 2)))
|
||||
r = native_list_append(r, "dur")
|
||||
r = native_list_append(r, "1000")
|
||||
r = native_list_append(r, "tilt")
|
||||
r = native_list_append(r, "1000")
|
||||
r = native_list_append(r, "breath")
|
||||
r = native_list_append(r, "8")
|
||||
return r
|
||||
}
|
||||
|
||||
// ── Scene geometry (afferent, camera) ────────────────────────────────────────
|
||||
//
|
||||
// The image half of the afferent metabolism, and the same principle as the
|
||||
// audio descriptor: a frame is never handed on raw. The realizer returns a
|
||||
// small pixel grid; THIS computes the descriptor, in El, because averaging
|
||||
// pixels is arithmetic and arithmetic is not a device concern.
|
||||
//
|
||||
// Returns 15 numbers — [w, h, meanR, meanG, meanB, brightness_pm, and a 3x3
|
||||
// luminance grid] — standing in for a multi-megapixel frame. The 3x3 grid is
|
||||
// the smallest thing that still says WHERE the light is, which is most of what
|
||||
// makes a scene comparable to another scene; a single brightness average would
|
||||
// make a lamp on the left indistinguishable from a lamp on the right.
|
||||
//
|
||||
// Luminance is Rec. 601 (0.299R + 0.587G + 0.114B), in integer per-mille, so
|
||||
// the descriptor is reproducible rather than subject to float drift.
|
||||
fn organ_image_descriptor() -> [Int] {
|
||||
let out: [Int] = native_list_empty()
|
||||
let frame: Any = camera_capture_rgb()
|
||||
if frame == 0 {
|
||||
return out
|
||||
}
|
||||
let w: Int = el_map_get(frame, "width")
|
||||
let h: Int = el_map_get(frame, "height")
|
||||
let gw: Int = el_map_get(frame, "grid_w")
|
||||
let gh: Int = el_map_get(frame, "grid_h")
|
||||
let px: [Int] = el_map_get(frame, "pixels")
|
||||
let np: Int = native_list_len(px)
|
||||
if np < 3 {
|
||||
return out
|
||||
}
|
||||
let count: Int = np / 3
|
||||
let rsum: Int = 0
|
||||
let gsum: Int = 0
|
||||
let bsum: Int = 0
|
||||
// 3x3 accumulators, row-major
|
||||
let cell: [Int] = native_list_empty()
|
||||
let cn: [Int] = native_list_empty()
|
||||
let z: Int = 0
|
||||
while z < 9 {
|
||||
cell = native_list_append(cell, 0)
|
||||
cn = native_list_append(cn, 0)
|
||||
z = z + 1
|
||||
}
|
||||
// El has no list-set, so the cells are summed into parallel scalars and
|
||||
// reassembled — nine explicit accumulators would be worse to read than one
|
||||
// pass per cell over a grid this small.
|
||||
let c0: Int = 0
|
||||
let c1: Int = 0
|
||||
let c2: Int = 0
|
||||
let c3: Int = 0
|
||||
let c4: Int = 0
|
||||
let c5: Int = 0
|
||||
let c6: Int = 0
|
||||
let c7: Int = 0
|
||||
let c8: Int = 0
|
||||
let n0: Int = 0
|
||||
let n1: Int = 0
|
||||
let n2: Int = 0
|
||||
let n3: Int = 0
|
||||
let n4: Int = 0
|
||||
let n5: Int = 0
|
||||
let n6: Int = 0
|
||||
let n7: Int = 0
|
||||
let n8: Int = 0
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let r: Int = native_list_get(px, i * 3)
|
||||
let g: Int = native_list_get(px, i * 3 + 1)
|
||||
let b: Int = native_list_get(px, i * 3 + 2)
|
||||
rsum = rsum + r
|
||||
gsum = gsum + g
|
||||
bsum = bsum + b
|
||||
let lum: Int = (299 * r + 587 * g + 114 * b) / 1000
|
||||
let x: Int = i - (i / gw) * gw
|
||||
let y: Int = i / gw
|
||||
let cx: Int = x * 3 / gw
|
||||
let cy: Int = y * 3 / gh
|
||||
if cx > 2 {
|
||||
cx = 2
|
||||
}
|
||||
if cy > 2 {
|
||||
cy = 2
|
||||
}
|
||||
let idx: Int = cy * 3 + cx
|
||||
if idx == 0 {
|
||||
c0 = c0 + lum
|
||||
n0 = n0 + 1
|
||||
}
|
||||
if idx == 1 {
|
||||
c1 = c1 + lum
|
||||
n1 = n1 + 1
|
||||
}
|
||||
if idx == 2 {
|
||||
c2 = c2 + lum
|
||||
n2 = n2 + 1
|
||||
}
|
||||
if idx == 3 {
|
||||
c3 = c3 + lum
|
||||
n3 = n3 + 1
|
||||
}
|
||||
if idx == 4 {
|
||||
c4 = c4 + lum
|
||||
n4 = n4 + 1
|
||||
}
|
||||
if idx == 5 {
|
||||
c5 = c5 + lum
|
||||
n5 = n5 + 1
|
||||
}
|
||||
if idx == 6 {
|
||||
c6 = c6 + lum
|
||||
n6 = n6 + 1
|
||||
}
|
||||
if idx == 7 {
|
||||
c7 = c7 + lum
|
||||
n7 = n7 + 1
|
||||
}
|
||||
if idx == 8 {
|
||||
c8 = c8 + lum
|
||||
n8 = n8 + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
let rA: Int = rsum / count
|
||||
let gA: Int = gsum / count
|
||||
let bA: Int = bsum / count
|
||||
let bright: Int = (299 * rA + 587 * gA + 114 * bA) / 255
|
||||
out = native_list_append(out, w)
|
||||
out = native_list_append(out, h)
|
||||
out = native_list_append(out, rA)
|
||||
out = native_list_append(out, gA)
|
||||
out = native_list_append(out, bA)
|
||||
out = native_list_append(out, bright)
|
||||
if n0 < 1 {
|
||||
n0 = 1
|
||||
}
|
||||
if n1 < 1 {
|
||||
n1 = 1
|
||||
}
|
||||
if n2 < 1 {
|
||||
n2 = 1
|
||||
}
|
||||
if n3 < 1 {
|
||||
n3 = 1
|
||||
}
|
||||
if n4 < 1 {
|
||||
n4 = 1
|
||||
}
|
||||
if n5 < 1 {
|
||||
n5 = 1
|
||||
}
|
||||
if n6 < 1 {
|
||||
n6 = 1
|
||||
}
|
||||
if n7 < 1 {
|
||||
n7 = 1
|
||||
}
|
||||
if n8 < 1 {
|
||||
n8 = 1
|
||||
}
|
||||
out = native_list_append(out, c0 / n0)
|
||||
out = native_list_append(out, c1 / n1)
|
||||
out = native_list_append(out, c2 / n2)
|
||||
out = native_list_append(out, c3 / n3)
|
||||
out = native_list_append(out, c4 / n4)
|
||||
out = native_list_append(out, c5 / n5)
|
||||
out = native_list_append(out, c6 / n6)
|
||||
out = native_list_append(out, c7 / n7)
|
||||
out = native_list_append(out, c8 / n8)
|
||||
organ_disclose("FEAT(image): 15-number scene-geometry vs " + int_to_str(w * h * 3) + " pixel-channels — the descriptor travels, the frame does not.")
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Own-core tone ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The smallest possible proof that the organ owns its medium end to end: a sine
|
||||
// with a gentle attack and release, computed here, played by us, no file and no
|
||||
// library anywhere in the path.
|
||||
fn organ_tone(hz: Int, ms: Int, sr: Int) -> [Int] {
|
||||
let n: Int = sr * ms / 1000
|
||||
let out: [Int] = native_list_empty()
|
||||
let two_pi: Float = 6.283185307
|
||||
let srf: Float = int_to_float(sr)
|
||||
let hzf: Float = int_to_float(hz)
|
||||
let i: Int = 0
|
||||
// 20 ms of ramp at each end; a square-edged tone clicks, and a click is the
|
||||
// organ announcing that it does not understand envelopes.
|
||||
let ramp: Int = sr / 50
|
||||
if ramp < 1 {
|
||||
ramp = 1
|
||||
}
|
||||
while i < n {
|
||||
let t: Float = int_to_float(i) / srf
|
||||
let s: Float = math_sin(two_pi * hzf * t)
|
||||
let env: Int = 32767
|
||||
if i < ramp {
|
||||
env = 32767 * i / ramp
|
||||
}
|
||||
let tail: Int = n - i
|
||||
if tail < ramp {
|
||||
env = 32767 * tail / ramp
|
||||
}
|
||||
let v: Int = float_to_int(s * 9000.0) * env / 32767
|
||||
out = native_list_append(out, v)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// organ_cli.el — the organ's command surface. main() lives here.
|
||||
//
|
||||
// One binary, the same verbs the Swift program had, and nothing behind them
|
||||
// except El and two thin device realizers. This file is the proof surface: if
|
||||
// `organ speak` makes a sound and no Swift binary is in the process tree, the
|
||||
// claim in organ.el's header is true.
|
||||
//
|
||||
// Verbs, and what each one demonstrates:
|
||||
//
|
||||
// grant/revoke/status the Neuron-level consent lock, inspectable
|
||||
// speak <wav> efferent — audio out of El's own speaker
|
||||
// tone <hz> <ms> own-core synthesis: computed in El, played by El,
|
||||
// never touching the disk
|
||||
// say <name> <codes...> fetch a VOICE FROM THE ENGRAM and render through it
|
||||
// listen <sec> <out> afferent — mic capture, consent-gated, fails closed
|
||||
// see <out.jpg> afferent — one camera frame, same two locks
|
||||
// wav-info <wav> WAV geometry, parsed in El
|
||||
// feat-audio <wav> capture -> compact descriptor (8 numbers)
|
||||
// feat-image <jpg> frame -> compact scene-geometry
|
||||
// voiceprint <wav> F0 + formants F1-F5 by LPC, in El
|
||||
// imitate <in> <out> LPC analysis-resynthesis, in El
|
||||
// hear-imitate <sec> the closed loop: hear a voice, take its signature,
|
||||
// speak back in it
|
||||
// ingest-audio <wav> descriptor -> engram node (the capture becomes geometry)
|
||||
// ingest-voice <wav> <n> voiceprint -> engram voice region (how a voice is learned)
|
||||
// converse <manifest> full-duplex interruptible utterance
|
||||
//
|
||||
// The descriptors are the point of the afferent half. A capture is NEVER handed
|
||||
// on raw: a three-second recording is ~48,000 samples and what leaves this
|
||||
// process is eight numbers. That is both the privacy rail (the stream stays
|
||||
// local because only its shape travels) and the reason the engram can hold a
|
||||
// perception at all — geometry is storable, a waveform is not.
|
||||
|
||||
fn cli_usage() -> Bool {
|
||||
println("organ — Neuron's I/O organ, native El (own-core, local, consent-gated)")
|
||||
println(" grant|revoke <camera|mic> Neuron-level consent")
|
||||
println(" status consent + device state")
|
||||
println(" speak <file.wav> play a WAV aloud (efferent)")
|
||||
println(" tone [hz] [ms] synthesize and play, no file at all")
|
||||
println(" say <voice> <CODE> [CODE...] fetch voice FROM THE ENGRAM, render, speak")
|
||||
println(" listen <sec> <out.wav> mic capture 16k mono (afferent)")
|
||||
println(" see <out.jpg> one camera frame (afferent)")
|
||||
println(" wav-info <file.wav> WAV geometry")
|
||||
println(" feat-audio <file.wav> compact audio descriptor (8 numbers)")
|
||||
println(" feat-image compact scene-geometry from the camera")
|
||||
println(" voiceprint <voice.wav> F0 + formants F1-F5 (LPC)")
|
||||
println(" imitate <in.wav> <out.wav> LPC analysis-resynthesis")
|
||||
println(" hear-imitate <sec> <out.wav> mic -> signature -> imitate -> speak aloud")
|
||||
println(" ingest-audio <file.wav> descriptor -> engram node (geometry)")
|
||||
println(" ingest-voice <voice.wav> <n> voiceprint -> engram voice region")
|
||||
println(" converse <manifest.json> [--authority PM] [--barge-at MS[:kind]] [--live-mic] [--resume]")
|
||||
return true
|
||||
}
|
||||
|
||||
// The engram the organ reads and writes. Its own store, never production's.
|
||||
fn cli_engram_dir() -> String {
|
||||
let d: String = env("ORGAN_ENGRAM")
|
||||
if str_eq(d, "") {
|
||||
return "peripheral/.engram"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
fn cli_open_engram() -> Bool {
|
||||
let dir: String = cli_engram_dir()
|
||||
fs_mkdir(dir)
|
||||
let ok: Int = engram_store_boot(dir)
|
||||
if ok == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── formatting helpers ───────────────────────────────────────────────────────
|
||||
|
||||
fn cli_f(v: Float, dec: Int) -> String {
|
||||
return format_float(v, dec)
|
||||
}
|
||||
|
||||
// ── the descriptor, printed and ingested ─────────────────────────────────────
|
||||
//
|
||||
// [seconds, sr, ch, rms, peak, zcr, centroid, f0] — the same eight numbers the
|
||||
// Swift produced, computed in El, and the compression ratio is the headline:
|
||||
// a few dozen bytes standing in for a few hundred kilobytes.
|
||||
fn cli_audio_descriptor_text(v: [Float], path: String) -> String {
|
||||
let secs: Float = native_list_get(v, 0)
|
||||
let sr: Float = native_list_get(v, 1)
|
||||
let ch: Float = native_list_get(v, 2)
|
||||
let rms: Float = native_list_get(v, 3)
|
||||
let peak: Float = native_list_get(v, 4)
|
||||
let zcr: Float = native_list_get(v, 5)
|
||||
let cen: Float = native_list_get(v, 6)
|
||||
let f0: Float = native_list_get(v, 7)
|
||||
return "Heard sound (afferent, mic): " + cli_f(secs, 2) + "s at " + cli_f(sr, 0) + "Hz. RMS energy " + cli_f(rms, 4) + ", peak " + cli_f(peak, 4) + ", zero-crossing rate " + cli_f(zcr, 0) + "Hz, spectral centroid " + cli_f(cen, 0) + "Hz, estimated voice pitch F0 " + cli_f(f0, 0) + "Hz. Compact voice/sound signature (8 numbers) — phonetic geometry seed."
|
||||
}
|
||||
|
||||
fn cli_feat_audio(path: String) -> Bool {
|
||||
let v: [Float] = dsp_compute_audio(path)
|
||||
if native_list_len(v) < 8 {
|
||||
println("{\"ok\": false, \"op\": \"feat-audio\", \"error\": \"cannot read PCM\"}")
|
||||
return false
|
||||
}
|
||||
organ_disclose("FEAT(audio): 8-number signature vs " + int_to_str(float_to_int(native_list_get(v, 0) * native_list_get(v, 1))) + " raw samples.")
|
||||
println("{\"ok\": true, \"op\": \"feat-audio\", \"file\": \"" + path + "\", \"seconds\": " + cli_f(native_list_get(v, 0), 4) + ", \"sample_rate\": " + cli_f(native_list_get(v, 1), 0) + ", \"channels\": " + cli_f(native_list_get(v, 2), 0) + ", \"rms\": " + cli_f(native_list_get(v, 3), 6) + ", \"peak\": " + cli_f(native_list_get(v, 4), 6) + ", \"zcr_hz\": " + cli_f(native_list_get(v, 5), 4) + ", \"centroid_hz\": " + cli_f(native_list_get(v, 6), 4) + ", \"f0_hz\": " + cli_f(native_list_get(v, 7), 4) + "}")
|
||||
return true
|
||||
}
|
||||
|
||||
fn cli_voiceprint(path: String) -> Bool {
|
||||
let v: [Float] = dsp_voiceprint(path)
|
||||
if native_list_len(v) < 4 {
|
||||
println("{\"ok\": false, \"op\": \"voiceprint\", \"error\": \"cannot read speech\"}")
|
||||
return false
|
||||
}
|
||||
let nf: Int = float_to_int(native_list_get(v, 3))
|
||||
let fs: String = ""
|
||||
let bs: String = ""
|
||||
let i: Int = 0
|
||||
while i < nf {
|
||||
if i > 0 {
|
||||
fs = fs + ", "
|
||||
bs = bs + ", "
|
||||
}
|
||||
fs = fs + cli_f(native_list_get(v, 4 + i * 2), 3)
|
||||
bs = bs + cli_f(native_list_get(v, 5 + i * 2), 3)
|
||||
i = i + 1
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"voiceprint\", \"file\": \"" + path + "\", \"f0_hz\": " + cli_f(native_list_get(v, 0), 4) + ", \"f0_range\": [" + cli_f(native_list_get(v, 1), 4) + ", " + cli_f(native_list_get(v, 2), 4) + "], \"formants_hz\": [" + fs + "], \"bandwidths_hz\": [" + bs + "]}")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
let a: [String] = args()
|
||||
let n: Int = native_list_len(a)
|
||||
if n < 1 {
|
||||
cli_usage()
|
||||
return
|
||||
}
|
||||
let cmd: String = native_list_get(a, 0)
|
||||
|
||||
// ---- consent -----------------------------------------------------------
|
||||
if str_eq(cmd, "grant") {
|
||||
if n < 2 {
|
||||
println("grant needs a device")
|
||||
return
|
||||
}
|
||||
organ_grant(native_list_get(a, 1))
|
||||
println("{\"ok\": true, \"op\": \"grant\", \"consent\": \"" + organ_consent_status() + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "revoke") {
|
||||
if n < 2 {
|
||||
println("revoke needs a device")
|
||||
return
|
||||
}
|
||||
organ_revoke(native_list_get(a, 1))
|
||||
println("{\"ok\": true, \"op\": \"revoke\", \"consent\": \"" + organ_consent_status() + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "status") {
|
||||
println("{\"ok\": true, \"op\": \"status\", \"consent\": \"" + organ_consent_status() + "\", \"speaker\": \"" + speaker_name() + "\", \"speaker_available\": " + int_to_str(speaker_available()) + ", \"mic_os_authorized\": " + int_to_str(mic_available()) + ", \"camera_os_authorized\": " + int_to_str(camera_available()) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- efferent ----------------------------------------------------------
|
||||
if str_eq(cmd, "speak") {
|
||||
if n < 2 {
|
||||
println("speak needs a wav")
|
||||
return
|
||||
}
|
||||
let ok: Bool = organ_speak_wav(native_list_get(a, 1))
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"speak\", \"played_aloud\": " + bool_to_str(ok) + "}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "tone") {
|
||||
let hz: Int = 220
|
||||
let ms: Int = 1000
|
||||
if n >= 2 {
|
||||
hz = str_to_int(native_list_get(a, 1))
|
||||
}
|
||||
if n >= 3 {
|
||||
ms = str_to_int(native_list_get(a, 2))
|
||||
}
|
||||
let s: [Int] = organ_tone(hz, ms, 16000)
|
||||
let ok: Bool = organ_speak_samples(s, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"tone\", \"hz\": " + int_to_str(hz) + ", \"ms\": " + int_to_str(ms) + ", \"samples\": " + int_to_str(native_list_len(s)) + ", \"file\": null}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- the voice, from the engram ----------------------------------------
|
||||
if str_eq(cmd, "say") {
|
||||
if n < 3 {
|
||||
println("say needs <voice> <CODE> [CODE...]")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let vname: String = native_list_get(a, 1)
|
||||
let g: [Int] = organ_voice_fetch(vname)
|
||||
if native_list_len(g) < 6 {
|
||||
println("{\"ok\": false, \"op\": \"say\", \"error\": \"no voice region '" + vname + "' in the engram\"}")
|
||||
return
|
||||
}
|
||||
// Codes and the phoneme map come from the LANGUAGE side. The organ does
|
||||
// not know what a word is and never looks one up.
|
||||
let pmap: [String] = ingest_phonetics("elp/data/phonetics.psv")
|
||||
let codes: [String] = native_list_empty()
|
||||
let i: Int = 2
|
||||
while i < n {
|
||||
codes = native_list_append(codes, native_list_get(a, i))
|
||||
i = i + 1
|
||||
}
|
||||
let voice: [String] = organ_voice_profile(vname, g)
|
||||
let s: [Int] = synth_codes(codes, voice, pmap)
|
||||
let ok: Bool = organ_speak_samples(s, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"say\", \"voice\": \"" + vname + "\", \"f0\": " + int_to_str(native_list_get(g, 0)) + ", \"kf\": " + int_to_str(native_list_get(g, 2)) + ", \"codes\": " + int_to_str(native_list_len(codes)) + ", \"samples\": " + int_to_str(native_list_len(s)) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- afferent ----------------------------------------------------------
|
||||
if str_eq(cmd, "listen") {
|
||||
if n < 3 {
|
||||
println("listen needs <sec> <out.wav>")
|
||||
return
|
||||
}
|
||||
let secs: Int = str_to_int(native_list_get(a, 1))
|
||||
let out: String = native_list_get(a, 2)
|
||||
if organ_may_listen() == false {
|
||||
println("{\"ok\": false, \"op\": \"listen\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
organ_disclose("MIC: capturing " + int_to_str(secs) + "s (16 kHz mono, LOCAL, never egresses).")
|
||||
let s: [Int] = mic_capture_pcm16(secs, 16000)
|
||||
let got: Int = native_list_len(s)
|
||||
if got <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"listen\", \"error\": \"capture returned nothing\"}")
|
||||
return
|
||||
}
|
||||
let ok: Bool = write_wav(s, 16000, out)
|
||||
organ_disclose("MIC: captured " + int_to_str(got) + " frames — ready to hand to the ingest organ.")
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"listen\", \"file\": \"" + out + "\", \"frames\": " + int_to_str(got) + ", \"sample_rate\": 16000}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "see") {
|
||||
if n < 2 {
|
||||
println("see needs an out path")
|
||||
return
|
||||
}
|
||||
if organ_may_see() == false {
|
||||
println("{\"ok\": false, \"op\": \"see\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
organ_disclose("CAMERA: capturing one frame (LOCAL, never egresses).")
|
||||
let ok: Int = camera_capture_jpeg(native_list_get(a, 1))
|
||||
println("{\"ok\": " + int_to_str(ok) + ", \"op\": \"see\", \"file\": \"" + native_list_get(a, 1) + "\"}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- descriptors -------------------------------------------------------
|
||||
if str_eq(cmd, "wav-info") {
|
||||
if n < 2 {
|
||||
println("wav-info needs a wav")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let w: [Float] = dsp_read_wav(p)
|
||||
if dsp_wav_n(w) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"wav-info\"}")
|
||||
return
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"wav-info\", \"sample_rate\": " + int_to_str(dsp_wav_sr(w)) + ", \"channels\": " + int_to_str(dsp_wav_ch(w)) + ", \"frames\": " + int_to_str(dsp_wav_n(w)) + "}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "feat-audio") {
|
||||
if n < 2 {
|
||||
println("feat-audio needs a wav")
|
||||
return
|
||||
}
|
||||
cli_feat_audio(native_list_get(a, 1))
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "feat-image") {
|
||||
if organ_may_see() == false {
|
||||
println("{\"ok\": false, \"op\": \"feat-image\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
let f: [Int] = organ_image_descriptor()
|
||||
if native_list_len(f) < 15 {
|
||||
println("{\"ok\": false, \"op\": \"feat-image\", \"error\": \"no frame\"}")
|
||||
return
|
||||
}
|
||||
let grid: String = ""
|
||||
let i: Int = 6
|
||||
while i < 15 {
|
||||
if i > 6 {
|
||||
grid = grid + ", "
|
||||
}
|
||||
grid = grid + int_to_str(native_list_get(f, i))
|
||||
i = i + 1
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"feat-image\", \"width\": " + int_to_str(native_list_get(f, 0)) + ", \"height\": " + int_to_str(native_list_get(f, 1)) + ", \"mean_rgb\": [" + int_to_str(native_list_get(f, 2)) + ", " + int_to_str(native_list_get(f, 3)) + ", " + int_to_str(native_list_get(f, 4)) + "], \"brightness_pm\": " + int_to_str(native_list_get(f, 5)) + ", \"luma_grid\": [" + grid + "]}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "voiceprint") {
|
||||
if n < 2 {
|
||||
println("voiceprint needs a wav")
|
||||
return
|
||||
}
|
||||
cli_voiceprint(native_list_get(a, 1))
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "imitate") {
|
||||
if n < 3 {
|
||||
println("imitate needs <in.wav> <out.wav>")
|
||||
return
|
||||
}
|
||||
let s: [Int] = dsp_imitate(native_list_get(a, 1))
|
||||
if native_list_len(s) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"imitate\"}")
|
||||
return
|
||||
}
|
||||
let ok: Bool = write_wav(s, 16000, native_list_get(a, 2))
|
||||
organ_disclose("IMITATE: rebuilt the voice from its own LPC signature (own-core, no training, no stolen voice).")
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"imitate\", \"out\": \"" + native_list_get(a, 2) + "\", \"samples\": " + int_to_str(native_list_len(s)) + ", \"method\": \"LPC analysis-resynthesis\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "hear-imitate") {
|
||||
if n < 3 {
|
||||
println("hear-imitate needs <sec> <out.wav>")
|
||||
return
|
||||
}
|
||||
let secs: Int = str_to_int(native_list_get(a, 1))
|
||||
let out: String = native_list_get(a, 2)
|
||||
if organ_may_listen() == false {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
let heard: String = out + ".heard.wav"
|
||||
organ_disclose("HEAR-IMITATE: open the ear, listen " + int_to_str(secs) + "s, take the voice, speak it back.")
|
||||
let s: [Int] = mic_capture_pcm16(secs, 16000)
|
||||
if native_list_len(s) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"capture returned nothing\"}")
|
||||
return
|
||||
}
|
||||
write_wav(s, 16000, heard)
|
||||
let re: [Int] = dsp_imitate(heard)
|
||||
if native_list_len(re) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"could not model the voice\"}")
|
||||
return
|
||||
}
|
||||
write_wav(re, 16000, out)
|
||||
let ok: Bool = organ_speak_samples(re, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"hear-imitate\", \"heard\": \"" + heard + "\", \"out\": \"" + out + "\", \"spoke_aloud\": " + bool_to_str(ok) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- the afferent wire: descriptor -> geometry --------------------------
|
||||
if str_eq(cmd, "ingest-audio") {
|
||||
if n < 2 {
|
||||
println("ingest-audio needs a wav")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let v: [Float] = dsp_compute_audio(p)
|
||||
if native_list_len(v) < 8 {
|
||||
println("{\"ok\": false, \"op\": \"ingest-audio\"}")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let content: String = cli_audio_descriptor_text(v, p)
|
||||
let id: String = engram_node(content, "Observation", 70)
|
||||
engram_store_checkpoint()
|
||||
organ_disclose("INGEST: the capture is now GEOMETRY in the engram (node " + id + ") — the descriptor travelled, the stream did not.")
|
||||
println("{\"ok\": true, \"op\": \"ingest-audio\", \"node_id\": \"" + id + "\", \"content\": \"" + content + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "ingest-voice") {
|
||||
if n < 3 {
|
||||
println("ingest-voice needs <voice.wav> <name>")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let name: String = native_list_get(a, 2)
|
||||
let v: [Float] = dsp_voiceprint(p)
|
||||
if native_list_len(v) < 10 {
|
||||
println("{\"ok\": false, \"op\": \"ingest-voice\", \"error\": \"no voiced frames\"}")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let f0: Int = float_to_int(native_list_get(v, 0))
|
||||
let f1: Int = float_to_int(native_list_get(v, 4))
|
||||
let f2: Int = float_to_int(native_list_get(v, 6))
|
||||
let f3: Int = float_to_int(native_list_get(v, 8))
|
||||
// kf is the vocal-tract scale: this speaker's F1 against the nominal
|
||||
// /AA/ F1 of 730 Hz. One number standing for a tract length.
|
||||
let kf: Int = 1000 * f1 / 730
|
||||
let f0e: Int = f0 * 85 / 100
|
||||
let id: String = organ_voice_ingest(name, f0, f0e, kf, f1, f2, f3, "el-organ-lpc-voiceprint", "COARSE")
|
||||
engram_store_checkpoint()
|
||||
println("{\"ok\": true, \"op\": \"ingest-voice\", \"node_id\": \"" + id + "\", \"name\": \"" + name + "\", \"f0\": " + int_to_str(f0) + ", \"kf\": " + int_to_str(kf) + ", \"f1\": " + int_to_str(f1) + ", \"f2\": " + int_to_str(f2) + ", \"f3\": " + int_to_str(f3) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- converse ----------------------------------------------------------
|
||||
if str_eq(cmd, "converse") {
|
||||
if n < 2 {
|
||||
println("converse needs a manifest")
|
||||
return
|
||||
}
|
||||
let mf: String = native_list_get(a, 1)
|
||||
let authority: Int = 500
|
||||
let barge: Int = 0 - 1
|
||||
let kind: String = "bargein"
|
||||
let live: Bool = false
|
||||
let resume: Bool = false
|
||||
let i: Int = 2
|
||||
while i < n {
|
||||
let f: String = native_list_get(a, i)
|
||||
if str_eq(f, "--authority") {
|
||||
if i + 1 < n {
|
||||
authority = str_to_int(native_list_get(a, i + 1))
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if str_eq(f, "--barge-at") {
|
||||
if i + 1 < n {
|
||||
let spec: String = native_list_get(a, i + 1)
|
||||
let c: Int = str_index_of(spec, ":")
|
||||
if c < 0 {
|
||||
barge = str_to_int(spec)
|
||||
} else {
|
||||
barge = str_to_int(str_slice(spec, 0, c))
|
||||
kind = str_slice(spec, c + 1, str_len(spec))
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if str_eq(f, "--live-mic") {
|
||||
live = true
|
||||
}
|
||||
if str_eq(f, "--resume") {
|
||||
resume = true
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
let plan: [String] = conv_load_manifest(mf)
|
||||
if resume {
|
||||
plan = conv_load_resume()
|
||||
organ_disclose("CONVERSE: resuming — \"as I was saying...\" (" + int_to_str(plan_count(plan)) + " segments left).")
|
||||
} else {
|
||||
organ_disclose("CONVERSE: utterance = \"" + conv_utterance(mf) + "\" (" + int_to_str(plan_count(plan)) + " segments).")
|
||||
}
|
||||
let stopped: Int = conv_run(plan, authority, barge, kind, live)
|
||||
println("{\"ok\": true, \"op\": \"converse\", \"stopped_at\": " + int_to_str(stopped) + ", \"complete\": " + bool_to_str(stopped < 0) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
cli_usage()
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
// organ_converse.el — full-duplex, interruptible speech. The turn-taking organ.
|
||||
//
|
||||
// WHAT THIS IS FOR. A system that plays an utterance to completion and only
|
||||
// then listens is not conversational, it is a loudspeaker with a queue. Being
|
||||
// interruptible is not a feature bolted onto speech; it is most of what makes
|
||||
// speech social. So the utterance is not a blob of audio — it is an ordered,
|
||||
// SALIENCE-TAGGED MEANING-PLAN, and the organ speaks it while listening, decides
|
||||
// what to do when interrupted, and can pick the thread back up afterwards.
|
||||
//
|
||||
// THREE THINGS HAVE TO BE TRUE, and each one is a place naive implementations
|
||||
// go wrong:
|
||||
//
|
||||
// Barge-in is AT THE SAMPLE. When the mic hears speech, output stops on the
|
||||
// spot — not at the end of the current buffer, not at the end of the segment.
|
||||
// A listener experiences even a fifth of a second of continued talking as
|
||||
// being talked over. This is why the speaker realizer has pause/resume and
|
||||
// reports played_frames: "finish the buffer" is not barge-in.
|
||||
//
|
||||
// Yield-or-hold is a DECISION, not a rule. Stopping every time anyone makes a
|
||||
// noise is its own failure — it means Neuron can never finish a sentence that
|
||||
// matters. So the choice is grounded: how salient is what I am mid-saying,
|
||||
// how close am I to done, and how much authority does the interrupter have.
|
||||
// Holding the floor is justified when what I am saying matters AND finishing
|
||||
// is cheap AND the interrupter is not high-priority. Otherwise yield, because
|
||||
// the polite default is the right default.
|
||||
//
|
||||
// A backchannel is NOT an interruption. "mm-hm" means keep going. Treating it
|
||||
// as a barge-in makes the system stop every three seconds during ordinary
|
||||
// listening behaviour, which is worse than not listening at all. It is
|
||||
// distinguished by being brief and low-energy: sample again shortly after
|
||||
// onset, and if the speech already died away it was a backchannel.
|
||||
//
|
||||
// AND THE UTTERANCE SURVIVES. On yield, the remaining plan is persisted, so
|
||||
// Neuron can resume — "as I was saying" — instead of losing the thought. An
|
||||
// interruption should cost a turn, not the content.
|
||||
//
|
||||
// The AEC rail: the microphone runs with the OS voice-processing unit enabled
|
||||
// so it does not hear our own speaker. Without it Neuron barges in on its own
|
||||
// voice on the first syllable and the whole loop is unusable in a real room.
|
||||
//
|
||||
// Note what is NOT here: nothing about words. A segment carries a `text` field
|
||||
// purely as a label for disclosure. The organ speaks pre-rendered audio and
|
||||
// never inspects language — that is the language faculty's, and the seam holds.
|
||||
|
||||
// ── The meaning-plan ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Stored as a flat [String] with stride 3 — file, salience-per-mille, text —
|
||||
// because El has no record type and parallel lists drift out of step under
|
||||
// editing. Salience is an integer per-mille rather than a Float so the decision
|
||||
// arithmetic stays exact and reproducible; a turn-taking decision that varies
|
||||
// with floating-point rounding is not one you can debug.
|
||||
|
||||
fn plan_new() -> [String] {
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
fn plan_add(plan: [String], file: String, salience_pm: Int, text: String) -> [String] {
|
||||
let p: [String] = plan
|
||||
p = native_list_append(p, file)
|
||||
p = native_list_append(p, int_to_str(salience_pm))
|
||||
p = native_list_append(p, text)
|
||||
return p
|
||||
}
|
||||
|
||||
fn plan_count(plan: [String]) -> Int {
|
||||
return native_list_len(plan) / 3
|
||||
}
|
||||
|
||||
fn plan_file(plan: [String], i: Int) -> String {
|
||||
return native_list_get(plan, i * 3)
|
||||
}
|
||||
|
||||
fn plan_salience(plan: [String], i: Int) -> Int {
|
||||
return str_to_int(native_list_get(plan, i * 3 + 1))
|
||||
}
|
||||
|
||||
fn plan_text(plan: [String], i: Int) -> String {
|
||||
return native_list_get(plan, i * 3 + 2)
|
||||
}
|
||||
|
||||
// ── Manifest ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// {"utterance": "...", "segments": [{"file":..., "salience":0.9, "text":"..."}]}
|
||||
// Salience arrives as a 0..1 float in the manifest and is converted once, here,
|
||||
// at the edge — the same discipline the runtime uses for wire encodings.
|
||||
|
||||
fn conv_salience_pm(raw: String) -> Int {
|
||||
// "0.85" -> 850. Parsed by hand rather than through a float so a manifest
|
||||
// typo degrades to a visible number instead of a silent 0.0.
|
||||
let dot: Int = str_index_of(raw, ".")
|
||||
if dot < 0 {
|
||||
let whole: Int = str_to_int(raw)
|
||||
return whole * 1000
|
||||
}
|
||||
let ip: Int = str_to_int(str_slice(raw, 0, dot))
|
||||
let frac: String = str_slice(raw, dot + 1, str_len(raw))
|
||||
let pm: Int = 0
|
||||
let scale: Int = 100
|
||||
let i: Int = 0
|
||||
while i < 3 {
|
||||
let d: Int = 0
|
||||
if i < str_len(frac) {
|
||||
let c: Int = str_char_code(frac, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 {
|
||||
d = c - 48
|
||||
}
|
||||
}
|
||||
}
|
||||
pm = pm + d * scale
|
||||
scale = scale / 10
|
||||
i = i + 1
|
||||
}
|
||||
return ip * 1000 + pm
|
||||
}
|
||||
|
||||
fn conv_load_manifest(path: String) -> [String] {
|
||||
let plan: [String] = plan_new()
|
||||
let raw: String = fs_read(path)
|
||||
if str_eq(raw, "") {
|
||||
organ_disclose("CONVERSE: cannot read manifest " + path)
|
||||
return plan
|
||||
}
|
||||
let segs: String = json_get_raw(raw, "segments")
|
||||
let n: Int = json_array_len(segs)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let seg: String = json_array_get(segs, i)
|
||||
let file: String = json_get_string(seg, "file")
|
||||
let text: String = json_get_string(seg, "text")
|
||||
let sal: String = json_get_raw(seg, "salience")
|
||||
let pm: Int = conv_salience_pm(sal)
|
||||
if pm <= 0 {
|
||||
pm = 500
|
||||
}
|
||||
plan = plan_add(plan, file, pm, text)
|
||||
i = i + 1
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
fn conv_utterance(path: String) -> String {
|
||||
let raw: String = fs_read(path)
|
||||
return json_get_string(raw, "utterance")
|
||||
}
|
||||
|
||||
// ── The decision ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Returns: 0 = backchannel, carry on seamlessly
|
||||
// 1 = hold the floor ("hang on, let me finish this thought")
|
||||
// 2 = yield (stop, let them in)
|
||||
//
|
||||
// All arguments are per-mille integers. Holding requires BOTH that the material
|
||||
// is worth finishing AND that the interrupter is not high-authority — either
|
||||
// condition alone is not enough, because "what I'm saying is important" is
|
||||
// exactly the reasoning that produces a system nobody can get a word in against.
|
||||
fn conv_decide(salience_pm: Int, progress_pm: Int, authority_pm: Int, is_backchannel: Bool) -> Int {
|
||||
if is_backchannel {
|
||||
return 0
|
||||
}
|
||||
let hold_score: Int = (salience_pm * 6 + progress_pm * 4) / 10
|
||||
if hold_score >= 600 {
|
||||
if authority_pm < 800 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// ── Resume ───────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The remaining plan, written where a later run can find it. This is what turns
|
||||
// an interruption into a pause rather than a loss.
|
||||
|
||||
fn conv_resume_path() -> String {
|
||||
let home: String = env("PERIPH_HOME")
|
||||
if str_eq(home, "") {
|
||||
return "peripheral/.resume.json"
|
||||
}
|
||||
return home + "/.resume.json"
|
||||
}
|
||||
|
||||
// Minimal JSON string escaping. Written here rather than reached for from the
|
||||
// runtime because the organ needs exactly two escapes and no dependency: a
|
||||
// segment label containing a quote or a backslash must not be able to produce a
|
||||
// resume file that fails to parse and silently loses the thread.
|
||||
fn conv_escape(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c == 34 {
|
||||
out = out + "\\\""
|
||||
} else {
|
||||
if c == 92 {
|
||||
out = out + "\\\\"
|
||||
} else {
|
||||
if c >= 32 {
|
||||
out = out + str_slice(s, i, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn conv_persist_resume(plan: [String], start_at: Int, reason: String) -> Bool {
|
||||
let n: Int = plan_count(plan)
|
||||
let body: String = "{\"resume_from\": " + int_to_str(start_at) + ", \"reason\": \"" + reason + "\", \"segments\": ["
|
||||
let i: Int = start_at
|
||||
let first: Bool = true
|
||||
while i < n {
|
||||
if first == false {
|
||||
body = body + ", "
|
||||
}
|
||||
body = body + "{\"file\": \"" + plan_file(plan, i) + "\", \"salience\": " + int_to_str(plan_salience(plan, i)) + ", \"text\": \"" + conv_escape(plan_text(plan, i)) + "\"}"
|
||||
first = false
|
||||
i = i + 1
|
||||
}
|
||||
body = body + "]}\n"
|
||||
let ok: Bool = fs_write(conv_resume_path(), body)
|
||||
organ_disclose("CONVERSE: meaning-plan persisted (" + int_to_str(n - start_at) + " segments remain) — Neuron can resume the thread.")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn conv_clear_resume() -> Bool {
|
||||
return fs_write(conv_resume_path(), "")
|
||||
}
|
||||
|
||||
// Read a persisted plan back. Salience is already per-mille here (we wrote it),
|
||||
// so it is NOT re-scaled — the manifest and the resume file are different
|
||||
// formats on purpose, and conflating them silently divides every salience by a
|
||||
// thousand.
|
||||
fn conv_load_resume() -> [String] {
|
||||
let plan: [String] = plan_new()
|
||||
let raw: String = fs_read(conv_resume_path())
|
||||
if str_eq(raw, "") {
|
||||
return plan
|
||||
}
|
||||
let segs: String = json_get_raw(raw, "segments")
|
||||
let n: Int = json_array_len(segs)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let seg: String = json_array_get(segs, i)
|
||||
plan = plan_add(plan, json_get_string(seg, "file"), json_get_int(seg, "salience"), json_get_string(seg, "text"))
|
||||
i = i + 1
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// ── The loop ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// live_mic : open the microphone with AEC and let real speech drive barge-in.
|
||||
// barge_ms : if >= 0, inject a barge event at that offset into the utterance
|
||||
// instead. Deterministic, so the decision paths can be exercised
|
||||
// without a room and a person — the same reason periph.swift has it.
|
||||
// kind : "backchannel" or "bargein", for the injected case.
|
||||
// authority : interrupter authority, per-mille.
|
||||
//
|
||||
// Returns the index the utterance stopped at, or -1 if it completed.
|
||||
|
||||
fn conv_run(plan: [String], authority_pm: Int, barge_ms: Int, kind: String, live_mic: Bool) -> Int {
|
||||
let n: Int = plan_count(plan)
|
||||
if n <= 0 {
|
||||
organ_disclose("CONVERSE: nothing to say.")
|
||||
return 0 - 1
|
||||
}
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("CONVERSE: no speaker on this build — cannot hold a conversation.")
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
let mic_live: Bool = false
|
||||
if live_mic {
|
||||
if organ_may_listen() {
|
||||
let m: Int = mic_monitor_start()
|
||||
if m == 1 {
|
||||
organ_disclose("CONVERSE: full-duplex — mic listening WHILE speaking, AEC on (won't self-interrupt).")
|
||||
mic_live = true
|
||||
}
|
||||
if m == 2 {
|
||||
organ_disclose("CONVERSE: full-duplex — mic listening, but AEC UNAVAILABLE; raising the VAD floor so we do not barge in on ourselves.")
|
||||
mic_live = true
|
||||
}
|
||||
if m == 0 {
|
||||
organ_disclose("CONVERSE: could not open the mic monitor — falling back to injected events.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if mic_live == false {
|
||||
organ_disclose("CONVERSE: deterministic mode (live mic off).")
|
||||
}
|
||||
|
||||
// Without AEC the mic hears the speaker, so the threshold has to sit above
|
||||
// our own output. This is a mitigation and not a fix: the honest note is
|
||||
// that barge-in is markedly less sensitive in this mode.
|
||||
let vad_pm: Int = 20
|
||||
if mic_live {
|
||||
if mic_monitor_start() == 2 {
|
||||
vad_pm = 60
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_ms: Int = 0
|
||||
let prior_ms: Int = 0
|
||||
let handled: Bool = false
|
||||
// An injected barge is ONE event, not a condition that stays true. Without
|
||||
// this the deadline re-fires on every poll after a backchannel resume, and
|
||||
// the utterance live-locks: paused, resumed, paused again, forever.
|
||||
let injected_fired: Bool = false
|
||||
let i: Int = 0
|
||||
|
||||
while i < n {
|
||||
let file: String = plan_file(plan, i)
|
||||
let sal: Int = plan_salience(plan, i)
|
||||
let frames: Int = wav_frames(file)
|
||||
let rate: Int = wav_rate(file)
|
||||
if frames <= 0 {
|
||||
organ_disclose("CONVERSE: missing or unreadable segment '" + file + "', skipping.")
|
||||
i = i + 1
|
||||
} else {
|
||||
let dur_ms: Int = frames * 1000 / rate
|
||||
organ_disclose("CONVERSE: speaking segment " + int_to_str(i + 1) + "/" + int_to_str(n) + " (salience " + int_to_str(sal) + "/1000) — \"" + plan_text(plan, i) + "\"")
|
||||
let started: Int = speaker_play_wav_async(file)
|
||||
if started == 0 {
|
||||
organ_disclose("CONVERSE: could not start playback for '" + file + "'.")
|
||||
i = i + 1
|
||||
} else {
|
||||
let seg_ms: Int = 0
|
||||
let done: Bool = false
|
||||
let interrupted: Bool = false
|
||||
let speech_ticks: Int = 0
|
||||
|
||||
while done == false {
|
||||
sleep_ms(10)
|
||||
seg_ms = seg_ms + 10
|
||||
|
||||
if speaker_playing() == 0 {
|
||||
done = true
|
||||
} else {
|
||||
// The tick counter is an approximation — each pass costs
|
||||
// more than the sleep it asked for. The DAC position is
|
||||
// the truth, so drive the injected deadline off THAT and
|
||||
// an injected barge lands where it was asked to land.
|
||||
let pos_ms: Int = speaker_played_frames() * 1000 / rate
|
||||
elapsed_ms = prior_ms + pos_ms
|
||||
// --- onset detection: real speech, or an injected event ---
|
||||
let onset: Bool = false
|
||||
if mic_live {
|
||||
let rms: Float = mic_monitor_rms()
|
||||
let rms_pm: Int = float_to_int(rms * 1000.0)
|
||||
if rms_pm > vad_pm {
|
||||
speech_ticks = speech_ticks + 1
|
||||
} else {
|
||||
speech_ticks = 0
|
||||
}
|
||||
// ~60ms of continuous voice: short enough to feel
|
||||
// instant, long enough that a door closing is not a turn.
|
||||
if speech_ticks >= 3 {
|
||||
if handled == false {
|
||||
onset = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if barge_ms >= 0 {
|
||||
if injected_fired == false {
|
||||
if elapsed_ms >= barge_ms {
|
||||
onset = true
|
||||
injected_fired = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if onset {
|
||||
handled = true
|
||||
// (1) BARGE-IN — pause on the spot.
|
||||
speaker_pause()
|
||||
let played: Int = speaker_played_frames()
|
||||
let at_ms: Int = played * 1000 / rate
|
||||
let progress_pm: Int = at_ms * 1000 / dur_ms
|
||||
if progress_pm > 1000 {
|
||||
progress_pm = 1000
|
||||
}
|
||||
organ_disclose("CONVERSE: << user speech at " + int_to_str(at_ms) + "ms into segment " + int_to_str(i + 1) + " — PAUSED instantly >>")
|
||||
|
||||
// (2) backchannel or real barge-in?
|
||||
let is_bc: Bool = false
|
||||
if barge_ms >= 0 {
|
||||
if str_eq(kind, "backchannel") {
|
||||
is_bc = true
|
||||
}
|
||||
} else {
|
||||
// Live: look again ~250ms after onset. If the
|
||||
// energy has already collapsed it was "mm-hm".
|
||||
sleep_ms(250)
|
||||
let r2: Float = mic_monitor_rms()
|
||||
if float_to_int(r2 * 1000.0) < 15 {
|
||||
is_bc = true
|
||||
}
|
||||
}
|
||||
|
||||
// (3) yield, hold, or carry on
|
||||
let d: Int = conv_decide(sal, progress_pm, authority_pm, is_bc)
|
||||
if d == 0 {
|
||||
organ_disclose("CONVERSE: read as BACKCHANNEL (\"mm-hm\") — keep going, resume seamlessly.")
|
||||
handled = false
|
||||
speech_ticks = 0
|
||||
speaker_resume()
|
||||
}
|
||||
if d == 1 {
|
||||
organ_disclose("CONVERSE: HOLD the floor — \"hang on, let me finish this thought.\" (salience " + int_to_str(sal) + ", progress " + int_to_str(progress_pm) + ")")
|
||||
speaker_resume()
|
||||
// Finish THIS segment, then yield the remainder:
|
||||
// holding is a request for a moment, not a claim
|
||||
// on the rest of the conversation.
|
||||
while speaker_playing() == 1 {
|
||||
sleep_ms(20)
|
||||
}
|
||||
speaker_stop()
|
||||
conv_persist_resume(plan, i + 1, "held-then-yield")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return i + 1
|
||||
}
|
||||
if d == 2 {
|
||||
organ_disclose("CONVERSE: YIELD — stop, let them in. Remembering where I was (resumable).")
|
||||
speaker_stop()
|
||||
conv_persist_resume(plan, i, "yield")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if interrupted == false {
|
||||
prior_ms = prior_ms + dur_ms
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conv_clear_resume()
|
||||
organ_disclose("CONVERSE: utterance complete (uninterrupted).")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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)."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CI wire-in:
|
||||
# foundation/el .gitea/workflows/ci-dev.yaml, ci-stage.yaml, sdk-release.yaml
|
||||
# Add an early step (before the build/publish steps). It must run from the
|
||||
# REPO ROOT, so override the job's `defaults.run.working-directory: lang`:
|
||||
# CI wire-in — DONE (2026-08-16). This block used to describe the wire-in as a
|
||||
# TODO, and it had never been done: the guard existed but ran nowhere, so it
|
||||
# caught nothing for as long as it has been in the tree. It is now an early step
|
||||
# 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
|
||||
# working-directory: ${{ github.workspace }}
|
||||
# run: bash scripts/check-single-runtime.sh
|
||||
#
|
||||
# Also add to .githooks/pre-commit so drift is caught before it is committed.
|
||||
# Its sibling scripts/check-runtime-growth.sh is wired in at the same points and
|
||||
# guards the other half of the problem: this script stops el_runtime.c being
|
||||
# COPIED, that one stops it GROWING.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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