Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5baab050e2 |
+50
-163
@@ -22,46 +22,39 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
> /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb (needed for Artifact Registry publish and downstream CI)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
@@ -87,23 +80,14 @@ jobs:
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -112,8 +96,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -122,8 +106,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -132,8 +116,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -142,8 +126,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -152,8 +136,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -162,8 +146,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -172,8 +156,8 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -182,58 +166,31 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (dev)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -241,15 +198,7 @@ jobs:
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -257,71 +206,9 @@ jobs:
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-dev \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-dev"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (dev)
|
||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:dev (or fall back to :latest on first run)
|
||||
BASE_TAG="dev"
|
||||
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:dev" \
|
||||
-t "${CI_BASE}:dev-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:dev"
|
||||
docker push "${CI_BASE}:dev-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
+39
-126
@@ -34,25 +34,35 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests - text
|
||||
@@ -87,7 +97,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -97,7 +107,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -107,7 +117,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -117,7 +127,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -127,7 +137,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -137,7 +147,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -147,7 +157,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -157,7 +167,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -167,74 +177,30 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Build elb (needed for epm and el-install builds below)
|
||||
- name: Build elb
|
||||
run: |
|
||||
mkdir -p dist/bin
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
# Publish only after merge (push event), not on PR validation runs
|
||||
- name: Publish El SDK to Artifact Registry (stage)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -242,7 +208,7 @@ jobs:
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -250,62 +216,9 @@ jobs:
|
||||
--repository=foundation-stage \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-stage"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK (stage)
|
||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base:stage (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:stage" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:stage" \
|
||||
-t "${CI_BASE}:stage-${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:stage"
|
||||
docker push "${CI_BASE}:stage-${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
@@ -34,26 +34,35 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Seed: use the committed linux-amd64 binary as the bootstrap
|
||||
- name: Bootstrap from committed linux binary (seed)
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
chmod +x dist/platform/elc-linux-amd64
|
||||
echo "seed elc (committed linux-amd64 binary)"
|
||||
dist/platform/elc-linux-amd64 --version || true
|
||||
|
||||
# Gen2: use seed to self-host compile the El compiler
|
||||
- name: Self-host compile El compiler (gen2)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen2.c \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen2 (self-hosted) elc built"
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Build elb binary
|
||||
@@ -65,30 +74,34 @@ jobs:
|
||||
-I el-compiler/runtime \
|
||||
dist/elb.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
echo "elb built"
|
||||
|
||||
# Build epm binary using elb (epm lives at repo root, not inside lang/)
|
||||
# Build epm binary (epm lives at repo root, not inside lang/)
|
||||
- name: Build epm
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
dist/platform/elc ../epm/src/epm.el > dist/epm.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/epm.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/bin/epm
|
||||
chmod +x dist/bin/epm
|
||||
echo "epm built"
|
||||
|
||||
# Build el-install binary using elb
|
||||
# Build el-install binary
|
||||
- name: Build el-install
|
||||
run: |
|
||||
ABS_ELB="$(pwd)/dist/bin/elb"
|
||||
ABS_ELC="$(pwd)/dist/platform/elc"
|
||||
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
ABS_OUT="$(pwd)/dist/bin"
|
||||
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
|
||||
dist/platform/elc tools/install/el-install.el > dist/el-install.c
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/el-install.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/bin/el-install
|
||||
chmod +x dist/bin/el-install
|
||||
echo "el-install built"
|
||||
|
||||
@@ -124,7 +137,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests - native (text)
|
||||
@@ -134,7 +147,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests - native (string)
|
||||
@@ -144,7 +157,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests - native (math)
|
||||
@@ -154,7 +167,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests - native (state)
|
||||
@@ -164,7 +177,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests - native (time)
|
||||
@@ -174,7 +187,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests - native (json)
|
||||
@@ -184,7 +197,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests - native (env)
|
||||
@@ -194,7 +207,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests - native (fs)
|
||||
@@ -204,7 +217,7 @@ jobs:
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
|
||||
@@ -228,7 +241,7 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
REPO: neuron-technologies/el
|
||||
run: |
|
||||
@@ -288,29 +301,21 @@ jobs:
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||
# and the SDK silently never published. Surface failures now.
|
||||
set -euo pipefail
|
||||
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||
|
||||
VERSION="${GITHUB_SHA:0:8}"
|
||||
VERSION="${GITEA_SHA:0:8}"
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elc \
|
||||
--package=el/elc \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/platform/elc
|
||||
|
||||
@@ -318,15 +323,7 @@ jobs:
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-elb \
|
||||
--version="${VERSION}" \
|
||||
--source=dist/bin/elb
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-c \
|
||||
--package=el/el_runtime.c \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.c
|
||||
|
||||
@@ -334,81 +331,20 @@ jobs:
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-h \
|
||||
--package=el/el_runtime.h \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.h
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-js \
|
||||
--version="${VERSION}" \
|
||||
--source=el-compiler/runtime/el_runtime.js
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
|
||||
- name: Rebuild ci-base with fresh El SDK
|
||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||
#
|
||||
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||
# runner where DinD/Docker availability is fragile. A failure here must
|
||||
# never block or redden the job — the SDK publish above is the deliverable.
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
|
||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||
gcloud config set project neuron-785695
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# Pull existing ci-base (system deps stay cached in the base layer)
|
||||
docker pull "${CI_BASE}:latest"
|
||||
|
||||
# Inline Dockerfile — only replaces the El SDK layer
|
||||
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
|
||||
ARG BASE
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
|
||||
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
|
||||
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg BASE="${CI_BASE}:latest" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-f /tmp/Dockerfile.ci-base-patch \
|
||||
-t "${CI_BASE}:latest" \
|
||||
-t "${CI_BASE}:${SHA}" \
|
||||
.
|
||||
|
||||
docker push "${CI_BASE}:latest"
|
||||
docker push "${CI_BASE}:${SHA}"
|
||||
|
||||
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
|
||||
rm -f /tmp/gcp-key.json
|
||||
|
||||
- name: Dispatch el-sdk-updated to downstream repos
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.neuralplatform.ai/api/v1
|
||||
run: |
|
||||
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
|
||||
for repo in neuron-technologies/forge; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_get(json: String, key: String) -> String
|
||||
extern fn generate_frame(frame: [String]) -> String
|
||||
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
|
||||
extern fn generate_frame(frame: Any) -> String
|
||||
extern fn generate_frame_lang(frame: Any, lang_code: String) -> String
|
||||
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> Any
|
||||
extern fn generate(semantic_form_json: String) -> String
|
||||
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
|
||||
|
||||
+28
-28
@@ -1,22 +1,22 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn slots_get(slots: [String], key: String) -> String
|
||||
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
|
||||
extern fn make_slots(k0: String, v0: String) -> [String]
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
|
||||
extern fn rule_id(rule: [String]) -> String
|
||||
extern fn rule_lhs(rule: [String]) -> String
|
||||
extern fn rule_rhs_len(rule: [String]) -> Int
|
||||
extern fn rule_rhs(rule: [String], idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
|
||||
extern fn build_rules() -> [[String]]
|
||||
extern fn get_rules() -> [[String]]
|
||||
extern fn find_rule(rule_id_str: String) -> [String]
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn slots_get(slots: Any, key: String) -> String
|
||||
extern fn slots_set(slots: Any, key: String, val: String) -> Any
|
||||
extern fn make_slots(k0: String, v0: String) -> Any
|
||||
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> Any
|
||||
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> Any
|
||||
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> Any
|
||||
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> Any
|
||||
extern fn rule_id(rule: Any) -> String
|
||||
extern fn rule_lhs(rule: Any) -> String
|
||||
extern fn rule_rhs_len(rule: Any) -> Int
|
||||
extern fn rule_rhs(rule: Any, idx: Int) -> String
|
||||
extern fn make_rule(id: String, lhs: String, r0: String) -> Any
|
||||
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> Any
|
||||
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> Any
|
||||
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> Any
|
||||
extern fn build_rules() -> Any
|
||||
extern fn get_rules() -> Any
|
||||
extern fn find_rule(rule_id_str: String) -> Any
|
||||
extern fn make_leaf(label: String, word: String) -> String
|
||||
extern fn make_node1(label: String, child0: String) -> String
|
||||
extern fn make_node2(label: String, child0: String, child1: String) -> String
|
||||
@@ -24,15 +24,15 @@ extern fn make_node3(label: String, child0: String, child1: String, child2: Stri
|
||||
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
|
||||
extern fn nlg_is_ws(c: String) -> Bool
|
||||
extern fn skip_ws(s: String, pos: Int) -> Int
|
||||
extern fn scan_token(s: String, start: Int) -> [String]
|
||||
extern fn scan_token(s: String, start: Int) -> Any
|
||||
extern fn render_tree(tree: String) -> String
|
||||
extern fn gram_word_order(profile: [String]) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
|
||||
extern fn gram_question_strategy(profile: [String]) -> String
|
||||
extern fn gram_word_order(profile: Any) -> String
|
||||
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: Any) -> String
|
||||
extern fn gram_build_vp(verb: String, aux: String, profile: Any) -> String
|
||||
extern fn gram_question_strategy(profile: Any) -> String
|
||||
extern fn is_pronoun(word: String) -> Bool
|
||||
extern fn build_np(referent: String, slots: [String]) -> String
|
||||
extern fn build_np(referent: String, slots: Any) -> String
|
||||
extern fn build_pp(loc: String) -> String
|
||||
extern fn build_vp_body(slots: [String]) -> String
|
||||
extern fn build_vp_from_slots(slots: [String]) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
|
||||
extern fn build_vp_body(slots: Any) -> String
|
||||
extern fn build_vp_from_slots(slots: Any) -> String
|
||||
extern fn generate_tree(rule_id_str: String, slots: Any) -> String
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
|
||||
extern fn lang_get(profile: [String], key: String) -> String
|
||||
extern fn lang_profile_en() -> [String]
|
||||
extern fn lang_profile_ja() -> [String]
|
||||
extern fn lang_profile_ar() -> [String]
|
||||
extern fn lang_profile_zh() -> [String]
|
||||
extern fn lang_profile_de() -> [String]
|
||||
extern fn lang_profile_es() -> [String]
|
||||
extern fn lang_profile_fi() -> [String]
|
||||
extern fn lang_profile_sw() -> [String]
|
||||
extern fn lang_profile_hi() -> [String]
|
||||
extern fn lang_profile_ru() -> [String]
|
||||
extern fn lang_profile_fr() -> [String]
|
||||
extern fn lang_profile_la() -> [String]
|
||||
extern fn lang_profile_he() -> [String]
|
||||
extern fn lang_profile_sa() -> [String]
|
||||
extern fn lang_profile_got() -> [String]
|
||||
extern fn lang_profile_non() -> [String]
|
||||
extern fn lang_profile_enm() -> [String]
|
||||
extern fn lang_profile_pi() -> [String]
|
||||
extern fn lang_profile_grc() -> [String]
|
||||
extern fn lang_profile_ang() -> [String]
|
||||
extern fn lang_profile_fro() -> [String]
|
||||
extern fn lang_profile_goh() -> [String]
|
||||
extern fn lang_profile_sga() -> [String]
|
||||
extern fn lang_profile_txb() -> [String]
|
||||
extern fn lang_profile_peo() -> [String]
|
||||
extern fn lang_profile_akk() -> [String]
|
||||
extern fn lang_profile_uga() -> [String]
|
||||
extern fn lang_profile_egy() -> [String]
|
||||
extern fn lang_profile_sux() -> [String]
|
||||
extern fn lang_profile_gez() -> [String]
|
||||
extern fn lang_profile_cop() -> [String]
|
||||
extern fn lang_from_code(code: String) -> [String]
|
||||
extern fn lang_default() -> [String]
|
||||
extern fn lang_is_isolating(profile: [String]) -> Bool
|
||||
extern fn lang_is_agglutinative(profile: [String]) -> Bool
|
||||
extern fn lang_is_fusional(profile: [String]) -> Bool
|
||||
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
|
||||
extern fn lang_is_rtl(profile: [String]) -> Bool
|
||||
extern fn lang_has_null_subject(profile: [String]) -> Bool
|
||||
extern fn lang_has_case(profile: [String]) -> Bool
|
||||
extern fn lang_has_gender(profile: [String]) -> Bool
|
||||
extern fn lang_word_order(profile: [String]) -> String
|
||||
extern fn lang_code(profile: [String]) -> String
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> Any
|
||||
extern fn lang_get(profile: Any, key: String) -> String
|
||||
extern fn lang_profile_en() -> Any
|
||||
extern fn lang_profile_ja() -> Any
|
||||
extern fn lang_profile_ar() -> Any
|
||||
extern fn lang_profile_zh() -> Any
|
||||
extern fn lang_profile_de() -> Any
|
||||
extern fn lang_profile_es() -> Any
|
||||
extern fn lang_profile_fi() -> Any
|
||||
extern fn lang_profile_sw() -> Any
|
||||
extern fn lang_profile_hi() -> Any
|
||||
extern fn lang_profile_ru() -> Any
|
||||
extern fn lang_profile_fr() -> Any
|
||||
extern fn lang_profile_la() -> Any
|
||||
extern fn lang_profile_he() -> Any
|
||||
extern fn lang_profile_sa() -> Any
|
||||
extern fn lang_profile_got() -> Any
|
||||
extern fn lang_profile_non() -> Any
|
||||
extern fn lang_profile_enm() -> Any
|
||||
extern fn lang_profile_pi() -> Any
|
||||
extern fn lang_profile_grc() -> Any
|
||||
extern fn lang_profile_ang() -> Any
|
||||
extern fn lang_profile_fro() -> Any
|
||||
extern fn lang_profile_goh() -> Any
|
||||
extern fn lang_profile_sga() -> Any
|
||||
extern fn lang_profile_txb() -> Any
|
||||
extern fn lang_profile_peo() -> Any
|
||||
extern fn lang_profile_akk() -> Any
|
||||
extern fn lang_profile_uga() -> Any
|
||||
extern fn lang_profile_egy() -> Any
|
||||
extern fn lang_profile_sux() -> Any
|
||||
extern fn lang_profile_gez() -> Any
|
||||
extern fn lang_profile_cop() -> Any
|
||||
extern fn lang_from_code(code: String) -> Any
|
||||
extern fn lang_default() -> Any
|
||||
extern fn lang_is_isolating(profile: Any) -> Bool
|
||||
extern fn lang_is_agglutinative(profile: Any) -> Bool
|
||||
extern fn lang_is_fusional(profile: Any) -> Bool
|
||||
extern fn lang_is_polysynthetic(profile: Any) -> Bool
|
||||
extern fn lang_is_rtl(profile: Any) -> Bool
|
||||
extern fn lang_has_null_subject(profile: Any) -> Bool
|
||||
extern fn lang_has_case(profile: Any) -> Bool
|
||||
extern fn lang_has_gender(profile: Any) -> Bool
|
||||
extern fn lang_word_order(profile: Any) -> String
|
||||
extern fn lang_code(profile: Any) -> String
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn akk_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn akk_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn akk_str_len(s: String) -> Int
|
||||
extern fn akk_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn ang_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn ang_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn ang_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn ang_str_last_char(s: String) -> String
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn ar_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn ar_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn ar_str_len(s: String) -> Int
|
||||
extern fn ar_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
|
||||
// ── String helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn cop_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn cop_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn cop_str_len(s: String) -> Int
|
||||
extern fn cop_drop(s: String, n: Int) -> String
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
// Dat: dem der dem den
|
||||
// Gen: des der des der
|
||||
|
||||
import "morphology.el"
|
||||
fn de_article_def(gender: String, gram_case: String, number: String) -> String {
|
||||
if str_eq(number, "pl") {
|
||||
if str_eq(gram_case, "nom") { return "die" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn de_article_def(gender: String, gram_case: String, number: String) -> String
|
||||
extern fn de_article_indef(gender: String, gram_case: String, number: String) -> String
|
||||
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
|
||||
// ── String helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn egy_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn egy_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn egy_str_len(s: String) -> Int
|
||||
extern fn egy_drop(s: String, n: Int) -> String
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn enm_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn enm_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn enm_drop(s: String, n: Int) -> String
|
||||
extern fn enm_first_char(s: String) -> String
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
// ── String helpers (local, matching morphology.el conventions) ────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn es_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn es_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn es_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn es_str_last_char(s: String) -> String
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
// If only neutral vowels are found, default to "front" (the conservative choice
|
||||
// for borrowed words and those without clear back vowels).
|
||||
|
||||
import "morphology.el"
|
||||
fn fi_harmony(word: String) -> String {
|
||||
let n: Int = str_len(word)
|
||||
let i: Int = n - 1
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn fi_harmony(word: String) -> String
|
||||
extern fn fi_suffix(base: String, harmony: String) -> String
|
||||
extern fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
|
||||
extern fn fi_str_last_char(s: String) -> String
|
||||
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
|
||||
extern fn fi_verb_stem(dict_form: String) -> String
|
||||
extern fn fi_irregular_verb(dict_form: String) -> [String]
|
||||
extern fn fi_irregular_verb(dict_form: String) -> Any
|
||||
extern fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String
|
||||
extern fn fi_past_stem(stem: String) -> String
|
||||
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String
|
||||
@@ -14,4 +14,4 @@ extern fn fi_negative(verb: String, person: String, number: String) -> String
|
||||
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
|
||||
extern fn fi_question_suffix(harmony: String) -> String
|
||||
extern fn fi_make_question(verb_form: String, harmony: String) -> String
|
||||
extern fn fi_full_paradigm(noun: String) -> [String]
|
||||
extern fn fi_full_paradigm(noun: String) -> Any
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
// ── String helpers (local, matching morphology.el conventions) ────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn fr_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn fr_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn fr_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn fr_str_last_char(s: String) -> String
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn fro_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn fro_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn fro_drop(s: String, n: Int) -> String
|
||||
extern fn fro_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn gez_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn gez_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn gez_str_len(s: String) -> Int
|
||||
extern fn gez_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn goh_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn goh_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn goh_drop(s: String, n: Int) -> String
|
||||
extern fn goh_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn got_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn got_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn got_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn got_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn grc_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn grc_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn grc_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn grc_str_last_char(s: String) -> String
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn he_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn he_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn he_str_len(s: String) -> Int
|
||||
extern fn he_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn hi_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn hi_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn hi_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn hi_str_last_char(s: String) -> String
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
// Note: this is a heuristic classifier for romanized input. For production use
|
||||
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
|
||||
|
||||
import "morphology.el"
|
||||
fn ja_verb_group(dict_form: String) -> String {
|
||||
// Irregular verbs (exact match on dictionary form)
|
||||
if str_eq(dict_form, "する") { return "irregular" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn ja_verb_group(dict_form: String) -> String
|
||||
extern fn ja_ichidan_stem(dict_form: String) -> String
|
||||
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn la_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn la_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn la_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn la_str_last_char(s: String) -> String
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn non_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn non_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn non_drop(s: String, n: Int) -> String
|
||||
extern fn non_last(s: String) -> String
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn peo_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn peo_drop(s: String, n: Int) -> String
|
||||
extern fn peo_ends(s: String, suf: String) -> Bool
|
||||
extern fn peo_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn pi_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn pi_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn pi_drop(s: String, n: Int) -> String
|
||||
extern fn pi_last_char(s: String) -> String
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
// The heuristic returns the most probable gender. Caller should override
|
||||
// for known exceptions (путь, рубль are masc despite -ь).
|
||||
|
||||
import "morphology.el"
|
||||
fn ru_gender(noun: String) -> String {
|
||||
let n: Int = str_len(noun)
|
||||
if n == 0 { return "m" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn ru_gender(noun: String) -> String
|
||||
extern fn ru_stem_type(noun: String, gender: String) -> String
|
||||
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sa_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sa_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sa_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sa_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sga_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sga_drop(s: String, n: Int) -> String
|
||||
extern fn sga_first(s: String) -> String
|
||||
extern fn sga_rest(s: String) -> String
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sux_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sux_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sux_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sux_str_last_char(s: String) -> String
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn sw_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sw_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn sw_str_drop_last(s: String, n: Int) -> String
|
||||
extern fn sw_str_first_char(s: String) -> String
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn txb_drop(s: String, n: Int) -> String {
|
||||
let len: Int = str_len(s)
|
||||
if n >= len { return "" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn txb_drop(s: String, n: Int) -> String
|
||||
extern fn txb_ends(s: String, suf: String) -> Bool
|
||||
extern fn txb_slot(person: String, number: String) -> Int
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
|
||||
// ── String helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
import "morphology.el"
|
||||
fn uga_str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn uga_str_ends(s: String, suf: String) -> Bool
|
||||
extern fn uga_str_len(s: String) -> Int
|
||||
extern fn uga_str_drop_last(s: String, n: Int) -> String
|
||||
|
||||
@@ -33,17 +33,6 @@
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
import "language-profile.el"
|
||||
import "morphology-es.el"
|
||||
import "morphology-fr.el"
|
||||
import "morphology-de.el"
|
||||
import "morphology-ru.el"
|
||||
import "morphology-fi.el"
|
||||
import "morphology-ar.el"
|
||||
import "morphology-hi.el"
|
||||
import "morphology-sw.el"
|
||||
import "morphology-la.el"
|
||||
import "morphology-ja.el"
|
||||
fn str_ends(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(s, suf)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn str_ends(s: String, suf: String) -> Bool
|
||||
extern fn str_last_char(s: String) -> String
|
||||
extern fn str_last2(s: String) -> String
|
||||
@@ -8,7 +8,7 @@ extern fn is_vowel(c: String) -> Bool
|
||||
extern fn morph_apply_suffix(base: String, suffix: String) -> String
|
||||
extern fn en_irregular_plural(word: String) -> String
|
||||
extern fn en_irregular_singular(word: String) -> String
|
||||
extern fn en_irregular_verb(base: String) -> [String]
|
||||
extern fn en_irregular_verb(base: String) -> Any
|
||||
extern fn en_verb_3sg(base: String) -> String
|
||||
extern fn en_should_double_final(base: String) -> Bool
|
||||
extern fn en_verb_past(base: String) -> String
|
||||
@@ -16,10 +16,10 @@ extern fn en_verb_gerund(base: String) -> String
|
||||
extern fn en_pluralize_regular(singular: String) -> String
|
||||
extern fn en_verb_form(base: String, tense: String, person: String, number: String) -> String
|
||||
extern fn agree_determiner(det: String, noun: String) -> String
|
||||
extern fn morph_pluralize(noun: String, profile: [String]) -> String
|
||||
extern fn morph_pluralize(noun: String, profile: Any) -> String
|
||||
extern fn morph_map_canonical(verb: String, code: String) -> String
|
||||
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: [String]) -> String
|
||||
extern fn morph_inflect(word: String, features: String, profile: [String]) -> String
|
||||
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: Any) -> String
|
||||
extern fn morph_inflect(word: String, features: String, profile: Any) -> String
|
||||
extern fn pluralize(singular: String) -> String
|
||||
extern fn singularize(plural: String) -> String
|
||||
extern fn verb_form(base: String, tense: String, person: String, number: String) -> String
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn agent_person(agent: String) -> String
|
||||
extern fn agent_number(agent: String) -> String
|
||||
extern fn realize_np(referent: String, number: String) -> String
|
||||
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String]
|
||||
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String
|
||||
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: Any) -> Any
|
||||
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: Any) -> String
|
||||
extern fn capitalize_first(s: String) -> String
|
||||
extern fn add_punct(s: String, intent: String) -> String
|
||||
extern fn realize_lang(form: [String], profile: [String]) -> String
|
||||
extern fn realize(form: [String]) -> String
|
||||
extern fn realize_lang(form: Any, profile: Any) -> String
|
||||
extern fn realize(form: Any) -> String
|
||||
|
||||
+15
-15
@@ -1,18 +1,18 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String]
|
||||
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String]
|
||||
extern fn sem_frame_simple(intent: String, subject: String) -> [String]
|
||||
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String]
|
||||
extern fn sem_intent(frame: [String]) -> String
|
||||
extern fn sem_subject(frame: [String]) -> String
|
||||
extern fn sem_object(frame: [String]) -> String
|
||||
extern fn sem_modifiers(frame: [String]) -> String
|
||||
extern fn sem_lang(frame: [String]) -> String
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> Any
|
||||
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> Any
|
||||
extern fn sem_frame_simple(intent: String, subject: String) -> Any
|
||||
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> Any
|
||||
extern fn sem_intent(frame: Any) -> String
|
||||
extern fn sem_subject(frame: Any) -> String
|
||||
extern fn sem_object(frame: Any) -> String
|
||||
extern fn sem_modifiers(frame: Any) -> String
|
||||
extern fn sem_lang(frame: Any) -> String
|
||||
extern fn sem_first_modifier(mods: String) -> String
|
||||
extern fn sem_intent_to_realize(intent: String) -> String
|
||||
extern fn sem_to_spec(frame: [String]) -> [String]
|
||||
extern fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String]
|
||||
extern fn sem_to_spec(frame: Any) -> Any
|
||||
extern fn sem_to_spec_full(frame: Any, verb: String, tense: String, aspect: String) -> Any
|
||||
extern fn sem_realize_greet(subject: String) -> String
|
||||
extern fn sem_realize(frame: [String]) -> String
|
||||
extern fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String
|
||||
extern fn sem_realize_lang(frame: [String], lang_code: String) -> String
|
||||
extern fn sem_realize(frame: Any) -> String
|
||||
extern fn sem_realize_full(frame: Any, verb: String, tense: String, aspect: String) -> String
|
||||
extern fn sem_realize_lang(frame: Any, lang_code: String) -> String
|
||||
|
||||
+19
-19
@@ -1,20 +1,20 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn lex_word(entry: [String]) -> String
|
||||
extern fn lex_pos(entry: [String]) -> String
|
||||
extern fn lex_form(entry: [String], idx: Int) -> String
|
||||
extern fn lex_class(entry: [String]) -> String
|
||||
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> [String]
|
||||
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> [String]
|
||||
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> [String]
|
||||
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> [String]
|
||||
extern fn build_vocab() -> [[String]]
|
||||
extern fn get_vocab() -> [[String]]
|
||||
extern fn vocab_lookup(word: String, lang_code: String) -> [String]
|
||||
extern fn vocab_lookup_en(word: String) -> [String]
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn lex_word(entry: Any) -> String
|
||||
extern fn lex_pos(entry: Any) -> String
|
||||
extern fn lex_form(entry: Any, idx: Int) -> String
|
||||
extern fn lex_class(entry: Any) -> String
|
||||
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> Any
|
||||
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> Any
|
||||
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> Any
|
||||
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> Any
|
||||
extern fn build_vocab() -> Any
|
||||
extern fn get_vocab() -> Any
|
||||
extern fn vocab_lookup(word: String, lang_code: String) -> Any
|
||||
extern fn vocab_lookup_en(word: String) -> Any
|
||||
extern fn vocab_synonym(word: String, lang_register: String, lang_code: String) -> String
|
||||
extern fn vocab_by_pos(pos: String) -> [[String]]
|
||||
extern fn vocab_by_class(cls: String) -> [[String]]
|
||||
extern fn entry_found(entry: [String]) -> Bool
|
||||
extern fn entry_word(entry: [String]) -> String
|
||||
extern fn entry_pos(entry: [String]) -> String
|
||||
extern fn entry_form(entry: [String], n: Int) -> String
|
||||
extern fn vocab_by_pos(pos: String) -> Any
|
||||
extern fn vocab_by_class(cls: String) -> Any
|
||||
extern fn entry_found(entry: Any) -> Bool
|
||||
extern fn entry_word(entry: Any) -> String
|
||||
extern fn entry_pos(entry: Any) -> String
|
||||
extern fn entry_form(entry: Any, n: Int) -> String
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
cc -std=c11 -O2 \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+69
-152
@@ -10,7 +10,6 @@ el_val_t query_param(el_val_t path, el_val_t key);
|
||||
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
|
||||
el_val_t extract_id(el_val_t path, el_val_t prefix);
|
||||
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t persist_canonical(void);
|
||||
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body);
|
||||
@@ -24,20 +23,13 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
||||
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
|
||||
|
||||
el_val_t bind_raw;
|
||||
el_val_t bind_str;
|
||||
el_val_t port;
|
||||
el_val_t data_dir_raw;
|
||||
el_val_t data_dir;
|
||||
el_val_t snapshot_path;
|
||||
el_val_t boot_snap;
|
||||
|
||||
el_val_t parse_port(el_val_t bind) {
|
||||
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
||||
@@ -116,22 +108,17 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t persist_canonical(void) {
|
||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
el_val_t dir = ({ el_val_t _if_result_1 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_1 = (EL_STR("/tmp/engram")); } else { _if_result_1 = (dir_raw); } _if_result_1; });
|
||||
engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
|
||||
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
|
||||
el_val_t sal_raw = json_get_float(body, EL_STR("salience"));
|
||||
el_val_t salience = ({ el_val_t _if_result_3 = 0; if ((sal_raw == el_from_float(0.0))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (sal_raw); } _if_result_3; });
|
||||
el_val_t node_type = json_get_string(body, EL_STR("node_type"));
|
||||
if (str_eq(node_type, EL_STR(""))) {
|
||||
node_type = EL_STR("Memory");
|
||||
}
|
||||
el_val_t salience = json_get_float(body, EL_STR("salience"));
|
||||
if (str_eq(salience, el_from_float(0.0))) {
|
||||
salience = el_from_float(0.5);
|
||||
}
|
||||
el_val_t id = engram_node(content, node_type, salience);
|
||||
el_val_t saved = persist_canonical();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -157,9 +144,11 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
|
||||
}
|
||||
|
||||
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
el_val_t dir = ({ el_val_t _if_result_4 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_4 = (EL_STR("/tmp/engram")); } else { _if_result_4 = (dir_raw); } _if_result_4; });
|
||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
engram_save(snap_path);
|
||||
el_val_t snap = fs_read(snap_path);
|
||||
if (str_eq(snap, EL_STR(""))) {
|
||||
@@ -174,22 +163,36 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
||||
}
|
||||
|
||||
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t q = ({ el_val_t _if_result_5 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_5 = (query_param(path, EL_STR("q"))); } else { _if_result_5 = (json_get_string(body, EL_STR("query"))); } _if_result_5; });
|
||||
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
|
||||
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
|
||||
el_val_t lim_either = ({ el_val_t _if_result_6 = 0; if ((lim_url > 0)) { _if_result_6 = (lim_url); } else { _if_result_6 = (lim_body); } _if_result_6; });
|
||||
el_val_t limit = ({ el_val_t _if_result_7 = 0; if ((lim_either > 0)) { _if_result_7 = (lim_either); } else { _if_result_7 = (20); } _if_result_7; });
|
||||
el_val_t q = EL_STR("");
|
||||
if (str_eq(method, EL_STR("GET"))) {
|
||||
q = query_param(path, EL_STR("q"));
|
||||
} else {
|
||||
q = json_get_string(body, EL_STR("query"));
|
||||
}
|
||||
el_val_t limit = query_int(path, EL_STR("limit"), 20);
|
||||
if (limit == 0) {
|
||||
limit = json_get_int(body, EL_STR("limit"));
|
||||
}
|
||||
if (limit == 0) {
|
||||
limit = 20;
|
||||
}
|
||||
return engram_search_json(q, limit);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t q = ({ el_val_t _if_result_8 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_8 = (query_param(path, EL_STR("q"))); } else { _if_result_8 = (json_get_string(body, EL_STR("query"))); } _if_result_8; });
|
||||
if (str_eq(q, EL_STR(""))) {
|
||||
return err_json(EL_STR("missing query"));
|
||||
el_val_t q = EL_STR("");
|
||||
el_val_t depth = 3;
|
||||
if (str_eq(method, EL_STR("GET"))) {
|
||||
q = query_param(path, EL_STR("q"));
|
||||
depth = query_int(path, EL_STR("depth"), 3);
|
||||
} else {
|
||||
q = json_get_string(body, EL_STR("query"));
|
||||
el_val_t bd = json_get_int(body, EL_STR("depth"));
|
||||
if (bd > 0) {
|
||||
depth = bd;
|
||||
}
|
||||
}
|
||||
el_val_t d_raw = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_9 = (json_get_int(body, EL_STR("depth"))); } _if_result_9; });
|
||||
el_val_t depth = ({ el_val_t _if_result_10 = 0; if ((d_raw > 0)) { _if_result_10 = (d_raw); } else { _if_result_10 = (3); } _if_result_10; });
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -197,12 +200,15 @@ el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
|
||||
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
|
||||
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
|
||||
el_val_t relation = ({ el_val_t _if_result_11 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_11 = (EL_STR("associates")); } else { _if_result_11 = (rel_raw); } _if_result_11; });
|
||||
el_val_t w_raw = json_get_float(body, EL_STR("weight"));
|
||||
el_val_t weight = ({ el_val_t _if_result_12 = 0; if ((w_raw == el_from_float(0.0))) { _if_result_12 = (el_from_float(0.5)); } else { _if_result_12 = (w_raw); } _if_result_12; });
|
||||
el_val_t relation = json_get_string(body, EL_STR("relation"));
|
||||
if (str_eq(relation, EL_STR(""))) {
|
||||
relation = EL_STR("associates");
|
||||
}
|
||||
el_val_t weight = json_get_float(body, EL_STR("weight"));
|
||||
if (str_eq(weight, el_from_float(0.0))) {
|
||||
weight = el_from_float(0.5);
|
||||
}
|
||||
engram_connect(from_id, to_id, weight, relation);
|
||||
el_val_t saved = persist_canonical();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -223,7 +229,6 @@ el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return err_json(EL_STR("missing node_id"));
|
||||
}
|
||||
engram_strengthen(id);
|
||||
el_val_t saved = persist_canonical();
|
||||
return ok_json();
|
||||
return 0;
|
||||
}
|
||||
@@ -234,26 +239,33 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return err_json(EL_STR("missing id"));
|
||||
}
|
||||
engram_forget(id);
|
||||
el_val_t saved = persist_canonical();
|
||||
return ok_json();
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t p_raw = json_get_string(body, EL_STR("path"));
|
||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
el_val_t dir = ({ el_val_t _if_result_13 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_13 = (EL_STR("/tmp/engram")); } else { _if_result_13 = (dir_raw); } _if_result_13; });
|
||||
el_val_t p = ({ el_val_t _if_result_14 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_14 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_14 = (p_raw); } _if_result_14; });
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
}
|
||||
engram_save(p);
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t p_raw = json_get_string(body, EL_STR("path"));
|
||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
el_val_t dir = ({ el_val_t _if_result_15 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_15 = (EL_STR("/tmp/engram")); } else { _if_result_15 = (dir_raw); } _if_result_15; });
|
||||
el_val_t p = ({ el_val_t _if_result_16 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_16 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_16 = (p_raw); } _if_result_16; });
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||
}
|
||||
engram_load(p);
|
||||
return ok_json();
|
||||
return 0;
|
||||
@@ -264,84 +276,6 @@ el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
el_val_t dir = ({ el_val_t _if_result_17 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_17 = (EL_STR("/tmp/engram")); } else { _if_result_17 = (dir_raw); } _if_result_17; });
|
||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
|
||||
engram_save(snap_path);
|
||||
el_val_t snap = fs_read(snap_path);
|
||||
if (str_eq(snap, EL_STR(""))) {
|
||||
return EL_STR("{\"nodes\":[],\"edges\":[]}");
|
||||
}
|
||||
return snap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||
if (str_eq(p, EL_STR(""))) {
|
||||
return err_json(EL_STR("path is required"));
|
||||
}
|
||||
if (str_eq(fs_read(p), EL_STR(""))) {
|
||||
return err_json(EL_STR("file missing or empty"));
|
||||
}
|
||||
el_val_t before_n = engram_node_count();
|
||||
el_val_t before_e = engram_edge_count();
|
||||
engram_load_merge(p);
|
||||
el_val_t added_n = (engram_node_count() - before_n);
|
||||
el_val_t added_e = (engram_edge_count() - before_e);
|
||||
el_val_t saved = persist_canonical();
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"nodes_added\":"), int_to_str(added_n)), EL_STR(",\"edges_added\":")), int_to_str(added_e)), EL_STR(",\"node_count\":")), int_to_str(engram_node_count())), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return err_json(EL_STR("missing content"));
|
||||
}
|
||||
el_val_t sal = el_from_float(0.3);
|
||||
el_val_t imp = el_from_float(0.3);
|
||||
el_val_t conf = el_from_float(0.8);
|
||||
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
|
||||
el_val_t ret_raw = env(EL_STR("ENGRAM_ISE_RETENTION_MS"));
|
||||
el_val_t ret_ms = ({ el_val_t _if_result_18 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_18 = (172800000); } else { _if_result_18 = (str_to_int(ret_raw)); } _if_result_18; });
|
||||
el_val_t pruned = engram_prune_telemetry(ret_ms);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"pruned\":")), int_to_str(pruned)), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body) {
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return err_json(EL_STR("missing content"));
|
||||
}
|
||||
el_val_t title = json_get_string(body, EL_STR("title"));
|
||||
el_val_t label = ({ el_val_t _if_result_19 = 0; if (str_eq(title, EL_STR(""))) { _if_result_19 = (str_slice(content, 0, 60)); } else { _if_result_19 = (title); } _if_result_19; });
|
||||
el_val_t category_raw = json_get_string(body, EL_STR("category"));
|
||||
el_val_t category = ({ el_val_t _if_result_20 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_20 = (EL_STR("other")); } else { _if_result_20 = (category_raw); } _if_result_20; });
|
||||
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
|
||||
el_val_t ktier = ({ el_val_t _if_result_21 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_21 = (EL_STR("note")); } else { _if_result_21 = (ktier_raw); } _if_result_21; });
|
||||
el_val_t project = json_get_string(body, EL_STR("project"));
|
||||
el_val_t tags_raw = json_get_raw(body, EL_STR("tags"));
|
||||
el_val_t tags_base = ({ el_val_t _if_result_22 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_22 = (EL_STR("[]")); } else { _if_result_22 = (tags_raw); } _if_result_22; });
|
||||
el_val_t base_len = str_len(tags_base);
|
||||
el_val_t head = str_slice(tags_base, 0, (base_len - 1));
|
||||
el_val_t sep = ({ el_val_t _if_result_23 = 0; if (str_eq(head, EL_STR("["))) { _if_result_23 = (EL_STR("")); } else { _if_result_23 = (EL_STR(",")); } _if_result_23; });
|
||||
el_val_t safe_cat = str_replace(category, EL_STR("\""), EL_STR("'"));
|
||||
el_val_t safe_tier = str_replace(ktier, EL_STR("\""), EL_STR("'"));
|
||||
el_val_t safe_proj = str_replace(project, EL_STR("\""), EL_STR("'"));
|
||||
el_val_t proj_tag = ({ el_val_t _if_result_24 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_24 = (EL_STR("")); } else { _if_result_24 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_24; });
|
||||
el_val_t tags = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(head, sep), EL_STR("\"category:")), safe_cat), EL_STR("\",\"tier:")), safe_tier), EL_STR("\"")), proj_tag), EL_STR("]"));
|
||||
el_val_t sal = el_from_float(0.5);
|
||||
el_val_t imp = el_from_float(0.5);
|
||||
el_val_t conf = el_from_float(0.9);
|
||||
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), label, sal, imp, conf, EL_STR("Semantic"), tags);
|
||||
el_val_t saved = persist_canonical();
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t check_auth_ok(el_val_t method, el_val_t body) {
|
||||
el_val_t key = env(EL_STR("ENGRAM_API_KEY"));
|
||||
if (str_eq(key, EL_STR(""))) {
|
||||
@@ -365,15 +299,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return route_health(method, path, body);
|
||||
}
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) {
|
||||
return route_emit_ise(method, path, body);
|
||||
}
|
||||
if (!check_auth_ok(method, body)) {
|
||||
return err_json(EL_STR("unauthorized"));
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
|
||||
return route_capture_knowledge(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
|
||||
return route_stats(method, path, body);
|
||||
}
|
||||
@@ -419,34 +347,23 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
||||
return route_load(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load-merge")) || str_eq(clean, EL_STR("/load-merge")))) {
|
||||
return route_load_merge(method, path, body);
|
||||
}
|
||||
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
|
||||
return route_sync(method, path, body);
|
||||
}
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int _argc, char** _argv) {
|
||||
el_runtime_init_args(_argc, _argv);
|
||||
bind_raw = env(EL_STR("ENGRAM_BIND"));
|
||||
bind_str = ({ el_val_t _if_result_25 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_25 = (EL_STR(":8742")); } else { _if_result_25 = (bind_raw); } _if_result_25; });
|
||||
bind_str = env(EL_STR("ENGRAM_BIND"));
|
||||
if (str_eq(bind_str, EL_STR(""))) {
|
||||
bind_str = EL_STR(":8742");
|
||||
}
|
||||
port = parse_port(bind_str);
|
||||
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
data_dir = ({ el_val_t _if_result_26 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (data_dir_raw); } _if_result_26; });
|
||||
data_dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(data_dir, EL_STR(""))) {
|
||||
data_dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
||||
engram_load(snapshot_path);
|
||||
boot_snap = fs_read(snapshot_path);
|
||||
if (!str_eq(boot_snap, EL_STR(""))) {
|
||||
if (engram_node_count() == 0) {
|
||||
println(EL_STR("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes \xe2\x80\x94 preserving copy at snapshot.failed-load.json"));
|
||||
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.failed-load.json")), boot_snap);
|
||||
} else {
|
||||
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.boot-backup.json")), boot_snap);
|
||||
}
|
||||
}
|
||||
println(EL_STR("[engram] runtime-native graph engine"));
|
||||
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
||||
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
|
||||
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
|
||||
sequences live promotion after the engine/HNSW cutover settles.
|
||||
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
|
||||
|
||||
Grounding mechanism designed with Will (memory `9e09a59f`, refining
|
||||
`1a861007`). This is the HOW for #50.
|
||||
|
||||
---
|
||||
|
||||
## (a) How grounded edge-propagation integrates into the dream/consolidation cycle
|
||||
|
||||
The beat already exists. `neuron/awareness.el` runs a heartbeat (~every
|
||||
`beat_ms`); each beat calls `hebb_consolidate()` — which drains the self-formed
|
||||
Hebbian associations out of the fast in-process store and writes them, over the
|
||||
threshold `ENGRAM_HEBB_LINK_MIN`, into the durable engram (`:8742`) — and then
|
||||
`emit_heartbeat()`.
|
||||
|
||||
Grounded edge-propagation slots into the **same beat, immediately after
|
||||
consolidation** (awareness.el line 1286–1288):
|
||||
|
||||
```
|
||||
hebb_consolidate() // lay down the tethers (edges) that cleared threshold
|
||||
ground_propagate() // <-- NEW: grade beliefs ALONG those tethers
|
||||
emit_heartbeat() // report gep_* gauges beside hebb_*
|
||||
```
|
||||
|
||||
This ordering is the point. Consolidation lays down the wiring; propagation
|
||||
grades the beliefs along it, in the same breath. Memory `69b8babe`:
|
||||
memory-consolidation and staying-yourself are one physics — forming a memory and
|
||||
grading a belief are the same gravity run in two passes of one beat.
|
||||
|
||||
The propagation runs **inside the engram** as the native
|
||||
`engram_ground_propagate()` over the durable flat node/edge arrays (the store
|
||||
the consolidated edges just landed in). The soul invokes it over HTTP
|
||||
(`POST /api/ground/propagate`) and folds the returned `gep_*` telemetry into the
|
||||
heartbeat stream next to `hebb_cands / hebb_mass / hebb_edges`.
|
||||
|
||||
**Bounded by construction** (per the live-graph reality — 70.7% of nodes
|
||||
isolated, connected core ~28%, hub first-hop fan-out in the thousands):
|
||||
- **1-hop only.** No BFS spreading activation — a belief is graded from its
|
||||
DIRECT grounded neighbors, so there is no per-hop breadth explosion.
|
||||
- **Beam-capped** at `GEP_MAX_CORR = 256` corroborators per belief.
|
||||
- **Salience-ordered, `GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat; the rest
|
||||
next beat. Work per beat is O(beliefs × degree), hard-bounded.
|
||||
- **Isolated / starved beliefs** are counted and surfaced (`gep_isolated`,
|
||||
`gep_starved`) as an interoceptive sparse-region signal for the
|
||||
edge-formation / embedding pass (#20). #50 CONSUMES edges; it does not form
|
||||
them. A belief with no grounded neighbor has nothing to tether to — correct
|
||||
per the anti-delusion gravity law (`0b15017c`), not a gap.
|
||||
|
||||
---
|
||||
|
||||
## (b) The implementation
|
||||
|
||||
Represented faithfully to the spec — **grounding is a Hebbian-weighted
|
||||
collection over time, never a scalar.**
|
||||
|
||||
- **Grounding = an append-only event ring** on the node (`GepGrounding`),
|
||||
structurally parallel to the ACT-R base-level access ring already in
|
||||
`EngramNode` (`access_ts[K]`). Each event is `{ts, sign±, mag, corroborator
|
||||
signature}`. Append-only, supersede-not-delete; events aged out of the ring
|
||||
are counted (`older_count`), never faked away.
|
||||
- **Standing is DERIVED, recency-weighted, never stored** —
|
||||
`standing = clamp(GEP_BASE + Σ_events sign·mag·age^(-D), 0, 1)`, exactly the
|
||||
ACT-R base-level shape `ln Σ t^-d` (`ENGRAM_BLL_D = 0.5`) but sign-carrying so
|
||||
LTD subtracts. Memory `1a861007`: the collection is primary, the standing is
|
||||
its emergent aggregate. Mirrored onto `confidence` each beat so downstream
|
||||
reads (verifier #43, realizer calibration `0041d917`) never speak above the
|
||||
grounding.
|
||||
- **Update = LTP/LTD with a threshold.** Per belief, gather corroborators along
|
||||
incident edges, weighted by `edge.weight` (the Hebbian weight) × the
|
||||
neighbor's own standing. **Anti-delusion gravity:** only neighbors already
|
||||
`≥ GEP_LIKELY_MIN` may corroborate — grounding flows FROM the grounded core.
|
||||
- **Convergent INDEPENDENT corroboration** is the driver. Independence is
|
||||
enforced by **union-find over the corroborator set**: two corroborators are
|
||||
the same independent source if they are the same node, reached by multiple
|
||||
edges, or linked to each other (an echo chain / shared derivation). Support is
|
||||
summed **per independent component** (max-magnitude member), and the threshold
|
||||
gate requires BOTH a mass floor (`pos ≥ GEP_THETA`) AND an independence-count
|
||||
floor (`n_independent ≥ GEP_N_MIN`). The count gate is the guard against one
|
||||
node echoed N times.
|
||||
- **Sub-threshold is transient.** Support present but below threshold →
|
||||
`subthreshold_hits++`, no durable event, no lasting shift (Will's exact spec).
|
||||
- **Graduation / decay.** Cross up → LTP event appended → standing climbs
|
||||
`conjecture → likely → grounded`. Contradiction past threshold → LTD →
|
||||
`grounded → likely → conjecture`. Nothing latches; withdraw support and the
|
||||
collection ages and relaxes (`271f1163`, nothing is settled).
|
||||
|
||||
### Files
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `gep_core.h` | The mechanism. Pure C, libm only (own-the-core). Single source of truth: `GepGrounding`, `gep_standing`, `gep_append`, union-find independence, `gep_propagate_node`, `gep_beat`. |
|
||||
| `gep_proof.c` | Self-contained proof harness — builds the three scenarios, prints raw before/after. |
|
||||
| `engram_ground_propagate.staged.c` | GATED runtime native. Wires the SAME `gep_core.h` primitives to the live `EngramStore` (adj cache, flat arrays). Splice plan + relation→polarity + belief gate. Compiles only when spliced (verified: every runtime symbol it references — `engram_adj_rebuild`, `adj_from_len`, `engram_find_node_index`, `ENGRAM_LAYER_SAFETY`, `istr_contains`, … — exists in the release runtime). |
|
||||
| `awareness.beat.patch.el` | GATED beat hook — `ground_propagate()` + the insert between `hebb_consolidate()` and `emit_heartbeat()`. |
|
||||
| `server.route.patch.el` | GATED route — `POST /api/ground/propagate`. |
|
||||
|
||||
### Constants
|
||||
`BASE=0.10 LIKELY_MIN=0.34 GROUNDED_MIN=0.66 N_MIN=3 THETA=0.30 D=0.5`
|
||||
(`N_MIN` parameterizes Will's "13 adjacent things" — the count threshold is a
|
||||
knob; 3 here for a crisp proof.)
|
||||
|
||||
---
|
||||
|
||||
## (c) PROOF LEDGER — raw grounding before/after
|
||||
|
||||
Deterministic. Build `cc -std=c11 -O2 -o gep_proof gep_proof.c -lm`, run
|
||||
`./gep_proof` (full transcript in `PROOF_OUTPUT.txt`).
|
||||
|
||||
### (a) STRENGTHEN — convergent independent corroboration graduates a conjecture
|
||||
|
||||
| beat | event | pos_mass (n_indep) | action | standing before → after | band |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 3 independent grounded corroborators | 0.4050 (3) | **LTP** | 0.1000 → **0.4842** | conjecture → **likely** ⬆ |
|
||||
| 2 | neighborhood grows to 5 | 0.6750 (5) | **LTP** | 0.1496 → **0.7379** | conjecture → **grounded** ⬆ |
|
||||
| 3 | support sustained (5) | 0.6750 (5) | LTP | 0.2110 → 0.7993 | grounded (sustained) |
|
||||
| 4 | corroboration withdrawn (+10min) | 0.0000 (0) | isolated | 0.1612 → 0.1612 | relaxing |
|
||||
| 5 | still withdrawn (+1h) | — | isolated | 0.1263 | relaxing |
|
||||
| 6 | still withdrawn (+4h) | — | isolated | 0.1130 | → conjecture |
|
||||
|
||||
Grounding grew **on its own** past threshold and graduated conjecture → likely →
|
||||
grounded, then **relaxed** once independent support stopped. Living, not a
|
||||
latched flag.
|
||||
|
||||
### (b) DECAY — convergent independent contradiction erodes a grounded belief
|
||||
|
||||
| beat | event | neg_mass (n_indep) | action | standing before → after | band |
|
||||
|---|---|---|---|---|---|
|
||||
| — | seed (prior LTP) | — | — | **0.9500** | grounded |
|
||||
| 1 | 3 independent contradictions | 0.5400 (3) | **LTD** | 0.9500 → **0.4570** | grounded → **likely** ⬇ |
|
||||
| 2 | contradiction broadens to 5 | 0.9000 (5) | **LTD** | 0.1461 → **0.0000** | conjecture ⬇ |
|
||||
| 3–4 | contradiction sustained (5) | 0.9000 (5) | LTD | 0.0000 | conjecture |
|
||||
|
||||
Grounding decayed grounded → likely → conjecture under accreting independent
|
||||
contradiction. The door never shut — history is retained (the event ring keeps
|
||||
growing), the belief stays falsifiable in both directions.
|
||||
|
||||
### (c) INDEPENDENCE GUARD — the load-bearing property
|
||||
|
||||
Identical fan-in (N=5), identical edge weight (0.30), identical corroborator
|
||||
standing (~0.90). **The only difference is whether the five are independent.**
|
||||
|
||||
| sub-case | topology | pos_mass | **n_indep** | action | standing 0.1000 → |
|
||||
|---|---|---|---|---|---|
|
||||
| **C1** | 5 DISTINCT, no inter-links | 1.3500 | **5** | **LTP** | **0.9741 (grounded)** ⬆ |
|
||||
| **C2** | 5 mutually-linked (echo of one source) | 0.2700 | **1** | sub-threshold | 0.1000 (unchanged) |
|
||||
| **C3** | 1 node reached by 5 parallel edges | 0.2700 | **1** | sub-threshold | 0.1000 (unchanged) |
|
||||
|
||||
Same raw fan-in, opposite outcome. Union-find collapses the echoes to a single
|
||||
independent component; the count gate (`n_indep ≥ N_MIN`) then refuses them.
|
||||
**Circular self-reinforcement cannot manufacture grounding** — a conjecture can
|
||||
only be grounded by evidence that is genuinely independent of itself.
|
||||
|
||||
---
|
||||
|
||||
**RAILS honored:** isolated worktree; built/proven on a clone; the live soul
|
||||
(`:8742` / `:7770`) untouched; no fight with the cutover (built against current
|
||||
release source; staged native rebases cleanly onto it); no new libraries
|
||||
(libm only); identity keystones untouched. **Not promoted** — gated artifact +
|
||||
ledger for the main loop to sequence.
|
||||
@@ -1,75 +0,0 @@
|
||||
GROUNDED EDGE-PROPAGATION — PROOF LEDGER (task #50)
|
||||
constants: BASE=0.10 LIKELY_MIN=0.34 GROUNDED_MIN=0.66 N_MIN=3 THETA=0.30 D=0.5
|
||||
|
||||
=== SCENARIO A — STRENGTHEN: convergent independent corroboration ===
|
||||
seed: conjecture has NO grounding events; corroborators pre-grounded.
|
||||
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
|
||||
beat 1 (t=+0s) 3 independent grounded corroborators appear
|
||||
incident_edges=3 pos_mass=0.4050 (n_indep=3) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> LTP (strengthen) standing 0.1000 (conjecture) -> 0.4842 (likely) [GRADUATED]
|
||||
beat 2 (t=+60s) neighborhood grows to 5 corroborators
|
||||
incident_edges=5 pos_mass=0.6750 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> LTP (strengthen) standing 0.1496 (conjecture) -> 0.7379 (grounded) [GRADUATED]
|
||||
beat 3 (t=+120s) support sustained (5)
|
||||
incident_edges=5 pos_mass=0.6750 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> LTP (strengthen) standing 0.2110 (conjecture) -> 0.7993 (grounded) [GRADUATED]
|
||||
beat 4 (t=+720s) corroboration withdrawn (+10min)
|
||||
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> isolated (no edges) standing 0.1612 (conjecture) -> 0.1612 (conjecture)
|
||||
beat 5 (t=+3600s) still withdrawn (+1h)
|
||||
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> isolated (no edges) standing 0.1263 (conjecture) -> 0.1263 (conjecture)
|
||||
beat 6 (t=+14400s) still withdrawn (+4h)
|
||||
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> isolated (no edges) standing 0.1130 (conjecture) -> 0.1130 (conjecture)
|
||||
RESULT: grounding grew automatically past threshold and graduated,
|
||||
then relaxed once the independent support stopped — living,
|
||||
not a latched flag.
|
||||
|
||||
=== SCENARIO B — DECAY: convergent independent CONTRADICTION ===
|
||||
seed: belief pre-grounded by a strong prior LTP event.
|
||||
belief standing=0.9500 band=grounded events=1 subthresh=0
|
||||
beat 1 (t=+0s) 3 independent contradictions
|
||||
incident_edges=3 pos_mass=0.0000 (n_indep=0) neg_mass=0.5400 (n_indep=3) THETA=0.30 N_MIN=3
|
||||
-> LTD (decay) standing 0.9500 (grounded) -> 0.4570 (likely) [DEMOTED]
|
||||
beat 2 (t=+60s) contradiction broadens to 5
|
||||
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
|
||||
-> LTD (decay) standing 0.1461 (conjecture) -> 0.0000 (conjecture)
|
||||
beat 3 (t=+120s) contradiction sustained (5)
|
||||
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
|
||||
-> LTD (decay) standing 0.0401 (conjecture) -> 0.0000 (conjecture)
|
||||
beat 4 (t=+180s) contradiction sustained (5)
|
||||
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
|
||||
-> LTD (decay) standing 0.0000 (conjecture) -> 0.0000 (conjecture)
|
||||
RESULT: grounding decayed grounded->likely->conjecture under
|
||||
convergent independent contradiction. The door never shut
|
||||
on the belief; its history is retained (events keep growing).
|
||||
|
||||
=== SCENARIO C — INDEPENDENCE GUARD (the load-bearing property) ===
|
||||
Both sub-cases: N=5 corroborators, edge weight 0.30, corroborator
|
||||
standing ~0.90. ONLY difference: whether the 5 are independent.
|
||||
|
||||
-- C1: 5 DISTINCT independent corroborators --
|
||||
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
|
||||
beat 1 (t=+0s) 5 independent corroborators (no inter-links)
|
||||
incident_edges=5 pos_mass=1.3500 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> LTP (strengthen) standing 0.1000 (conjecture) -> 0.9741 (grounded) [GRADUATED]
|
||||
|
||||
-- C2: 5 corroborators, but mutually-linked (echo of ONE source) --
|
||||
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
|
||||
beat 1 (t=+0s) 5 echoed (mutually-linked) corroborators
|
||||
incident_edges=5 pos_mass=0.2700 (n_indep=1) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> sub-threshold (no shift) standing 0.1000 (conjecture) -> 0.1000 (conjecture)
|
||||
|
||||
-- C3: ONE corroborator, reached by 5 parallel edges --
|
||||
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
|
||||
beat 1 (t=+0s) same node, 5 parallel edges
|
||||
incident_edges=5 pos_mass=0.2700 (n_indep=1) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
|
||||
-> sub-threshold (no shift) standing 0.1000 (conjecture) -> 0.1000 (conjecture)
|
||||
|
||||
RESULT: identical raw fan-in (5) and mass inputs; C1 grounds because
|
||||
the corroboration is INDEPENDENT (5 components), C2/C3 do not
|
||||
because it collapses to ONE source. Circular self-reinforcement
|
||||
cannot manufacture grounding.
|
||||
|
||||
DONE.
|
||||
@@ -1,60 +0,0 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// awareness.beat.patch.el — GATED integration hook for task #50.
|
||||
// NOT APPLIED. Shows exactly how grounded edge-propagation couples into the
|
||||
// dream/consolidation beat in neuron/awareness.el. Promotion sequenced by the
|
||||
// main loop after the engine cutover settles.
|
||||
//
|
||||
// WHY HERE. The heartbeat is the beat. Today it runs hebb_consolidate() to
|
||||
// drain the self-formed Hebbian associations into the durable store, then
|
||||
// emit_heartbeat(). Grounded edge-propagation belongs in the SAME beat, AFTER
|
||||
// consolidation: the edges hebb_consolidate() just wrote are the tethers
|
||||
// grounding propagates along. Consolidation lays down the wiring; propagation
|
||||
// grades the beliefs along it. One beat, coupled — memory 69b8babe: memory-
|
||||
// consolidation and staying-yourself are one physics.
|
||||
//
|
||||
// The propagation itself runs INSIDE the engram (native engram_ground_propagate
|
||||
// over the durable flat node/edge arrays). The soul invokes it over HTTP and
|
||||
// folds the gep_* telemetry into the heartbeat stream next to the hebb_* gauges.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// [1] New helper — sibling to hebb_consolidate() (awareness.el ~line 99).
|
||||
// Fires one grounded edge-propagation beat on the durable store and returns
|
||||
// its JSON telemetry ({"gep_strengthened":..,"gep_graduations":.., ...}).
|
||||
fn ground_propagate() -> String {
|
||||
let url_env: String = env("SOUL_ISE_URL")
|
||||
let url_state: String = if str_eq(url_env, "") { state_get("soul_engram_url") } else { url_env }
|
||||
let engram_url: String = if str_eq(url_state, "") { "http://localhost:8742" } else { url_state }
|
||||
// Same auth envelope as hebb_consolidate — this is a graph mutation (it
|
||||
// appends grounding events + updates confidence), so it is gated on _auth.
|
||||
let key_state: String = state_get("soul_engram_api_key")
|
||||
let api_key: String = if str_eq(key_state, "") { env("ENGRAM_API_KEY") } else { key_state }
|
||||
let auth_part: String = if str_eq(api_key, "") { "{}" } else { "{\"_auth\":\"" + api_key + "\"}" }
|
||||
let resp: String = http_post_json(engram_url + "/api/ground/propagate", auth_part)
|
||||
if str_eq(resp, "") { return "" }
|
||||
return resp
|
||||
}
|
||||
|
||||
// [2] Beat hook — insert between hebb_consolidate() and emit_heartbeat()
|
||||
// (awareness.el line 1286-1288). Replaces:
|
||||
//
|
||||
// let wb_sent_n: Int = hebb_consolidate()
|
||||
// state_set("soul.hebb_wb_sent", int_to_str(wb_sent_n))
|
||||
// emit_heartbeat()
|
||||
//
|
||||
// with:
|
||||
//
|
||||
// let wb_sent_n: Int = hebb_consolidate()
|
||||
// state_set("soul.hebb_wb_sent", int_to_str(wb_sent_n))
|
||||
// // Grounded edge-propagation — grade beliefs along the tethers
|
||||
// // consolidation just laid down. Threshold-gated by convergent
|
||||
// // independent corroboration; automatic, salience-ordered, bounded.
|
||||
// let gep_tel: String = ground_propagate()
|
||||
// state_set("soul.gep_last", gep_tel)
|
||||
// emit_heartbeat()
|
||||
//
|
||||
// [3] emit_heartbeat() (awareness.el ~line 201) folds soul.gep_last into the
|
||||
// heartbeat payload beside the hebb_* gauges, so graduation/decay counts
|
||||
// are visible in the durable ISE stream — the same observability discipline
|
||||
// the Hebbian rule earned (a mechanism you cannot see in the stream is a
|
||||
// mechanism you cannot trust): read state_get("soul.gep_last") and splice
|
||||
// it into the heartbeat JSON object.
|
||||
@@ -1,188 +0,0 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* engram_ground_propagate.staged.c — GATED runtime native for task #50.
|
||||
*
|
||||
* STAGED, NOT COMPILED INTO THE LIVE BINARY. This mirrors the
|
||||
* geometric_retrieve.staged.c staging pattern (memory 1cc231ec): it references
|
||||
* runtime-internal types (EngramStore, EngramNode, EngramEdge, engram_global,
|
||||
* engram_now_ms, the adj cache) and therefore compiles ONLY when spliced into
|
||||
* lang/releases/v1.0.0-20260501/el_runtime.c. Splice + promotion is sequenced
|
||||
* by the main loop AFTER the engine+HNSW cutover settles — do NOT hand-apply.
|
||||
*
|
||||
* It is the production form of the mechanism proven in gep_proof.c: the SAME
|
||||
* gep_core.h primitives (GepGrounding ring, gep_standing, gep_append,
|
||||
* union-find independence), wired directly to the live flat node/edge arrays.
|
||||
*
|
||||
* ── SPLICE PLAN (three additive edits to el_runtime.c; nothing removed) ──────
|
||||
*
|
||||
* [1] EngramNode struct (~line 6061, after hebb_elig_ts): add the grounding
|
||||
* collection. Additive; zero-initialized by the existing calloc/memset
|
||||
* paths, so legacy snapshots degrade gracefully to an empty history.
|
||||
*
|
||||
* GepGrounding grounding; // task #50 — append-only grounding ring
|
||||
*
|
||||
* [2] #include "gep_core.h" near the other engram includes, and paste the
|
||||
* body of this file below the Hebbian section (after engram_hebb_drain_json).
|
||||
*
|
||||
* [3] Persistence (engram_save node JSON ~7934 / engram_load parser ~8186):
|
||||
* serialize the grounding ring as a compact "grounding" array of
|
||||
* [ts,sign,mag] triples + subthreshold_hits so standing survives a
|
||||
* round-trip. Helpers gep_grounding_to_json / gep_grounding_parse below.
|
||||
* Until wired, grounding is in-RAM only (like the Hebbian eligibility
|
||||
* trace) — correct for a first gated rollout, but standing resets on boot.
|
||||
*
|
||||
* [4] EL surface: declare engram_ground_propagate in el_runtime.h + el_seed.c,
|
||||
* add route_ground_propagate to engram/src/server.el, called from the
|
||||
* awareness.el consolidation beat (see awareness.beat.patch.el).
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
#include "gep_core.h"
|
||||
|
||||
/* Relation → evidential polarity. Supportive relations transmit grounding
|
||||
* gravity (+1); contradictory relations erode it (-1); everything else is a
|
||||
* NON-evidential edge (structural / navigational) and is ignored (0) — an
|
||||
* association is not a corroboration. Extend deliberately; a mis-classified
|
||||
* relation is a false corroboration. */
|
||||
static int8_t gep_relation_polarity(const char* rel) {
|
||||
if (!rel) return 0;
|
||||
if (!strcmp(rel, "supports") || !strcmp(rel, "corroborates") ||
|
||||
!strcmp(rel, "derived-from") || !strcmp(rel, "hebbian-associate") ||
|
||||
!strcmp(rel, "grounds") || !strcmp(rel, "confirms")) return +1;
|
||||
if (!strcmp(rel, "contradicts") || !strcmp(rel, "refutes") ||
|
||||
!strcmp(rel, "negates") || !strcmp(rel, "conflicts-with")) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Which nodes are BELIEFS/CONJECTURES subject to grounding propagation. Facts
|
||||
* imported as knowledge are already grounded by provenance; identity/safety
|
||||
* layers are never re-graded here. Gate on node_type + the conjecture tag. */
|
||||
static int gep_is_belief(const EngramNode* n) {
|
||||
if (!n || !n->node_type) return 0;
|
||||
if (n->layer_id == ENGRAM_LAYER_SAFETY) return 0; /* never re-grade safety */
|
||||
return !strcmp(n->node_type, "Memory") ||
|
||||
!strcmp(n->node_type, "Conjecture") ||
|
||||
!strcmp(n->node_type, "Hypothesis") ||
|
||||
!strcmp(n->node_type, "Belief") ||
|
||||
(n->tags && istr_contains(n->tags, "conjecture"));
|
||||
}
|
||||
|
||||
/* Grounding standing of an engram node, derived from its collection. This is
|
||||
* the value the verifier (#43) and realizer (calibrated assertion, 0041d917)
|
||||
* read — and it is written back into epistemic_confidence-equivalent surfaces
|
||||
* so "never speak above the grounding" is enforced from one source of truth. */
|
||||
double engram_grounding_standing(const EngramNode* n, int64_t now_ms) {
|
||||
return gep_standing(&n->grounding, now_ms);
|
||||
}
|
||||
|
||||
/* ── The beat: one pass of grounded edge-propagation over the whole store ────
|
||||
* Called from the consolidation/dream heartbeat. 1-hop, beam-capped, salience-
|
||||
* ordered so a bounded slice of the highest-salience beliefs is processed per
|
||||
* beat (the rest next beat) — never a full-graph blow-up on a 12k-node store.
|
||||
* Returns JSON telemetry for the heartbeat stream. */
|
||||
#define GEP_BELIEFS_PER_BEAT 512 /* bound work per beat; salience-prioritized */
|
||||
|
||||
el_val_t engram_ground_propagate(void) {
|
||||
EngramStore* g = engram_get();
|
||||
int64_t now = engram_now_ms();
|
||||
engram_adj_rebuild(g); /* ensure adj_from/adj_to are current */
|
||||
|
||||
int strengthened = 0, decayed = 0, subthreshold = 0;
|
||||
int graduations = 0, demotions = 0, isolated = 0, starved = 0, processed = 0;
|
||||
|
||||
for (int64_t bi = 0; bi < g->node_count && processed < GEP_BELIEFS_PER_BEAT; bi++) {
|
||||
EngramNode* b = &g->nodes[bi];
|
||||
if (!gep_is_belief(b)) continue;
|
||||
processed++;
|
||||
|
||||
int before = gep_band_rank(gep_standing(&b->grounding, now));
|
||||
|
||||
/* Gather independent corroborators over incident edges (both directions),
|
||||
* anti-delusion gated (neighbor must already be ≥ LIKELY_MIN). */
|
||||
GepCorrSet cs; cs.n = 0; int incident = 0;
|
||||
int* out = g->adj_from[bi]; int out_n = g->adj_from_len[bi];
|
||||
int* in = g->adj_to[bi]; int in_n = g->adj_to_len[bi];
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
int* lst = pass ? in : out; int ln = pass ? in_n : out_n;
|
||||
for (int k = 0; k < ln; k++) {
|
||||
EngramEdge* e = &g->edges[lst[k]];
|
||||
int8_t pol = gep_relation_polarity(e->relation);
|
||||
if (pol == 0) continue;
|
||||
incident++;
|
||||
const char* cid = pass ? e->from_id : e->to_id;
|
||||
int64_t ci = engram_find_node_index(cid);
|
||||
if (ci < 0 || ci == bi) continue;
|
||||
double cstand = gep_standing(&g->nodes[ci].grounding, now);
|
||||
if (cstand < GEP_LIKELY_MIN) continue; /* no tether */
|
||||
double contrib = e->weight * cstand * (double)pol;
|
||||
int ex = -1;
|
||||
for (int q = 0; q < cs.n; q++) if (cs.node_idx[q] == (int)ci) { ex = q; break; }
|
||||
if (ex >= 0) { if (fabs(contrib) > fabs(cs.contrib[ex])) cs.contrib[ex] = contrib; }
|
||||
else if (cs.n < GEP_MAX_CORR) {
|
||||
cs.node_idx[cs.n] = (int)ci; cs.contrib[cs.n] = contrib;
|
||||
cs.parent[cs.n] = cs.n; cs.n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse mutually-derived corroborators (an edge between two of them)
|
||||
* into one independent component — the independence guard. */
|
||||
for (int x = 0; x < cs.n; x++) {
|
||||
int64_t nx = cs.node_idx[x];
|
||||
int* xout = g->adj_from[nx]; int xn = g->adj_from_len[nx];
|
||||
for (int k = 0; k < xn; k++) {
|
||||
const char* tid = g->edges[xout[k]].to_id;
|
||||
int64_t ti = engram_find_node_index(tid);
|
||||
for (int y = 0; y < cs.n; y++)
|
||||
if (cs.node_idx[y] == (int)ti) { gep_uf_union(&cs, x, y); break; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-component max-magnitude, split by polarity → convergent independent
|
||||
* support mass + independence count. */
|
||||
double comp_best[GEP_MAX_CORR]; int comp_root[GEP_MAX_CORR], ncomp = 0;
|
||||
for (int i = 0; i < cs.n; i++) {
|
||||
int r = gep_uf_find(&cs, i), slot = -1;
|
||||
for (int kk = 0; kk < ncomp; kk++) if (comp_root[kk] == r) { slot = kk; break; }
|
||||
if (slot < 0) { slot = ncomp++; comp_root[slot] = r; comp_best[slot] = cs.contrib[i]; }
|
||||
else if (fabs(cs.contrib[i]) > fabs(comp_best[slot])) comp_best[slot] = cs.contrib[i];
|
||||
}
|
||||
double pos = 0, neg = 0; int np = 0, nn = 0; uint64_t sig = 1469598103934665603ULL;
|
||||
for (int k = 0; k < ncomp; k++) {
|
||||
if (comp_best[k] > 0) { pos += comp_best[k]; np++; }
|
||||
else if (comp_best[k] < 0) { neg += -comp_best[k]; nn++; }
|
||||
sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL;
|
||||
}
|
||||
|
||||
double net = pos - neg;
|
||||
if (net > 0 && pos >= GEP_THETA && np >= GEP_N_MIN) {
|
||||
gep_append(&b->grounding, now, +1, tanh(GEP_MAG_GAIN * net), sig);
|
||||
strengthened++;
|
||||
} else if (net < 0 && neg >= GEP_THETA && nn >= GEP_N_MIN) {
|
||||
gep_append(&b->grounding, now, -1, tanh(GEP_MAG_GAIN * (-net)), sig);
|
||||
decayed++;
|
||||
} else if (np > 0 || nn > 0) {
|
||||
b->grounding.subthreshold_hits++; subthreshold++;
|
||||
} else if (incident == 0) { isolated++; }
|
||||
else { starved++; }
|
||||
|
||||
/* Mirror the derived standing onto confidence so downstream reads
|
||||
* (activate epistemic_confidence, realizer calibration) never exceed the
|
||||
* grounding. Faithful representation, single source of truth. */
|
||||
double stand = gep_standing(&b->grounding, now);
|
||||
b->confidence = stand;
|
||||
b->updated_at = now;
|
||||
|
||||
int after = gep_band_rank(stand);
|
||||
if (after > before) graduations++;
|
||||
if (after < before) demotions++;
|
||||
}
|
||||
|
||||
/* Heartbeat telemetry — the gep_* line, sibling to the hebb_* gauges. */
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof buf,
|
||||
"{\"gep_processed\":%d,\"gep_strengthened\":%d,\"gep_decayed\":%d,"
|
||||
"\"gep_subthreshold\":%d,\"gep_graduations\":%d,\"gep_demotions\":%d,"
|
||||
"\"gep_isolated\":%d,\"gep_starved\":%d}",
|
||||
processed, strengthened, decayed, subthreshold,
|
||||
graduations, demotions, isolated, starved);
|
||||
return EL_STR(el_strdup(buf));
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* gep_core.h — Grounded Edge-Propagation, the core mechanism (task #50).
|
||||
*
|
||||
* Edge-aware, dream-coupled consolidation. Runs DURING the consolidation/dream
|
||||
* beat (awareness.el hebb_consolidate → engram_ground_propagate). Grounding
|
||||
* propagates + strengthens/decays along edges, threshold-gated by CONVERGENT
|
||||
* INDEPENDENT corroboration from adjacent grounded nodes.
|
||||
*
|
||||
* This header is the single source of truth for the algorithm. It is pure C
|
||||
* (libm only — own-the-core, no new libraries) and operates on a compact graph
|
||||
* view (GepGraph) that both the proof harness and the runtime native populate
|
||||
* from the live EngramStore (nodes/edges flat arrays + adj_from/adj_to).
|
||||
*
|
||||
* SPEC (Will, 2026-08-15; memory 9e09a59f, refines 1a861007):
|
||||
* - A grounding is a VECTOR + its HEBBIAN WEIGHTS — a weighted structure over
|
||||
* the evidential neighborhood, NOT a scalar and NOT a flat list. It APPENDS
|
||||
* and GROWS on SIGNIFICANT change. => grounding = an APPEND-ONLY event ring
|
||||
* (GepGrounding), parallel to the ACT-R base-level access_ts ring already in
|
||||
* EngramNode. Current standing is DERIVED, recency-weighted, never stored.
|
||||
* - UPDATE = LTP/LTD with a THRESHOLD (the key nonlinearity). Sub-threshold =
|
||||
* recorded in history but TRANSIENT (no lasting shift). Cross the threshold
|
||||
* of convergent support → grounding STRENGTHENS. Contradiction/erosion past
|
||||
* threshold → grounding DECAYS. Automatic, event-driven, salience-gated.
|
||||
* - DRIVER = CONVERGENT INDEPENDENT CORROBORATION (coherentism, mechanized):
|
||||
* when N INDEPENDENT adjacent nodes ground as likely-true around a
|
||||
* conjecture (Will's example: 13), its grounding grows on its own.
|
||||
* - INDEPENDENCE is load-bearing: N DISTINCT corroborators, not one node
|
||||
* echoed N times. Guards against circular self-reinforcement.
|
||||
* - ANTI-DELUSION GRAVITY (memory 0b15017c): support flows only FROM already-
|
||||
* grounded neighbors. A belief cannot ground from ungrounded speculation,
|
||||
* however self-consistent — nothing tethers it to the grounded core.
|
||||
* - NOTHING IS SETTLED (memory 271f1163): grounded is strongly-held, still
|
||||
* falsifiable. Decay path stays open on every node; history is append-only,
|
||||
* supersede-not-delete.
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
#ifndef GEP_CORE_H
|
||||
#define GEP_CORE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Constants ──────────────────────────────────────────────────────────────
|
||||
* GEP_DECAY_D matches ENGRAM_BLL_D (0.5, canonical ACT-R): the derived standing
|
||||
* is recency-weighted over the grounding-event collection exactly as the
|
||||
* base-level term is recency-weighted over the access ring (memory 1a861007:
|
||||
* "structurally the ACT-R base-level pattern, a sum over time-stamped events").
|
||||
*/
|
||||
#define GEP_DECAY_D 0.5 /* ACT-R power-law recency exponent */
|
||||
#define GEP_BASE 0.10 /* standing floor of a bare conjecture */
|
||||
#define GEP_LIKELY_MIN 0.34 /* band: conjecture < LIKELY ≤ likely */
|
||||
#define GEP_GROUNDED_MIN 0.66 /* band: likely < GROUNDED ≤ grounded */
|
||||
#define GEP_N_MIN 3 /* min INDEPENDENT corroborators to cross */
|
||||
#define GEP_THETA 0.30 /* min convergent-support MASS to cross */
|
||||
#define GEP_MAG_GAIN 1.0 /* net-support → event-magnitude gain (tanh) */
|
||||
#define GEP_EVENT_RING 32 /* grounding-history depth kept exactly */
|
||||
|
||||
/* A single grounding event — one contact with the evidential neighborhood.
|
||||
* Append-only; the ring is the collection-over-time, the standing is derived. */
|
||||
typedef struct {
|
||||
int64_t ts; /* wall-clock ms of the grounding event */
|
||||
int8_t sign; /* +1 = LTP (strengthen), -1 = LTD (decay) */
|
||||
double mag; /* magnitude in (0,1], = tanh(gain·|net independent support|)*/
|
||||
uint64_t sig; /* signature of the independent corroborator set (audit) */
|
||||
} GepEvent;
|
||||
|
||||
/* The grounding of one node: an append-only ring of events + transient counters.
|
||||
* older_count keeps the tail (events aged out of the ring) so the collection is
|
||||
* never silently lost — supersede-not-delete. subthreshold_hits records beats
|
||||
* where support was present but did NOT cross threshold (transient, no shift). */
|
||||
typedef struct {
|
||||
GepEvent ev[GEP_EVENT_RING];
|
||||
int head; /* next write slot */
|
||||
int filled; /* valid entries (≤ GEP_EVENT_RING) */
|
||||
int64_t older_count; /* durable events aged past the ring */
|
||||
int subthreshold_hits; /* transient sub-threshold beats, no shift */
|
||||
} GepGrounding;
|
||||
|
||||
typedef struct {
|
||||
const char* id;
|
||||
GepGrounding gr;
|
||||
int is_belief; /* 1 = subject to propagation (conjecture/belief) */
|
||||
} GepNode;
|
||||
|
||||
/* An edge carries a HEBBIAN WEIGHT (EngramEdge.weight) and a polarity derived
|
||||
* from its relation: supportive (supports/corroborates/derived-from/hebbian-
|
||||
* associate) = +1, contradictory (contradicts/refutes) = -1. */
|
||||
typedef struct {
|
||||
int from; /* node index */
|
||||
int to; /* node index */
|
||||
double weight; /* Hebbian edge weight, [0,1] */
|
||||
int8_t polarity; /* +1 supportive, -1 contradictory */
|
||||
} GepEdge;
|
||||
|
||||
typedef struct {
|
||||
GepNode* nodes; int n_nodes;
|
||||
GepEdge* edges; int n_edges;
|
||||
} GepGraph;
|
||||
|
||||
typedef struct {
|
||||
int strengthened; /* beliefs that took an LTP event this beat */
|
||||
int decayed; /* beliefs that took an LTD event this beat */
|
||||
int subthreshold; /* beliefs with support present but below threshold */
|
||||
int graduations; /* band-up transitions (conjecture→likely→grounded) */
|
||||
int demotions; /* band-down transitions */
|
||||
int isolated; /* belief nodes with ZERO incident edges (sparse graph) */
|
||||
int starved; /* belief nodes with edges but NO grounded corroborator */
|
||||
} GepBeatStats;
|
||||
|
||||
/* Real-graph note (live measurement 2026-08-15): 70.7% of nodes are isolated,
|
||||
* connected core ~28%. Grounded edge-propagation is definitionally scoped to
|
||||
* the connected core — a belief with no grounded neighbor has nothing to
|
||||
* tether to (anti-delusion gravity). isolated/starved are surfaced as an
|
||||
* interoceptive signal for the edge-formation / embedding pass (#20) to try to
|
||||
* connect them; #50 CONSUMES edges, it does not form them. */
|
||||
|
||||
/* ── Standing derivation: collection → scalar, recency-weighted ─────────────
|
||||
* standing = clamp( GEP_BASE + Σ_events sign·mag·age^(-D) , 0, 1 ).
|
||||
* Exactly the ACT-R base-level shape (Σ t^-d) but sign-carrying so LTD subtracts.
|
||||
* The value is a pure function of wall-clock time — idempotent, never stored. */
|
||||
static inline double gep_standing(const GepGrounding* g, int64_t now_ms) {
|
||||
double raw = 0.0;
|
||||
for (int i = 0; i < g->filled; i++) {
|
||||
double age = (double)(now_ms - g->ev[i].ts) / 1000.0;
|
||||
if (age < 1.0) age = 1.0; /* clock-skew / same-beat → 1s */
|
||||
raw += (double)g->ev[i].sign * g->ev[i].mag * pow(age, -GEP_DECAY_D);
|
||||
}
|
||||
double s = GEP_BASE + raw;
|
||||
if (s < 0.0) s = 0.0;
|
||||
if (s > 1.0) s = 1.0;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Band label from a standing value. */
|
||||
static inline const char* gep_band(double standing) {
|
||||
if (standing >= GEP_GROUNDED_MIN) return "grounded";
|
||||
if (standing >= GEP_LIKELY_MIN) return "likely";
|
||||
return "conjecture";
|
||||
}
|
||||
static inline int gep_band_rank(double standing) {
|
||||
if (standing >= GEP_GROUNDED_MIN) return 2;
|
||||
if (standing >= GEP_LIKELY_MIN) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Append one grounding event to the ring (append-only; oldest slot recycles,
|
||||
* its loss counted in older_count so the collection's depth is never faked). */
|
||||
static inline void gep_append(GepGrounding* g, int64_t ts, int8_t sign,
|
||||
double mag, uint64_t sig) {
|
||||
if (g->filled >= GEP_EVENT_RING) g->older_count++;
|
||||
g->ev[g->head].ts = ts;
|
||||
g->ev[g->head].sign = sign;
|
||||
g->ev[g->head].mag = mag;
|
||||
g->ev[g->head].sig = sig;
|
||||
g->head = (g->head + 1) % GEP_EVENT_RING;
|
||||
if (g->filled < GEP_EVENT_RING) g->filled++;
|
||||
}
|
||||
|
||||
/* ── Independence via union-find over corroborators ─────────────────────────
|
||||
* Two corroborators are the SAME independent source if they are the same node,
|
||||
* or if a direct edge links them (mutually-derived / echoed through a chain).
|
||||
* Counting DISTINCT components — not raw corroborator count — is the guard
|
||||
* against one node echoed N times reading as N independent corroborations. */
|
||||
#define GEP_MAX_CORR 256
|
||||
typedef struct {
|
||||
int node_idx[GEP_MAX_CORR]; /* corroborator node index */
|
||||
double contrib[GEP_MAX_CORR]; /* weight·standing(c) */
|
||||
int parent[GEP_MAX_CORR]; /* union-find parent */
|
||||
int n;
|
||||
} GepCorrSet;
|
||||
|
||||
static int gep_uf_find(GepCorrSet* s, int x) {
|
||||
while (s->parent[x] != x) { s->parent[x] = s->parent[s->parent[x]]; x = s->parent[x]; }
|
||||
return x;
|
||||
}
|
||||
static void gep_uf_union(GepCorrSet* s, int a, int b) {
|
||||
int ra = gep_uf_find(s, a), rb = gep_uf_find(s, b);
|
||||
if (ra != rb) s->parent[ra] = rb;
|
||||
}
|
||||
/* index of node_idx within the corroborator set, or -1 */
|
||||
static int gep_corr_index_of(const GepCorrSet* s, int node_idx) {
|
||||
for (int i = 0; i < s->n; i++) if (s->node_idx[i] == node_idx) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ── The beat: grounded edge-propagation over one belief node ───────────────
|
||||
* Returns +1 if an LTP event was appended, -1 if LTD, 0 if sub-threshold/none.
|
||||
* out_pos/out_neg/out_np/out_nn expose the raw support decomposition for the
|
||||
* proof ledger (mass and independent-component counts on each polarity). */
|
||||
static int gep_propagate_node(GepGraph* g, int b, int64_t now_ms,
|
||||
double* out_pos, double* out_neg,
|
||||
int* out_np, int* out_nn, int* out_incident) {
|
||||
GepCorrSet cs; cs.n = 0;
|
||||
int incident = 0; /* any edge touching b at all — isolation detector */
|
||||
|
||||
/* 1. Gather corroborators along incident edges. Anti-delusion gravity:
|
||||
* only ALREADY-grounded neighbors (standing ≥ LIKELY_MIN) may corroborate.
|
||||
* Each contributes weight·standing; polarity kept via signed contrib.
|
||||
* 1-HOP ONLY — no BFS fan-out, so no per-hop breadth explosion. The
|
||||
* corroborator working set is hard-capped at GEP_MAX_CORR (beam bound
|
||||
* against hub belief nodes with thousands of incident edges). */
|
||||
for (int e = 0; e < g->n_edges; e++) {
|
||||
int c = -1; int8_t pol = 0;
|
||||
if (g->edges[e].from == b) { c = g->edges[e].to; pol = g->edges[e].polarity; }
|
||||
else if (g->edges[e].to == b) { c = g->edges[e].from; pol = g->edges[e].polarity; }
|
||||
else continue;
|
||||
incident++;
|
||||
if (c < 0 || c == b) continue;
|
||||
double cs_standing = gep_standing(&g->nodes[c].gr, now_ms);
|
||||
if (cs_standing < GEP_LIKELY_MIN) continue; /* ungrounded ⇒ no pull */
|
||||
double contribution = g->edges[e].weight * cs_standing * (double)pol;
|
||||
int existing = gep_corr_index_of(&cs, c);
|
||||
if (existing >= 0) {
|
||||
/* same corroborator id reached twice (multi-edge echo): keep the
|
||||
* strongest-magnitude contribution, do NOT add — one source, one vote */
|
||||
if (fabs(contribution) > fabs(cs.contrib[existing]))
|
||||
cs.contrib[existing] = contribution;
|
||||
} else if (cs.n < GEP_MAX_CORR) { /* beam bound against hub belief nodes */
|
||||
cs.node_idx[cs.n] = c;
|
||||
cs.contrib[cs.n] = contribution;
|
||||
cs.parent[cs.n] = cs.n;
|
||||
cs.n++;
|
||||
}
|
||||
}
|
||||
if (out_incident) *out_incident = incident;
|
||||
|
||||
/* 2. Collapse mutually-derived corroborators (an edge between two of them =
|
||||
* echo chain / shared derivation) into one independent component. */
|
||||
for (int e = 0; e < g->n_edges; e++) {
|
||||
int ia = gep_corr_index_of(&cs, g->edges[e].from);
|
||||
int ib = gep_corr_index_of(&cs, g->edges[e].to);
|
||||
if (ia >= 0 && ib >= 0) gep_uf_union(&cs, ia, ib);
|
||||
}
|
||||
|
||||
/* 3. Per independent component, take the MAX-magnitude member (echoes don't
|
||||
* inflate mass either), split by polarity. Convergent INDEPENDENT support
|
||||
* = sum over components; independence count = number of components. */
|
||||
double comp_best[GEP_MAX_CORR];
|
||||
int comp_root[GEP_MAX_CORR]; int n_comp = 0;
|
||||
for (int i = 0; i < cs.n; i++) {
|
||||
int r = gep_uf_find(&cs, i);
|
||||
int slot = -1;
|
||||
for (int k = 0; k < n_comp; k++) if (comp_root[k] == r) { slot = k; break; }
|
||||
if (slot < 0) { slot = n_comp++; comp_root[slot] = r; comp_best[slot] = cs.contrib[i]; }
|
||||
else if (fabs(cs.contrib[i]) > fabs(comp_best[slot])) comp_best[slot] = cs.contrib[i];
|
||||
}
|
||||
double pos = 0.0, neg = 0.0; int np = 0, nn = 0;
|
||||
uint64_t sig = 1469598103934665603ULL; /* FNV offset — signature of the set */
|
||||
for (int k = 0; k < n_comp; k++) {
|
||||
if (comp_best[k] > 0.0) { pos += comp_best[k]; np++; }
|
||||
else if (comp_best[k] < 0.0) { neg += -comp_best[k]; nn++; }
|
||||
sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL;
|
||||
}
|
||||
if (out_pos) *out_pos = pos; if (out_neg) *out_neg = neg;
|
||||
if (out_np) *out_np = np; if (out_nn) *out_nn = nn;
|
||||
|
||||
double net = pos - neg;
|
||||
|
||||
/* 4. Threshold gate. Convergent independent corroboration must clear BOTH a
|
||||
* MASS threshold (THETA) and an INDEPENDENCE-count threshold (N_MIN).
|
||||
* The count gate is the independence guard: echoed support collapses to
|
||||
* one component and never reaches N_MIN however large the raw fan-in. */
|
||||
if (net > 0.0 && pos >= GEP_THETA && np >= GEP_N_MIN) {
|
||||
double mag = tanh(GEP_MAG_GAIN * net);
|
||||
gep_append(&g->nodes[b].gr, now_ms, +1, mag, sig);
|
||||
return +1;
|
||||
}
|
||||
if (net < 0.0 && neg >= GEP_THETA && nn >= GEP_N_MIN) {
|
||||
double mag = tanh(GEP_MAG_GAIN * (-net));
|
||||
gep_append(&g->nodes[b].gr, now_ms, -1, mag, sig);
|
||||
return -1;
|
||||
}
|
||||
/* Sub-threshold: support seen but did not cross. Recorded, transient, no
|
||||
* lasting shift — exactly Will's "recorded in history but transient". */
|
||||
if (np > 0 || nn > 0) g->nodes[b].gr.subthreshold_hits++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Run one consolidation/dream beat over every belief node in the graph. */
|
||||
static inline GepBeatStats gep_beat(GepGraph* g, int64_t now_ms) {
|
||||
GepBeatStats st; memset(&st, 0, sizeof st);
|
||||
for (int b = 0; b < g->n_nodes; b++) {
|
||||
if (!g->nodes[b].is_belief) continue;
|
||||
int before = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms));
|
||||
double pos, neg; int np, nn, incident;
|
||||
int r = gep_propagate_node(g, b, now_ms, &pos, &neg, &np, &nn, &incident);
|
||||
int after = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms));
|
||||
if (r > 0) st.strengthened++;
|
||||
else if (r < 0) st.decayed++;
|
||||
else if (np > 0 || nn > 0) st.subthreshold++;
|
||||
else if (incident == 0) st.isolated++; /* sparse-graph reality */
|
||||
else st.starved++; /* has edges, no grounded neighbor */
|
||||
if (after > before) st.graduations++;
|
||||
if (after < before) st.demotions++;
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
#endif /* GEP_CORE_H */
|
||||
@@ -1,232 +0,0 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* gep_proof.c — PROOF LEDGER for grounded edge-propagation (task #50).
|
||||
*
|
||||
* Self-contained. Builds three scenarios on an in-memory GepGraph that mirrors
|
||||
* the live EngramStore's flat node/edge arrays, runs the consolidation/dream
|
||||
* beat (gep_beat), and prints RAW grounding before/after for each:
|
||||
*
|
||||
* (A) STRENGTHEN — a conjecture + N independent grounded corroborators.
|
||||
* Grounding grows past threshold, GRADUATES conjecture→
|
||||
* likely→grounded, then RELAXES when corroboration stops
|
||||
* (nothing is settled).
|
||||
* (B) DECAY — a grounded belief meets N independent CONTRADICTORY
|
||||
* corroborators. Grounding decays grounded→likely→conjecture.
|
||||
* (C) INDEPENDENCE GUARD — identical fan-in of N=5, weights, and standings.
|
||||
* C1: 5 DISTINCT independent corroborators → grounds.
|
||||
* C2: the SAME support echoed (5 mutually-linked / one node
|
||||
* repeated) → collapses to 1 independent → does NOT.
|
||||
*
|
||||
* Build: cc -std=c11 -O2 -o gep_proof gep_proof.c -lm
|
||||
* Run: ./gep_proof
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "gep_core.h"
|
||||
|
||||
#define T0 1786000000000LL /* fixed base time (ms) — deterministic */
|
||||
#define BEAT_MS 60000LL /* 60s heartbeat cadence (awareness.el) */
|
||||
|
||||
/* Seed a node's grounding with a prior LTP event so it reads as already-grounded
|
||||
* (a member of the grounded core that gravity radiates from). mag→standing:
|
||||
* standing = GEP_BASE + mag (event at ~now). */
|
||||
static void seed_grounded(GepNode* n, double mag, int64_t ts) {
|
||||
memset(&n->gr, 0, sizeof n->gr);
|
||||
gep_append(&n->gr, ts, +1, mag, 0);
|
||||
}
|
||||
|
||||
/* Re-anchor every NON-belief node (the corroborators/refuters) as a freshly-
|
||||
* grounded member of the core AT time `now`. These nodes are, by definition,
|
||||
* sustained members of the grounded core — each has its OWN ongoing
|
||||
* corroboration — so their standing must be read as grounded at each beat, not
|
||||
* left to power-law-decay out of the core between beats. The belief-under-test
|
||||
* is NEVER re-anchored: its trajectory is driven only by the propagation. */
|
||||
static void anchor_core(GepGraph* g, int64_t now, double mag) {
|
||||
for (int i = 0; i < g->n_nodes; i++)
|
||||
if (!g->nodes[i].is_belief) seed_grounded(&g->nodes[i], mag, now);
|
||||
}
|
||||
|
||||
static void print_node(const char* tag, GepNode* n, int64_t now) {
|
||||
double s = gep_standing(&n->gr, now);
|
||||
printf(" %-14s standing=%.4f band=%-10s events=%d subthresh=%d\n",
|
||||
tag, s, gep_band(s), n->gr.filled, n->gr.subthreshold_hits);
|
||||
}
|
||||
|
||||
/* Run one beat over a single belief node b and print the raw support decomposition. */
|
||||
static void beat_and_report(GepGraph* g, int b, int64_t now, int beatno,
|
||||
const char* note) {
|
||||
anchor_core(g, now, 0.80); /* corroborators stay grounded at each beat */
|
||||
double s_before = gep_standing(&g->nodes[b].gr, now);
|
||||
int r_before = gep_band_rank(s_before);
|
||||
double pos, neg; int np, nn, incident;
|
||||
int r = gep_propagate_node(g, b, now, &pos, &neg, &np, &nn, &incident);
|
||||
double s_after = gep_standing(&g->nodes[b].gr, now);
|
||||
int r_after = gep_band_rank(s_after);
|
||||
const char* action = (r > 0) ? "LTP (strengthen)"
|
||||
: (r < 0) ? "LTD (decay)"
|
||||
: (np || nn) ? "sub-threshold (no shift)"
|
||||
: (incident == 0) ? "isolated (no edges)"
|
||||
: "starved (no grounded neighbor)";
|
||||
printf(" beat %d (t=+%llds) %s\n", beatno,
|
||||
(long long)((now - T0) / 1000), note ? note : "");
|
||||
printf(" incident_edges=%d pos_mass=%.4f (n_indep=%d) neg_mass=%.4f (n_indep=%d)"
|
||||
" THETA=%.2f N_MIN=%d\n",
|
||||
incident, pos, np, neg, nn, (double)GEP_THETA, GEP_N_MIN);
|
||||
printf(" -> %-26s standing %.4f (%s) -> %.4f (%s)%s\n",
|
||||
action, s_before, gep_band(s_before), s_after, gep_band(s_after),
|
||||
(r_after > r_before) ? " [GRADUATED]"
|
||||
: (r_after < r_before) ? " [DEMOTED]" : "");
|
||||
}
|
||||
|
||||
/* ── Scenario A — STRENGTHEN + graduation + relaxation ───────────────────── */
|
||||
static void scenario_A(void) {
|
||||
printf("\n=== SCENARIO A — STRENGTHEN: convergent independent corroboration ===\n");
|
||||
/* nodes[0] = the conjecture (belief). nodes[1..8] = independent corroborators,
|
||||
* each already grounded, each tethered to the conjecture by a weak young
|
||||
* hebbian-associate edge (weight 0.15 = ENGRAM_HEBB_LINK_W0). The corroborators
|
||||
* are NOT linked to each other → fully independent. */
|
||||
static GepNode nodes[9];
|
||||
static GepEdge edges[8];
|
||||
memset(nodes, 0, sizeof nodes);
|
||||
nodes[0].id = "conjecture"; nodes[0].is_belief = 1; /* bare: standing = BASE */
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
nodes[i].id = "corroborator";
|
||||
seed_grounded(&nodes[i], 0.80, T0); /* standing ≈ 0.90 → grounded core */
|
||||
}
|
||||
GepGraph g = { nodes, 9, edges, 0 };
|
||||
|
||||
printf(" seed: conjecture has NO grounding events; corroborators pre-grounded.\n");
|
||||
print_node("conjecture", &nodes[0], T0);
|
||||
|
||||
/* Beat 1: 3 independent corroborators have grounded up around the conjecture. */
|
||||
g.n_edges = 0;
|
||||
for (int i = 1; i <= 3; i++)
|
||||
edges[g.n_edges++] = (GepEdge){ 0, i, 0.15, +1 };
|
||||
beat_and_report(&g, 0, T0, 1, "3 independent grounded corroborators appear");
|
||||
|
||||
/* Beat 2: the neighborhood fills in — 5 independent corroborators now. */
|
||||
g.n_edges = 0;
|
||||
for (int i = 1; i <= 5; i++)
|
||||
edges[g.n_edges++] = (GepEdge){ 0, i, 0.15, +1 };
|
||||
beat_and_report(&g, 0, T0 + BEAT_MS, 2, "neighborhood grows to 5 corroborators");
|
||||
|
||||
/* Beat 3: support sustained at 5 (grounding refreshed). */
|
||||
beat_and_report(&g, 0, T0 + 2 * BEAT_MS, 3, "support sustained (5)");
|
||||
|
||||
/* Beats 4-6: corroboration REMOVED (neighbors superseded / no longer ground).
|
||||
* No new events; the collection ages → standing relaxes. Nothing is settled. */
|
||||
g.n_edges = 0;
|
||||
beat_and_report(&g, 0, T0 + 12 * BEAT_MS, 4, "corroboration withdrawn (+10min)");
|
||||
beat_and_report(&g, 0, T0 + 60 * BEAT_MS, 5, "still withdrawn (+1h)");
|
||||
beat_and_report(&g, 0, T0 + 240 * BEAT_MS, 6, "still withdrawn (+4h)");
|
||||
printf(" RESULT: grounding grew automatically past threshold and graduated,\n"
|
||||
" then relaxed once the independent support stopped — living,\n"
|
||||
" not a latched flag.\n");
|
||||
}
|
||||
|
||||
/* ── Scenario B — DECAY via accreting contradiction ─────────────────────── */
|
||||
static void scenario_B(void) {
|
||||
printf("\n=== SCENARIO B — DECAY: convergent independent CONTRADICTION ===\n");
|
||||
static GepNode nodes[6];
|
||||
static GepEdge edges[5];
|
||||
memset(nodes, 0, sizeof nodes);
|
||||
nodes[0].id = "belief"; nodes[0].is_belief = 1;
|
||||
/* Seed the belief as already GROUNDED via a strong prior LTP event. */
|
||||
seed_grounded(&nodes[0], 0.85, T0);
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
nodes[i].id = "refuter";
|
||||
seed_grounded(&nodes[i], 0.80, T0); /* grounded contradictors */
|
||||
}
|
||||
GepGraph g = { nodes, 6, edges, 0 };
|
||||
|
||||
printf(" seed: belief pre-grounded by a strong prior LTP event.\n");
|
||||
print_node("belief", &nodes[0], T0);
|
||||
|
||||
/* Contradiction accretes over successive beats: 3 then 5 independent grounded
|
||||
* refuters (polarity -1). Each beat past threshold appends an LTD event.
|
||||
* Beat 1 runs at the seed instant so the trajectory starts from grounded. */
|
||||
g.n_edges = 0;
|
||||
for (int i = 1; i <= 3; i++) edges[g.n_edges++] = (GepEdge){ 0, i, 0.20, -1 };
|
||||
beat_and_report(&g, 0, T0, 1, "3 independent contradictions");
|
||||
|
||||
g.n_edges = 0;
|
||||
for (int i = 1; i <= 5; i++) edges[g.n_edges++] = (GepEdge){ 0, i, 0.20, -1 };
|
||||
beat_and_report(&g, 0, T0 + BEAT_MS, 2, "contradiction broadens to 5");
|
||||
beat_and_report(&g, 0, T0 + 2 * BEAT_MS, 3, "contradiction sustained (5)");
|
||||
beat_and_report(&g, 0, T0 + 3 * BEAT_MS, 4, "contradiction sustained (5)");
|
||||
printf(" RESULT: grounding decayed grounded->likely->conjecture under\n"
|
||||
" convergent independent contradiction. The door never shut\n"
|
||||
" on the belief; its history is retained (events keep growing).\n");
|
||||
}
|
||||
|
||||
/* ── Scenario C — INDEPENDENCE GUARD ─────────────────────────────────────── */
|
||||
static void scenario_C(void) {
|
||||
printf("\n=== SCENARIO C — INDEPENDENCE GUARD (the load-bearing property) ===\n");
|
||||
printf(" Both sub-cases: N=5 corroborators, edge weight 0.30, corroborator\n"
|
||||
" standing ~0.90. ONLY difference: whether the 5 are independent.\n");
|
||||
|
||||
/* C1 — 5 DISTINCT INDEPENDENT corroborators (no edges among them). */
|
||||
{
|
||||
printf("\n -- C1: 5 DISTINCT independent corroborators --\n");
|
||||
static GepNode nodes[6];
|
||||
static GepEdge edges[5];
|
||||
memset(nodes, 0, sizeof nodes);
|
||||
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
|
||||
for (int i = 1; i <= 5; i++) { nodes[i].id = "corr"; seed_grounded(&nodes[i], 0.80, T0); }
|
||||
for (int i = 1; i <= 5; i++) edges[i-1] = (GepEdge){ 0, i, 0.30, +1 };
|
||||
GepGraph g = { nodes, 6, edges, 5 };
|
||||
print_node("conjecture", &nodes[0], T0);
|
||||
beat_and_report(&g, 0, T0, 1, "5 independent corroborators (no inter-links)");
|
||||
}
|
||||
|
||||
/* C2 — the SAME support echoed: 5 corroborators that are all mutually linked
|
||||
* (a derivation clique — one source echoed through the chain). Same fan-in to
|
||||
* the conjecture, same weights, same standings. Union-find collapses them to
|
||||
* ONE independent component → below N_MIN → NO strengthening. */
|
||||
{
|
||||
printf("\n -- C2: 5 corroborators, but mutually-linked (echo of ONE source) --\n");
|
||||
static GepNode nodes[6];
|
||||
static GepEdge edges[9]; /* 5 to conjecture + 4 chaining corr1..corr5 */
|
||||
memset(nodes, 0, sizeof nodes);
|
||||
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
|
||||
for (int i = 1; i <= 5; i++) { nodes[i].id = "corr"; seed_grounded(&nodes[i], 0.80, T0); }
|
||||
int ne = 0;
|
||||
for (int i = 1; i <= 5; i++) edges[ne++] = (GepEdge){ 0, i, 0.30, +1 };
|
||||
/* chain corr1-corr2-corr3-corr4-corr5: they are the same source echoed */
|
||||
for (int i = 1; i <= 4; i++) edges[ne++] = (GepEdge){ i, i+1, 0.30, +1 };
|
||||
GepGraph g = { nodes, 6, edges, ne };
|
||||
print_node("conjecture", &nodes[0], T0);
|
||||
beat_and_report(&g, 0, T0, 1, "5 echoed (mutually-linked) corroborators");
|
||||
}
|
||||
|
||||
/* C3 — degenerate echo: literally ONE corroborator reached by 5 parallel edges. */
|
||||
{
|
||||
printf("\n -- C3: ONE corroborator, reached by 5 parallel edges --\n");
|
||||
static GepNode nodes[2];
|
||||
static GepEdge edges[5];
|
||||
memset(nodes, 0, sizeof nodes);
|
||||
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
|
||||
nodes[1].id = "corr"; seed_grounded(&nodes[1], 0.80, T0);
|
||||
for (int i = 0; i < 5; i++) edges[i] = (GepEdge){ 0, 1, 0.30, +1 };
|
||||
GepGraph g = { nodes, 2, edges, 5 };
|
||||
print_node("conjecture", &nodes[0], T0);
|
||||
beat_and_report(&g, 0, T0, 1, "same node, 5 parallel edges");
|
||||
}
|
||||
|
||||
printf("\n RESULT: identical raw fan-in (5) and mass inputs; C1 grounds because\n"
|
||||
" the corroboration is INDEPENDENT (5 components), C2/C3 do not\n"
|
||||
" because it collapses to ONE source. Circular self-reinforcement\n"
|
||||
" cannot manufacture grounding.\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("GROUNDED EDGE-PROPAGATION — PROOF LEDGER (task #50)\n");
|
||||
printf("constants: BASE=%.2f LIKELY_MIN=%.2f GROUNDED_MIN=%.2f "
|
||||
"N_MIN=%d THETA=%.2f D=%.1f\n",
|
||||
(double)GEP_BASE, (double)GEP_LIKELY_MIN, (double)GEP_GROUNDED_MIN,
|
||||
GEP_N_MIN, (double)GEP_THETA, (double)GEP_DECAY_D);
|
||||
scenario_A();
|
||||
scenario_B();
|
||||
scenario_C();
|
||||
printf("\nDONE.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// server.route.patch.el — GATED route for task #50, for engram/src/server.el.
|
||||
// NOT APPLIED. Exposes the engram_ground_propagate native over HTTP so the
|
||||
// soul's consolidation beat can fire one grounded edge-propagation pass.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// [1] New handler — add beside route_strengthen (server.el ~line 194).
|
||||
// Mutation (appends grounding events, updates confidence), so it is gated
|
||||
// on _auth via check_auth_ok, exactly like /api/edges. Persists once after
|
||||
// the beat — the whole point of running propagation as one batched beat
|
||||
// rather than per-node is to pay the snapshot cost a single time.
|
||||
fn route_ground_propagate(method: String, path: String, body: String) -> String {
|
||||
if !check_auth_ok(method, body) { return err_json("unauthorized") }
|
||||
let tel: String = engram_ground_propagate() // native — one beat over the store
|
||||
let saved: Int = persist_canonical()
|
||||
return tel // gep_* telemetry JSON straight through
|
||||
}
|
||||
|
||||
// [2] Dispatch — register in handle_request (server.el ~line 461, next to the
|
||||
// /api/strengthen arm):
|
||||
//
|
||||
// if str_eq(method, "POST") && (str_eq(clean, "/api/ground/propagate")) {
|
||||
// return route_ground_propagate(method, clean, body)
|
||||
// }
|
||||
//
|
||||
// [3] Native declaration — engram_ground_propagate must be declared as an
|
||||
// extern runtime builtin (el_runtime.h) and seed-wrapped (el_seed.c /
|
||||
// el_seed.h __engram_ground_propagate) so the EL side can call it, same as
|
||||
// engram_strengthen / engram_hebb_drain_json.
|
||||
+49
-248
@@ -76,43 +76,13 @@ fn route_stats(method: String, path: String, body: String) -> String {
|
||||
engram_stats_json()
|
||||
}
|
||||
|
||||
// (2026-07-18 self-review) Scoping sweep: `let` inside an if-block creates an
|
||||
// inner scope only — it does NOT mutate the outer binding (documented with
|
||||
// evidence in awareness.el, 2026-05-25). Every default/reassignment below used
|
||||
// that broken pattern, so defaults never applied: nodes were created with
|
||||
// node_type="" and salience=0.0, /api/search and /api/activate ALWAYS ran with
|
||||
// q="" regardless of input, edges defaulted to relation=""/weight=0.0, and
|
||||
// save/load with no "path" hit engram_save(""). Rewritten to the
|
||||
// `let x = if cond { a } else { b }` expression form (the pattern the newer
|
||||
// routes route_emit_ise/route_capture_knowledge already use correctly).
|
||||
// persist_canonical — save the canonical snapshot after a durable write.
|
||||
//
|
||||
// WHY (2026-07-22 self-review): the 2026-07-21 fix correctly stopped READ
|
||||
// routes from writing the canonical snapshot.json — but nothing was left
|
||||
// that saved it on WRITE. Every mutation (node create, edge create,
|
||||
// knowledge capture, forget, merge) lived only in RAM until someone POSTed
|
||||
// /api/save manually; a process restart silently discarded everything since
|
||||
// the last manual save. Observed live: two engram restarts during the
|
||||
// 2026-07-22 review reverted the store to a ~17h-old snapshot, destroying
|
||||
// same-day writes. Reads must never write the canonical; writes must always
|
||||
// persist it. ISE telemetry is deliberately excluded (48h-pruned, loss-
|
||||
// tolerant, ~2/min — snapshotting the whole store per heartbeat is waste;
|
||||
// any durable write that follows persists the pruning too).
|
||||
fn persist_canonical() -> Int {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
engram_save(dir + "/snapshot.json")
|
||||
return 1
|
||||
}
|
||||
|
||||
fn route_create_node(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
let nt_raw: String = json_get_string(body, "node_type")
|
||||
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
|
||||
let sal_raw: Float = json_get_float(body, "salience")
|
||||
let salience: Float = if sal_raw == 0.0 { 0.5 } else { sal_raw }
|
||||
let node_type: String = json_get_string(body, "node_type")
|
||||
if str_eq(node_type, "") { let node_type = "Memory" }
|
||||
let salience: Float = json_get_float(body, "salience")
|
||||
if salience == 0.0 { let salience = 0.5 }
|
||||
let id: String = engram_node(content, node_type, salience)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||
}
|
||||
|
||||
@@ -133,14 +103,13 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
||||
// via engram_save → fs_read of a SCRATCH export path. (2026-07-21 self-review:
|
||||
// previously this saved over the canonical snapshot.json on every GET — if the
|
||||
// process ever booted with a partial/empty store, the first read request
|
||||
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||
// via engram_save → fs_read of the canonical on-disk snapshot, which the
|
||||
// runtime keeps in lockstep with the in-memory graph. Live against the
|
||||
// running graph, not a stale export.
|
||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let snap_path: String = dir + "/.scan-export.json"
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let snap_path: String = dir + "/snapshot.json"
|
||||
engram_save(snap_path)
|
||||
let snap: String = fs_read(snap_path)
|
||||
if str_eq(snap, "") { return "[]" }
|
||||
@@ -153,34 +122,40 @@ fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
fn route_search(method: String, path: String, body: String) -> String {
|
||||
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||||
let lim_url: Int = query_int(path, "limit", 0)
|
||||
let lim_body: Int = json_get_int(body, "limit")
|
||||
let lim_either: Int = if lim_url > 0 { lim_url } else { lim_body }
|
||||
let limit: Int = if lim_either > 0 { lim_either } else { 20 }
|
||||
let q: String = ""
|
||||
if str_eq(method, "GET") {
|
||||
let q = query_param(path, "q")
|
||||
} else {
|
||||
let q = json_get_string(body, "query")
|
||||
}
|
||||
let limit: Int = query_int(path, "limit", 20)
|
||||
if limit == 0 { let limit = json_get_int(body, "limit") }
|
||||
if limit == 0 { let limit = 20 }
|
||||
return engram_search_json(q, limit)
|
||||
}
|
||||
|
||||
fn route_activate(method: String, path: String, body: String) -> String {
|
||||
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||||
// Guard: engram_activate with an empty query matches zero seeds, which
|
||||
// zeroes ALL carried working-memory weights (documented in awareness.el
|
||||
// perceive()). Never let an empty activation through to wipe WM.
|
||||
if str_eq(q, "") { return err_json("missing query") }
|
||||
let d_raw: Int = if str_eq(method, "GET") { query_int(path, "depth", 3) } else { json_get_int(body, "depth") }
|
||||
let depth: Int = if d_raw > 0 { d_raw } else { 3 }
|
||||
let q: String = ""
|
||||
let depth: Int = 3
|
||||
if str_eq(method, "GET") {
|
||||
let q = query_param(path, "q")
|
||||
let depth = query_int(path, "depth", 3)
|
||||
} else {
|
||||
let q = json_get_string(body, "query")
|
||||
let bd: Int = json_get_int(body, "depth")
|
||||
if bd > 0 { let depth = bd }
|
||||
}
|
||||
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
||||
}
|
||||
|
||||
fn route_create_edge(method: String, path: String, body: String) -> String {
|
||||
let from_id: String = json_get_string(body, "from_id")
|
||||
let to_id: String = json_get_string(body, "to_id")
|
||||
let rel_raw: String = json_get_string(body, "relation")
|
||||
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
|
||||
let w_raw: Float = json_get_float(body, "weight")
|
||||
let weight: Float = if w_raw == 0.0 { 0.5 } else { w_raw }
|
||||
let relation: String = json_get_string(body, "relation")
|
||||
if str_eq(relation, "") { let relation = "associates" }
|
||||
let weight: Float = json_get_float(body, "weight")
|
||||
if weight == 0.0 { let weight = 0.5 }
|
||||
engram_connect(from_id, to_id, weight, relation)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||
}
|
||||
|
||||
@@ -195,7 +170,6 @@ fn route_strengthen(method: String, path: String, body: String) -> String {
|
||||
let id: String = json_get_string(body, "node_id")
|
||||
if str_eq(id, "") { return err_json("missing node_id") }
|
||||
engram_strengthen(id)
|
||||
let saved: Int = persist_canonical()
|
||||
ok_json()
|
||||
}
|
||||
|
||||
@@ -203,24 +177,27 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
||||
let id: String = extract_id(path, "/api/nodes/")
|
||||
if str_eq(id, "") { return err_json("missing id") }
|
||||
engram_forget(id)
|
||||
let saved: Int = persist_canonical()
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_save(p)
|
||||
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
||||
}
|
||||
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_load(p)
|
||||
ok_json()
|
||||
}
|
||||
@@ -229,148 +206,6 @@ fn route_health(method: String, path: String, body: String) -> String {
|
||||
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
|
||||
}
|
||||
|
||||
// route_sync — return a snapshot of non-ISE/non-Working nodes for the soul daemon
|
||||
// to merge into its in-process graph via engram_load_merge.
|
||||
//
|
||||
// The soul calls GET /api/sync every SOUL_REFRESH_MS (default 10 min) to pull
|
||||
// new Knowledge/Memory/BacklogItem nodes from the authoritative HTTP Engram into
|
||||
// its in-process working store. Previously this returned 404 "not found", causing
|
||||
// the soul to write the error JSON to a temp file and attempt an empty merge.
|
||||
//
|
||||
// Strategy: save the current snapshot to disk, read it back, return the full
|
||||
// snapshot JSON. The soul's engram_load_merge handles large files gracefully
|
||||
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
||||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||||
fn route_sync(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||
let snap_path: String = dir + "/.sync-export.json"
|
||||
engram_save(snap_path)
|
||||
let snap: String = fs_read(snap_path)
|
||||
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
||||
return snap
|
||||
}
|
||||
|
||||
// route_load_merge — POST /api/load-merge {"path": "..."} — merge a snapshot
|
||||
// file into the live store WITHOUT resetting it (engram_load_merge skips nodes
|
||||
// already present by id). Added 2026-07-21 self-review to restore the 244 kn-
|
||||
// identity Knowledge nodes lost from the snapshot lineage between 05-13 and
|
||||
// 07-13. Requires an explicit path: refuses to run without one so it can never
|
||||
// be triggered accidentally against a default.
|
||||
fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") { return err_json("path is required") }
|
||||
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||
let before_n: Int = engram_node_count()
|
||||
let before_e: Int = engram_edge_count()
|
||||
engram_load_merge(p)
|
||||
let added_n: Int = engram_node_count() - before_n
|
||||
let added_e: Int = engram_edge_count() - before_e
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||||
}
|
||||
|
||||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||||
//
|
||||
// Endpoint: POST /api/neuron/state-events
|
||||
// Body: {"content": "<json-string>"}
|
||||
//
|
||||
// Auth: exempt (internal endpoint, soul daemon on same host, no _auth needed).
|
||||
// The soul's ise_post() sends {"content":"..."} without _auth; enforcing auth
|
||||
// here would silently drop all heartbeat/curiosity ISEs. Unauthenticated POST
|
||||
// to this endpoint is acceptable: ISE writes are observability-only, append-only,
|
||||
// and come from a trusted process on localhost.
|
||||
//
|
||||
// Salience/importance set to match engram_node_full ISE defaults used by the
|
||||
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
|
||||
// confidence=0.8, tier=Episodic).
|
||||
// (2026-06-26 self-review: added this route after discovering ise_post was
|
||||
// silently failing — the soul posts here but the endpoint didn't exist.)
|
||||
//
|
||||
// Retention (2026-07-16 self-review): an earlier comment here claimed ISEs
|
||||
// got temporal_decay_rate=1.617 — that was never implemented (engram_node_full
|
||||
// hardcodes 0.0), and per-node decay only dampens activation anyway; it never
|
||||
// removes nodes. By 2026-07-16 ISEs were 75% of the store (10,175 of 13,522
|
||||
// nodes, ~4,300/day, unbounded). ISEs are already WM-excluded in
|
||||
// engram_activate, so the fix is retention, not decay: every insert calls
|
||||
// engram_prune_telemetry(), a single O(nodes+edges) compaction pass that
|
||||
// removes ISEs older than ENGRAM_ISE_RETENTION_MS (default 48h), protecting
|
||||
// "session-start" labels and self_review events as durable history. At
|
||||
// ~3 ISEs/min this bounds telemetry at ~8.6k nodes instead of growing forever.
|
||||
fn route_emit_ise(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { return err_json("missing content") }
|
||||
let sal: Float = 0.3
|
||||
let imp: Float = 0.3
|
||||
let conf: Float = 0.8
|
||||
let id: String = engram_node_full(
|
||||
content, "InternalStateEvent", "state-event",
|
||||
sal, imp, conf,
|
||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||
)
|
||||
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
|
||||
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
|
||||
let pruned: Int = engram_prune_telemetry(ret_ms)
|
||||
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
|
||||
}
|
||||
|
||||
// ── Knowledge capture ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// route_capture_knowledge — direct Knowledge-node capture over HTTP.
|
||||
//
|
||||
// Endpoint: POST /api/neuron/knowledge/capture (auth required: "_auth" in body)
|
||||
// Body: {"content": "...", "title": "...", "category": "...",
|
||||
// "tier": "note|lesson|canonical", "tags": [...], "project": "...",
|
||||
// "_auth": "<key>"}
|
||||
//
|
||||
// WHY (2026-07-15 self-review): the world-ingestor integrator was designed
|
||||
// against this endpoint (its MCP-unavailable fallback), but the route never
|
||||
// existed — every direct push 404'd, and because the auth gate ran before
|
||||
// routing, the failure surfaced as {"error":"unauthorized"} and was
|
||||
// misdiagnosed for two weeks while world knowledge silently dropped.
|
||||
// POST /api/nodes was no substitute: it discards label/tags/tier, which
|
||||
// makes captured knowledge invisible to tag-scoped search and curiosity.
|
||||
//
|
||||
// The incoming knowledge tier (note/lesson/canonical) is preserved as a
|
||||
// "tier:<x>" tag rather than mapped onto Engram's cognitive tiers — Knowledge
|
||||
// nodes land in Semantic (stable reference), and the epistemic tier stays
|
||||
// queryable without inventing a lossy mapping.
|
||||
fn route_capture_knowledge(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { return err_json("missing content") }
|
||||
let title: String = json_get_string(body, "title")
|
||||
let label: String = if str_eq(title, "") { str_slice(content, 0, 60) } else { title }
|
||||
let category_raw: String = json_get_string(body, "category")
|
||||
let category: String = if str_eq(category_raw, "") { "other" } else { category_raw }
|
||||
let ktier_raw: String = json_get_string(body, "tier")
|
||||
let ktier: String = if str_eq(ktier_raw, "") { "note" } else { ktier_raw }
|
||||
let project: String = json_get_string(body, "project")
|
||||
let tags_raw: String = json_get_raw(body, "tags")
|
||||
let tags_base: String = if str_eq(tags_raw, "") { "[]" } else { tags_raw }
|
||||
// Merge category/tier/project markers into the tag array. Search matches
|
||||
// against the tags string, so these make captures findable by facet.
|
||||
let base_len: Int = str_len(tags_base)
|
||||
let head: String = str_slice(tags_base, 0, base_len - 1)
|
||||
let sep: String = if str_eq(head, "[") { "" } else { "," }
|
||||
let safe_cat: String = str_replace(category, "\"", "'")
|
||||
let safe_tier: String = str_replace(ktier, "\"", "'")
|
||||
let safe_proj: String = str_replace(project, "\"", "'")
|
||||
let proj_tag: String = if str_eq(safe_proj, "") { "" } else { ",\"project:" + safe_proj + "\"" }
|
||||
let tags: String = head + sep + "\"category:" + safe_cat + "\",\"tier:" + safe_tier + "\"" + proj_tag + "]"
|
||||
let sal: Float = 0.5
|
||||
let imp: Float = 0.5
|
||||
let conf: Float = 0.9
|
||||
let id: String = engram_node_full(
|
||||
content, "Knowledge", label,
|
||||
sal, imp, conf,
|
||||
"Semantic", tags
|
||||
)
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||||
@@ -397,22 +232,11 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// ISE posting is auth-exempt (internal soul daemon, same host, no _auth key)
|
||||
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/state-events") {
|
||||
return route_emit_ise(method, path, body)
|
||||
}
|
||||
|
||||
// Auth (when ENGRAM_API_KEY is set)
|
||||
if !check_auth_ok(method, body) {
|
||||
return err_json("unauthorized")
|
||||
}
|
||||
|
||||
// Knowledge capture (auth enforced above; the world-ingestor integrator
|
||||
// and any headless session without MCP push knowledge through this)
|
||||
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/knowledge/capture") {
|
||||
return route_capture_knowledge(method, path, body)
|
||||
}
|
||||
|
||||
// Stats
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
|
||||
return route_stats(method, path, body)
|
||||
@@ -469,45 +293,22 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||||
return route_load(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load-merge") || str_eq(clean, "/load-merge")) {
|
||||
return route_load_merge(method, path, body)
|
||||
}
|
||||
|
||||
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
||||
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
||||
return route_sync(method, path, body)
|
||||
}
|
||||
|
||||
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
|
||||
}
|
||||
|
||||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bind_raw: String = env("ENGRAM_BIND")
|
||||
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||||
let bind_str: String = env("ENGRAM_BIND")
|
||||
if str_eq(bind_str, "") { let bind_str = ":8742" }
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
let data_dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data_dir_raw }
|
||||
let data_dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
|
||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||
engram_load(snapshot_path)
|
||||
|
||||
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
|
||||
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
|
||||
// Preserve the evidence and warn loudly — and since read routes no longer write
|
||||
// the canonical path, a bad boot can no longer clobber the good snapshot.
|
||||
let boot_snap: String = fs_read(snapshot_path)
|
||||
if !str_eq(boot_snap, "") {
|
||||
if engram_node_count() == 0 {
|
||||
println("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes — preserving copy at snapshot.failed-load.json")
|
||||
fs_write(data_dir + "/snapshot.failed-load.json", boot_snap)
|
||||
} else {
|
||||
// Good load: keep a boot-time backup of the snapshot as loaded.
|
||||
fs_write(data_dir + "/snapshot.boot-backup.json", boot_snap)
|
||||
}
|
||||
}
|
||||
|
||||
println("[engram] runtime-native graph engine")
|
||||
println("[engram] data_dir=" + data_dir)
|
||||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||||
|
||||
@@ -17,16 +17,6 @@
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated install.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn manifest_name(src: String) -> String // manifest.el
|
||||
extern fn manifest_deps(src: String) -> String // manifest.el
|
||||
extern fn registry_token() -> String // registry.el
|
||||
extern fn registry_find(name: String, version: String) -> String // registry.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Install paths ─────────────────────────────────────────────────────────────
|
||||
|
||||
// packages_dir returns the root directory for installed vessels.
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// These symbols are defined in sibling epm modules or the El runtime and are
|
||||
// resolved at link time. The `extern fn` decls give elc the C prototype so the
|
||||
// generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang)
|
||||
// that reject implicit function declarations. Signature arity must match the
|
||||
// definition; return/param types are informational (all lower to el_val_t).
|
||||
extern fn config(key: String) -> String // El runtime builtin
|
||||
extern fn read_installed() -> String // install.el
|
||||
|
||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
||||
|
||||
@@ -6,15 +6,6 @@
|
||||
// Depends on: registry.el (registry_latest_version, registry_find),
|
||||
// install.el (read_installed, install_vessel, installed_version)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated update.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn read_installed() -> String // install.el
|
||||
extern fn installed_version(name: String) -> String // install.el
|
||||
extern fn install_vessel(name: String, version: String) -> Bool // install.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Semver helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// semver_part extracts the Nth dot-separated component from a semver string.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,711 +0,0 @@
|
||||
/*
|
||||
* ElBridge.java — Android Java companion to el_android.c.
|
||||
*
|
||||
* All public methods are static. The C JNI layer calls these to create views,
|
||||
* set properties, and manage the widget tree. Views are identified by integer
|
||||
* slot indices matching the C-side handle values.
|
||||
*
|
||||
* Threading: every method that touches a View dispatches to the UI thread
|
||||
* using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch
|
||||
* until the UI thread completes the operation. This mirrors the AppKit
|
||||
* dispatch_sync(main_queue, ^{}) pattern in el_appkit.m.
|
||||
*
|
||||
* Callbacks: Java sets listeners on views that call back into C via:
|
||||
* nativeOnClick(int slot)
|
||||
* nativeOnChange(int slot, String text)
|
||||
* nativeOnSubmit(int slot, String text)
|
||||
* These are declared native and implemented in el_android.c.
|
||||
*
|
||||
* Usage (in your Activity.onCreate):
|
||||
* System.loadLibrary("elruntime");
|
||||
* ElBridge.init(this);
|
||||
*
|
||||
* The native library calls __native_init() which calls nativeRegisterActivity
|
||||
* via the C side; alternatively call ElBridge.init(this) directly from Java.
|
||||
*
|
||||
* Compile requirements:
|
||||
* Android minSdkVersion 21 (Lollipop) or higher.
|
||||
* No third-party dependencies — uses only android.* framework classes.
|
||||
* For image loading from arbitrary file paths, BitmapFactory is used.
|
||||
* To replace with Glide/Picasso, edit createImageView only.
|
||||
*/
|
||||
|
||||
package com.neuron.el;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class ElBridge {
|
||||
|
||||
/* ── Native callbacks (implemented in el_android.c) ─────────────────── */
|
||||
|
||||
public static native void nativeOnClick(int slot);
|
||||
public static native void nativeOnChange(int slot, String text);
|
||||
public static native void nativeOnSubmit(int slot, String text);
|
||||
public static native void nativeRegisterActivity(Activity activity);
|
||||
|
||||
/* ── State ───────────────────────────────────────────────────────────── */
|
||||
|
||||
private static final int MAX_SLOTS = 4096;
|
||||
|
||||
private static Activity sActivity;
|
||||
private static Handler sUiHandler;
|
||||
private static View[] sViews = new View[MAX_SLOTS];
|
||||
private static int sNextSlot = 1; /* slot 0 reserved / null */
|
||||
|
||||
/* ── Init ────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Must be called from the Activity before any widget operations.
|
||||
* Typically called from Activity.onCreate after System.loadLibrary.
|
||||
*/
|
||||
public static void init(Activity activity) {
|
||||
sActivity = activity;
|
||||
sUiHandler = new Handler(Looper.getMainLooper());
|
||||
nativeRegisterActivity(activity);
|
||||
}
|
||||
|
||||
/* ── Slot management ─────────────────────────────────────────────────── */
|
||||
|
||||
private static int allocSlot(View v) {
|
||||
/* Find a free slot starting from sNextSlot, wrap around. */
|
||||
for (int i = 0; i < MAX_SLOTS - 1; i++) {
|
||||
int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1;
|
||||
if (sViews[idx] == null) {
|
||||
sViews[idx] = v;
|
||||
sNextSlot = (idx % (MAX_SLOTS - 1)) + 1;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
android.util.Log.e("ElBridge", "allocSlot: slot table full");
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static View getView(int slot) {
|
||||
if (slot <= 0 || slot >= MAX_SLOTS) return null;
|
||||
return sViews[slot];
|
||||
}
|
||||
|
||||
/* ── UI-thread dispatch helper ───────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Dispatch r on the UI thread and block until it completes.
|
||||
* Safe to call from the UI thread itself (runs inline without posting).
|
||||
*/
|
||||
private static void runSync(final Runnable r) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
r.run();
|
||||
} else {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
sUiHandler.post(new Runnable() {
|
||||
@Override public void run() {
|
||||
try { r.run(); } finally { latch.countDown(); }
|
||||
}
|
||||
});
|
||||
try { latch.await(); } catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Integer slot returning runSync helper ───────────────────────────── */
|
||||
|
||||
private interface IntSupplier { int get(); }
|
||||
|
||||
private static int runSyncInt(final IntSupplier s) {
|
||||
final int[] result = { -1 };
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() { result[0] = s.get(); }
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/* ── Context accessor ────────────────────────────────────────────────── */
|
||||
|
||||
private static Context ctx() { return sActivity; }
|
||||
|
||||
/* ── View creation ───────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Create a LinearLayout.
|
||||
* @param orientation 1=VERTICAL, 0=HORIZONTAL
|
||||
* @param spacing gap between children in dp; applied as bottom/right margin
|
||||
*/
|
||||
public static int createLinearLayout(final int orientation, final int spacing) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
LinearLayout ll = new LinearLayout(ctx());
|
||||
ll.setOrientation(orientation == 1
|
||||
? LinearLayout.VERTICAL
|
||||
: LinearLayout.HORIZONTAL);
|
||||
ll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
/* Spacing is stored so addChild can apply margins. */
|
||||
ll.setTag(R_TAG_SPACING, spacing);
|
||||
return allocSlot(ll);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a FrameLayout (ZStack equivalent). */
|
||||
public static int createFrameLayout() {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
FrameLayout fl = new FrameLayout(ctx());
|
||||
fl.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(fl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a ScrollView. */
|
||||
public static int createScrollView() {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
ScrollView sv = new ScrollView(ctx());
|
||||
sv.setLayoutParams(new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
sv.setFillViewport(true);
|
||||
return allocSlot(sv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a TextView with initial text. */
|
||||
public static int createTextView(final String text) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
TextView tv = new TextView(ctx());
|
||||
tv.setText(text != null ? text : "");
|
||||
tv.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(tv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a Button with a label. */
|
||||
public static int createButton(final String label) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
Button btn = new Button(ctx());
|
||||
btn.setText(label != null ? label : "");
|
||||
btn.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(btn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EditText.
|
||||
* @param placeholder hint text
|
||||
* @param singleLine true = single-line text field; false = multi-line text area
|
||||
*/
|
||||
public static int createEditText(final String placeholder, final boolean singleLine) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
EditText et = new EditText(ctx());
|
||||
et.setHint(placeholder != null ? placeholder : "");
|
||||
if (singleLine) {
|
||||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
et.setMaxLines(1);
|
||||
et.setSingleLine(true);
|
||||
} else {
|
||||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
et.setMinLines(3);
|
||||
et.setSingleLine(false);
|
||||
et.setGravity(Gravity.TOP | Gravity.START);
|
||||
}
|
||||
et.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(et);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ImageView, loading from a file path via BitmapFactory.
|
||||
* If path is null/empty the ImageView is created with no image.
|
||||
*/
|
||||
public static int createImageView(final String path) {
|
||||
return runSyncInt(new IntSupplier() {
|
||||
@Override public int get() {
|
||||
ImageView iv = new ImageView(ctx());
|
||||
iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
iv.setAdjustViewBounds(true);
|
||||
if (path != null && !path.isEmpty()) {
|
||||
Bitmap bmp = BitmapFactory.decodeFile(path);
|
||||
if (bmp != null) {
|
||||
iv.setImageBitmap(bmp);
|
||||
} else {
|
||||
android.util.Log.w("ElBridge",
|
||||
"createImageView: failed to decode " + path);
|
||||
}
|
||||
}
|
||||
iv.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return allocSlot(iv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Window operations ───────────────────────────────────────────────── */
|
||||
|
||||
/** Set the Activity's content view to the view at slot. */
|
||||
public static void setContentView(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null && sActivity != null) {
|
||||
sActivity.setContentView(v);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set the Activity title. */
|
||||
public static void setTitle(final String title) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
if (sActivity != null) {
|
||||
sActivity.setTitle(title != null ? title : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Tree operations ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Add child view to parent view.
|
||||
* LinearLayout: child added as arranged child with spacing margin.
|
||||
* ScrollView: child replaces current document view.
|
||||
* FrameLayout / other ViewGroup: plain addView.
|
||||
*/
|
||||
public static void addChild(final int parentSlot, final int childSlot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View parent = getView(parentSlot);
|
||||
View child = getView(childSlot);
|
||||
if (parent == null || child == null) return;
|
||||
|
||||
/* Remove child from existing parent first. */
|
||||
if (child.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) child.getParent()).removeView(child);
|
||||
}
|
||||
|
||||
if (parent instanceof LinearLayout) {
|
||||
LinearLayout ll = (LinearLayout) parent;
|
||||
Object tag = ll.getTag(R_TAG_SPACING);
|
||||
int spacing = (tag instanceof Integer) ? (Integer) tag : 0;
|
||||
LinearLayout.LayoutParams lp;
|
||||
Object existingLp = child.getLayoutParams();
|
||||
if (existingLp instanceof LinearLayout.LayoutParams) {
|
||||
lp = (LinearLayout.LayoutParams) existingLp;
|
||||
} else {
|
||||
lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
/* Apply spacing as margin on the leading/top edge (after first child). */
|
||||
if (ll.getChildCount() > 0 && spacing > 0) {
|
||||
int px = dpToPx(spacing);
|
||||
if (ll.getOrientation() == LinearLayout.VERTICAL) {
|
||||
lp.topMargin = px;
|
||||
} else {
|
||||
lp.leftMargin = px;
|
||||
}
|
||||
}
|
||||
child.setLayoutParams(lp);
|
||||
ll.addView(child);
|
||||
} else if (parent instanceof ScrollView) {
|
||||
ScrollView sv = (ScrollView) parent;
|
||||
sv.removeAllViews();
|
||||
sv.addView(child);
|
||||
} else if (parent instanceof ViewGroup) {
|
||||
((ViewGroup) parent).addView(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove child from its parent. */
|
||||
public static void removeChild(final int parentSlot, final int childSlot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View parent = getView(parentSlot);
|
||||
View child = getView(childSlot);
|
||||
if (parent instanceof ViewGroup && child != null) {
|
||||
((ViewGroup) parent).removeView(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove the view from its parent and release the slot. */
|
||||
public static void destroyView(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
if (v.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) v.getParent()).removeView(v);
|
||||
}
|
||||
sViews[slot] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Property setters ────────────────────────────────────────────────── */
|
||||
|
||||
public static void setText(final int slot, final String text) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
String s = text != null ? text : "";
|
||||
if (v instanceof EditText) {
|
||||
((EditText) v).setText(s);
|
||||
} else if (v instanceof Button) {
|
||||
((Button) v).setText(s);
|
||||
} else if (v instanceof TextView) {
|
||||
((TextView) v).setText(s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String getText(final int slot) {
|
||||
final String[] result = { "" };
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v instanceof TextView) {
|
||||
CharSequence cs = ((TextView) v).getText();
|
||||
result[0] = cs != null ? cs.toString() : "";
|
||||
}
|
||||
}
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/** Set foreground text color. Components in [0,1]. */
|
||||
public static void setTextColor(final int slot, final float r, final float g,
|
||||
final float b, final float a) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v instanceof TextView) {
|
||||
((TextView) v).setTextColor(floatToArgb(r, g, b, a));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set background color using a GradientDrawable so corner radius is preserved. */
|
||||
public static void setBackgroundColor(final int slot, final float r, final float g,
|
||||
final float b, final float a) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ensureGradientBackground(v);
|
||||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||||
gd.setColor(floatToArgb(r, g, b, a));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font family and size.
|
||||
* family: "system" or null → system default; otherwise tries to load by name.
|
||||
* bold: if true uses Typeface.BOLD.
|
||||
*/
|
||||
public static void setFont(final int slot, final String family,
|
||||
final int sizeSp, final boolean bold) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof TextView)) return;
|
||||
TextView tv = (TextView) v;
|
||||
Typeface tf;
|
||||
if (family != null && !family.isEmpty()
|
||||
&& !family.equals("system")) {
|
||||
Typeface base = Typeface.create(family,
|
||||
bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
tf = (base != null) ? base
|
||||
: Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
} else {
|
||||
tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
}
|
||||
tv.setTypeface(tf);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set padding in dp. */
|
||||
public static void setPadding(final int slot, final int top, final int right,
|
||||
final int bottom, final int left) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) {
|
||||
v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set explicit width in dp. Passes MATCH_PARENT for negative values. */
|
||||
public static void setWidth(final int slot, final int widthDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.width = widthDp < 0
|
||||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||||
: dpToPx(widthDp);
|
||||
v.setLayoutParams(lp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set explicit height in dp. */
|
||||
public static void setHeight(final int slot, final int heightDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.height = heightDp < 0
|
||||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||||
: dpToPx(heightDp);
|
||||
v.setLayoutParams(lp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flex weight on a child of a LinearLayout.
|
||||
* flex > 0 → weight = flex, width/height = 0dp (expand).
|
||||
* flex == 0 → weight = 0, wrap_content (shrink to content).
|
||||
*/
|
||||
public static void setFlex(final int slot, final int flex) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||
if (lp instanceof LinearLayout.LayoutParams) {
|
||||
LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp;
|
||||
if (flex > 0) {
|
||||
llp.weight = (float) flex;
|
||||
/* Determine orientation from parent to set 0dp on the right axis. */
|
||||
if (v.getParent() instanceof LinearLayout) {
|
||||
LinearLayout parent = (LinearLayout) v.getParent();
|
||||
if (parent.getOrientation() == LinearLayout.VERTICAL) {
|
||||
llp.height = 0;
|
||||
} else {
|
||||
llp.width = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
llp.weight = 0f;
|
||||
}
|
||||
v.setLayoutParams(llp);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set corner radius in dp using a GradientDrawable background. */
|
||||
public static void setCornerRadius(final int slot, final float radiusDp) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
ensureGradientBackground(v);
|
||||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||||
gd.setCornerRadius(dpToPxF(radiusDp));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setEnabled(final int slot, final boolean enabled) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) v.setEnabled(enabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide a view.
|
||||
* @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics)
|
||||
*/
|
||||
public static void setVisibility(final int slot, final boolean visible) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Event listener registration ─────────────────────────────────────── */
|
||||
|
||||
/** Register an OnClickListener that calls back into C nativeOnClick. */
|
||||
public static void setOnClickListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (v == null) return;
|
||||
final int capturedSlot = slot;
|
||||
v.setOnClickListener(new View.OnClickListener() {
|
||||
@Override public void onClick(View view) {
|
||||
nativeOnClick(capturedSlot);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a TextWatcher on an EditText that calls back nativeOnChange
|
||||
* for every text change.
|
||||
*/
|
||||
public static void setOnChangeListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof EditText)) return;
|
||||
final int capturedSlot = slot;
|
||||
((EditText) v).addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {}
|
||||
@Override public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {}
|
||||
@Override public void afterTextChanged(Editable s) {
|
||||
nativeOnChange(capturedSlot, s != null ? s.toString() : "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an OnEditorActionListener on a single-line EditText that calls
|
||||
* nativeOnSubmit when the user presses the action/enter key.
|
||||
*/
|
||||
public static void setOnSubmitListener(final int slot) {
|
||||
runSync(new Runnable() {
|
||||
@Override public void run() {
|
||||
View v = getView(slot);
|
||||
if (!(v instanceof EditText)) return;
|
||||
final int capturedSlot = slot;
|
||||
((EditText) v).setOnEditorActionListener(
|
||||
new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView tv, int actionId,
|
||||
android.view.KeyEvent event) {
|
||||
nativeOnSubmit(capturedSlot,
|
||||
tv.getText() != null ? tv.getText().toString() : "");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Internal helpers ─────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Tag key used to stash the spacing value on LinearLayouts so addChild
|
||||
* can apply the correct margin between children.
|
||||
* We use a stable integer resource-id-like value; because we do not have
|
||||
* a resources file here we use View.generateViewId() lazily.
|
||||
*/
|
||||
private static int sSpacingTagKey = 0;
|
||||
|
||||
private static int R_TAG_SPACING;
|
||||
static {
|
||||
R_TAG_SPACING = View.generateViewId();
|
||||
}
|
||||
|
||||
/** Convert dp to pixels using the Activity's display metrics. */
|
||||
private static int dpToPx(float dp) {
|
||||
if (sActivity == null) return (int) dp;
|
||||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
private static float dpToPxF(float dp) {
|
||||
if (sActivity == null) return dp;
|
||||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||||
return dp * density;
|
||||
}
|
||||
|
||||
/** Convert RGBA float components (0–1) to an Android ARGB int. */
|
||||
private static int floatToArgb(float r, float g, float b, float a) {
|
||||
int ai = Math.round(a * 255f);
|
||||
int ri = Math.round(r * 255f);
|
||||
int gi = Math.round(g * 255f);
|
||||
int bi = Math.round(b * 255f);
|
||||
return Color.argb(ai, ri, gi, bi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the view has a GradientDrawable background so that both color
|
||||
* and corner radius can be set independently. If the current background
|
||||
* is already a GradientDrawable it is reused; otherwise a new transparent
|
||||
* one is installed.
|
||||
*/
|
||||
private static void ensureGradientBackground(View v) {
|
||||
if (!(v.getBackground() instanceof GradientDrawable)) {
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setColor(Color.TRANSPARENT);
|
||||
v.setBackground(gd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,554 +0,0 @@
|
||||
# Platform Bridge Specification — el-native
|
||||
|
||||
This document is the authoritative reference for anyone implementing a new platform bridge for the el-native widget system. Read it top to bottom before writing a single line of code.
|
||||
|
||||
---
|
||||
|
||||
## What a Platform Bridge Is
|
||||
|
||||
The el compiler (`elc`) emits C code. That C code calls `__`-prefixed functions for everything OS-related: printing, file I/O, threading, and — when building a native UI app — widget creation and event handling. These `__` functions are the *OS boundary*.
|
||||
|
||||
For native UI, the bridge is the translation layer between that fixed C API and whatever platform toolkit you are targeting. The bridge owns:
|
||||
|
||||
1. A **slot table** of up to 4096 widget objects, indexed by `int64_t` handle.
|
||||
2. Implementations of all 33 `__*` widget functions declared in `el_native_target.h`.
|
||||
3. Callback dispatch from platform events back into El function symbols resolved via `dlsym` (or a platform equivalent).
|
||||
|
||||
The thin wrappers in `el_seed.c` (`#ifdef EL_TARGET_*` blocks) marshal between `el_val_t` and native C types, then call through to the bridge. The bridge itself never touches `el_val_t` — it works only with plain C types (`int64_t`, `const char*`, `int`, `float`).
|
||||
|
||||
**Existing bridges:**
|
||||
|
||||
| File | Platform | Toolkit | Language |
|
||||
|------|----------|---------|----------|
|
||||
| `el_appkit.m` | macOS | AppKit | ObjC (MRC) |
|
||||
| `el_gtk4.c` | Linux | GTK4 | C |
|
||||
| `el_win32.c` | Windows | Win32/ComCtl | C |
|
||||
| `el_uikit.m` | iOS | UIKit | ObjC (MRC) |
|
||||
| `el_android.c` + `ElBridge.java` | Android | View/JNI | C + Java |
|
||||
| `el_sdl2.c` | Embedded Linux / Pi | SDL2 | C |
|
||||
| `el_lvgl.c` | Microcontrollers | LVGL | C |
|
||||
|
||||
---
|
||||
|
||||
## The Slot System
|
||||
|
||||
Every widget — window, button, label, container, image — is stored in a static array:
|
||||
|
||||
```c
|
||||
#define EL_<PLATFORM>_MAX_WIDGETS 4096
|
||||
|
||||
typedef struct {
|
||||
ElWidgetKind kind;
|
||||
/* platform-specific object reference (pointer, handle, ID...) */
|
||||
/* callback names */
|
||||
char* cb_click;
|
||||
char* cb_change;
|
||||
} ElWidget;
|
||||
|
||||
static ElWidget _el_widgets[EL_<PLATFORM>_MAX_WIDGETS];
|
||||
```
|
||||
|
||||
Rules:
|
||||
- **Slot 0 is never valid.** Scan starts at index 1. This ensures 0 is never a valid handle.
|
||||
- **Handle = slot index.** An `int64_t` value returned to El code is a direct index into `_el_widgets[]`.
|
||||
- **-1 = invalid.** All create functions return -1 on failure (table full, platform API error).
|
||||
- **Slot is FREE until allocated, FREE again after destroy.** Use an `ElWidgetKind` enum where `0 = EL_WIDGET_FREE` to track liveness.
|
||||
- **4096 slots is the system-wide maximum.** This is intentional and sufficient for any realistic UI. Do not increase it without a compelling reason.
|
||||
|
||||
### Slot allocation and release pattern
|
||||
|
||||
```c
|
||||
static int64_t el_widget_alloc(ElWidgetKind kind, /* platform object ref */) {
|
||||
for (int i = 1; i < EL_<PLATFORM>_MAX_WIDGETS; i++) {
|
||||
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
||||
_el_widgets[i].kind = kind;
|
||||
/* store platform object ref — retain/addref if needed */
|
||||
_el_widgets[i].cb_click = NULL;
|
||||
_el_widgets[i].cb_change = NULL;
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
return -1; /* table full */
|
||||
}
|
||||
|
||||
static ElWidget* el_widget_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_<PLATFORM>_MAX_WIDGETS) return NULL;
|
||||
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
||||
return &_el_widgets[handle];
|
||||
}
|
||||
|
||||
static void el_widget_free(int64_t handle) {
|
||||
ElWidget* w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
/* release platform object ref */
|
||||
w->kind = EL_WIDGET_FREE;
|
||||
free(w->cb_click); w->cb_click = NULL;
|
||||
free(w->cb_change); w->cb_change = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
**NULL handle guard:** `el_widget_get` must return `NULL` for any handle that is 0, negative, out of range, or points to a `FREE` slot. Every `__` function that takes a handle must null-check the result of `el_widget_get` before doing anything. Failing to do so causes crashes or corruption when El code passes an uninitialized handle.
|
||||
|
||||
---
|
||||
|
||||
## The 33 Required Functions
|
||||
|
||||
Every bridge must implement all 33 functions listed below. They are grouped by category. The signatures shown are from `el_native_target.h` and from `el_seed.c`'s wrapper layer; the bridge itself uses plain C types (the wrappers do the `el_val_t` ↔ C-type conversion).
|
||||
|
||||
### Internal C signatures (what the bridge implements)
|
||||
|
||||
These are what your `.c` / `.m` file defines. The `el_seed.c` wrappers call these.
|
||||
|
||||
#### Lifecycle (2 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_init(void);
|
||||
```
|
||||
**Purpose:** Initialize the platform UI toolkit. Must be idempotent (safe to call more than once). Called once from `__native_init` before any widget creation.
|
||||
**Edge cases:** On platforms where the toolkit must be initialized before a display connection is established (X11, Wayland), this is where that happens. On Android, this is a no-op because ElBridge.java calls init from Java.
|
||||
|
||||
```c
|
||||
void el_<platform>_run_loop(void);
|
||||
```
|
||||
**Purpose:** Start the platform event loop. On most platforms this **never returns**. Exceptions: Android (the loop is Java-managed — this must be a no-op) and headless test builds.
|
||||
**Edge cases:** Must be called from the main thread. On iOS/UIKit, calls `UIApplicationMain` which never returns; El code must set `el_main_entry_fn` before calling this.
|
||||
|
||||
#### Window (3 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
|
||||
```
|
||||
**Purpose:** Create a top-level window. `w`/`h` = initial size in logical pixels. `mw`/`mh` = minimum size (0 = no minimum).
|
||||
**Returns:** Slot handle, or -1 on failure.
|
||||
**Edge cases:** NULL or empty `title` must be handled gracefully (use `""`). On mobile (iOS, Android), the concept of a "window" maps to the root view controller / activity root view — create that here.
|
||||
|
||||
```c
|
||||
void el_<platform>_window_show(int64_t handle);
|
||||
```
|
||||
**Purpose:** Make the window visible. On some platforms windows are hidden at creation; this makes them appear.
|
||||
**Edge cases:** NULL handle → return silently. Calling on an already-visible window is a no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_window_set_title(int64_t handle, const char* title);
|
||||
```
|
||||
**Purpose:** Update the window's title bar text at runtime.
|
||||
**Edge cases:** NULL handle or NULL title → no-op.
|
||||
|
||||
#### Layout containers (4 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_vstack_create(int spacing);
|
||||
int64_t el_<platform>_hstack_create(int spacing);
|
||||
```
|
||||
**Purpose:** Create a vertical/horizontal linear container. `spacing` = gap between children in logical pixels.
|
||||
**Returns:** Slot handle, or -1.
|
||||
**Edge cases:** spacing = 0 is valid and common.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_zstack_create(void);
|
||||
```
|
||||
**Purpose:** Create a z-axis layered container (children overlap, no stacking direction). Used for overlays.
|
||||
**Returns:** Slot handle, or -1.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_scroll_create(void);
|
||||
```
|
||||
**Purpose:** Create a scrollable container. Scrolls vertically by default. Only the first child added is the scrollable content.
|
||||
**Returns:** Slot handle, or -1.
|
||||
|
||||
#### Leaf widgets (5 functions)
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_label_create(const char* text);
|
||||
```
|
||||
**Purpose:** Create a non-editable text label.
|
||||
**Edge cases:** NULL/empty text → label with empty string, not a crash.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_button_create(const char* label);
|
||||
```
|
||||
**Purpose:** Create a clickable button. The `label` is the button's visible text.
|
||||
**Edge cases:** The button must wire up an action target at creation time so that click callbacks registered later via `el_<platform>_widget_on_click` will fire. On platforms with target-action (AppKit, UIKit), allocate the delegate object here.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_text_field_create(const char* placeholder);
|
||||
```
|
||||
**Purpose:** Create a single-line text input. `placeholder` is the hint text shown when empty.
|
||||
**Edge cases:** NULL placeholder → no hint text displayed.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_text_area_create(const char* placeholder);
|
||||
```
|
||||
**Purpose:** Create a multi-line text input (scrollable).
|
||||
**Edge cases:** Same as text_field_create. On AppKit, this wraps NSTextView inside NSScrollView — the slot's `obj` points to the scroll view, not the text view.
|
||||
|
||||
```c
|
||||
int64_t el_<platform>_image_create(const char* path_or_name);
|
||||
```
|
||||
**Purpose:** Create an image widget. `path_or_name` can be a filesystem path or a platform resource name. Try path first, fall back to named resource.
|
||||
**Edge cases:** Non-existent path → create an empty image widget (do not crash). NULL → same.
|
||||
|
||||
#### Widget properties (12 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_text(int64_t handle, const char* text);
|
||||
```
|
||||
**Purpose:** Update the text content of a label, button, text field, text area, or window title. Must dispatch on `kind` to use the correct API.
|
||||
**Edge cases:** NULL handle → no-op. NULL text → treat as `""`.
|
||||
|
||||
```c
|
||||
const char* el_<platform>_widget_get_text(int64_t handle);
|
||||
```
|
||||
**Purpose:** Return the current text of a widget. Returns a `const char*` that the `el_seed.c` wrapper wraps into an `el_val_t` string.
|
||||
**Edge cases:** NULL handle → return `""` (never NULL). The caller in `el_seed.c` handles the `strdup` lifetime issue — see the AppKit reference implementation notes.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_color(int64_t h, float r, float g, float b, float a);
|
||||
void el_<platform>_widget_set_bg_color(int64_t h, float r, float g, float b, float a);
|
||||
```
|
||||
**Purpose:** Set foreground (text) color and background color respectively. All channels are normalized floats [0.0, 1.0].
|
||||
**Edge cases:** For containers, `set_color` may be a no-op (no text); `set_bg_color` should set the layer/surface background. On platforms without alpha compositing, clamp alpha to 0 or 1.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_font(int64_t h, const char* family, int size, int bold);
|
||||
```
|
||||
**Purpose:** Set the font on a text-bearing widget. `family` = font family name or `"system"` for the platform default. `size` = point size. `bold` = 0 or 1.
|
||||
**Edge cases:** If `family` is not found, fall back to the system font. Non-text widgets (containers, images) → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_padding(int64_t h, int top, int right, int bottom, int left);
|
||||
```
|
||||
**Purpose:** Set internal padding/insets for a container or text area.
|
||||
**Edge cases:** On platforms where padding is per-view (not per-container), map to the nearest equivalent (margin, insets, text container inset). For leaf widgets other than text areas, this may be a partial no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_width(int64_t h, int width);
|
||||
void el_<platform>_widget_set_height(int64_t h, int height);
|
||||
```
|
||||
**Purpose:** Apply a fixed-size constraint. `width`/`height` in logical pixels.
|
||||
**Edge cases:** On platforms with Auto Layout or constraint systems, add a fixed-size constraint. Do not apply to windows (size is set at creation). Calling multiple times should override the previous constraint, not add another.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_flex(int64_t h, int flex);
|
||||
```
|
||||
**Purpose:** Set the flex/expansion factor. `flex > 0` → the widget expands to fill available space. `flex == 0` → hugs content size.
|
||||
**Edge cases:** Maps to content-hugging priority (AppKit), `GtkWidget::hexpand`/`vexpand` (GTK4), or layout weight (Android). For windows → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_corner_radius(int64_t h, int radius);
|
||||
```
|
||||
**Purpose:** Apply rounded corners to the widget's visual layer.
|
||||
**Edge cases:** Requires backing layer / GPU surface. On platforms without layer compositing (Win32 without DX), this may be a no-op or require manual painting. Radius in logical pixels.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_disabled(int64_t h, int disabled);
|
||||
```
|
||||
**Purpose:** Enable or disable user interaction. `disabled = 1` → greyed out, non-interactive.
|
||||
**Edge cases:** Only meaningful for interactive widgets (button, text field). For containers/labels → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_set_hidden(int64_t h, int hidden);
|
||||
```
|
||||
**Purpose:** Show or hide the widget. `hidden = 1` → invisible but still in layout.
|
||||
**Edge cases:** For windows, map to `orderOut`/`hide` or equivalent. For views, use `setHidden`/`gtk_widget_set_visible` or equivalent.
|
||||
|
||||
#### Tree management (3 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_add_child(int64_t parent, int64_t child);
|
||||
```
|
||||
**Purpose:** Attach a child widget to a parent container. Dispatch on parent kind:
|
||||
- Window → add to root content view/container
|
||||
- VStack/HStack → add as arranged/linear child
|
||||
- ZStack → add as overlapping subview
|
||||
- Scroll → set as document/content view (first child only)
|
||||
- Other → add as plain subview
|
||||
|
||||
**Edge cases:** NULL parent or child handle → no-op. Attempting to add a window as a child → no-op. Adding the same child twice is platform-defined behavior (tolerate it).
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_remove_child(int64_t parent, int64_t child);
|
||||
```
|
||||
**Purpose:** Detach child from its parent. The child slot remains allocated; the widget is not destroyed.
|
||||
**Edge cases:** NULL handles → no-op. Child not currently attached → no-op.
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_destroy(int64_t handle);
|
||||
```
|
||||
**Purpose:** Destroy a widget: remove from superview/parent, release the platform object, release callback strings, mark slot as FREE.
|
||||
**Edge cases:** For windows, close the window. Free any delegate/target objects stored in side tables. After destroy, the handle is invalid — El code must not use it again (this is the caller's responsibility, not enforced here).
|
||||
|
||||
#### Event registration (3 functions)
|
||||
|
||||
```c
|
||||
void el_<platform>_widget_on_click(int64_t h, const char* fn_name);
|
||||
void el_<platform>_widget_on_change(int64_t h, const char* fn_name);
|
||||
void el_<platform>_widget_on_submit(int64_t h, const char* fn_name);
|
||||
```
|
||||
**Purpose:** Register an El callback function by symbol name.
|
||||
- `on_click` → button press
|
||||
- `on_change` → text field value change (keystroke-level)
|
||||
- `on_submit` → text field Enter key (text field only; stored in `cb_click` slot in AppKit)
|
||||
|
||||
The implementation stores `strdup(fn_name)` in the widget's `cb_click` or `cb_change` field. The platform event handler calls `el_<platform>_invoke_cb` (see callback ABI section).
|
||||
**Edge cases:** NULL or empty `fn_name` → clear the callback (`free` + set NULL). Calling on a non-interactive widget (label, image) → no-op or store silently (harmless).
|
||||
|
||||
#### Manifest reader (1 function)
|
||||
|
||||
```c
|
||||
/* Note: __manifest_read is implemented in el_seed.c, not in the bridge. */
|
||||
/* Bridges do NOT need to implement this. */
|
||||
```
|
||||
|
||||
`__manifest_read` is handled entirely in the platform-independent section of `el_seed.c`. It reads a JSON/EL manifest file from the path in the `EL_MANIFEST` environment variable. Bridge authors do not need to implement this.
|
||||
|
||||
---
|
||||
|
||||
## The Callback ABI
|
||||
|
||||
When a platform event fires (button clicked, text changed), the bridge must call back into the El runtime. The mechanism:
|
||||
|
||||
```c
|
||||
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
||||
|
||||
static void el_<platform>_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
|
||||
if (!fn_name || !*fn_name) return;
|
||||
void* sym = dlsym(RTLD_DEFAULT, fn_name);
|
||||
if (!sym) return;
|
||||
ElCb2 fn = (ElCb2)sym;
|
||||
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
||||
}
|
||||
```
|
||||
|
||||
The El callback signature (in El source):
|
||||
```
|
||||
fn handler(handle: Int, data: String) -> Void
|
||||
```
|
||||
|
||||
Which compiles to:
|
||||
```c
|
||||
void handler(int64_t handle, int64_t data)
|
||||
```
|
||||
|
||||
Where `data` is a `const char*` cast to `int64_t` (the el `String` representation). For click events, `data` is `""`. For change/submit events, `data` is the current widget text.
|
||||
|
||||
**On platforms without `dlsym`** (Windows, some embedded systems): use `GetProcAddress(GetModuleHandle(NULL), fn_name)` on Windows, or maintain a manual symbol registration table for embedded targets where dynamic linking is unavailable.
|
||||
|
||||
**Thread safety for callbacks:** Callbacks fired from a background thread must be marshalled to the UI thread before calling into El code. El code may call `__widget_set_text` or other UI functions synchronously from within the callback — those must run on the UI thread.
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety Contract
|
||||
|
||||
**All platform UI operations must execute on the main/UI thread.** This is a hard requirement on every platform (AppKit, UIKit, GTK4, Win32, Android View, SDL2 main thread rule).
|
||||
|
||||
The reference pattern (AppKit):
|
||||
```c
|
||||
static void el_appkit_sync_main(void (^block)(void)) {
|
||||
if ([NSThread isMainThread]) {
|
||||
block();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For other platforms:
|
||||
- **GTK4:** `g_main_context_invoke` or `g_idle_add` + semaphore for synchronous dispatch
|
||||
- **Win32:** `SendMessage(hwnd, WM_APP, ...)` or `PostMessage` + wait
|
||||
- **Android:** `Activity.runOnUiThread`
|
||||
- **SDL2:** All ops must be called from the thread that initialized SDL (the main thread)
|
||||
- **LVGL:** `lv_lock()` / `lv_unlock()` for thread-safe access
|
||||
|
||||
El program flow is: `main()` → build UI (on main thread) → `__native_run_loop()`. Because UI is built before the run loop starts, most widget creation calls are already on the main thread. The sync dispatch wrapper exists to handle callbacks that arrive on worker threads (e.g., network callbacks that update UI).
|
||||
|
||||
---
|
||||
|
||||
## Integration Pattern
|
||||
|
||||
### In `el_native_target.h`
|
||||
|
||||
Add an `#ifdef EL_TARGET_<PLATFORM>` block declaring all 33 `__*` functions with their `el_val_t` signatures (identical to the existing blocks for MACOS, LINUX, etc.):
|
||||
|
||||
```c
|
||||
#ifdef EL_TARGET_<PLATFORM>
|
||||
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_<PLATFORM> */
|
||||
```
|
||||
|
||||
### In `el_seed.c`
|
||||
|
||||
Add an `#ifdef EL_TARGET_<PLATFORM>` block containing:
|
||||
1. `extern` declarations of all `el_<platform>_*` functions (your bridge's C API)
|
||||
2. Thin wrapper functions that marshal `el_val_t` ↔ C types and call through
|
||||
|
||||
The wrapper pattern (copy from the `EL_TARGET_MACOS` block and substitute the platform name):
|
||||
|
||||
```c
|
||||
#ifdef EL_TARGET_<PLATFORM>
|
||||
|
||||
/* Forward declarations — implemented in el_<platform>.c */
|
||||
extern void el_<platform>_init(void);
|
||||
extern void el_<platform>_run_loop(void);
|
||||
extern int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
|
||||
/* ... all others ... */
|
||||
|
||||
/* Wrappers */
|
||||
void __native_init(void) { el_<platform>_init(); }
|
||||
void __native_run_loop(void) { el_<platform>_run_loop(); }
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height) {
|
||||
return (el_val_t)el_<platform>_window_create(
|
||||
EL_CSTR(title),
|
||||
(int)(int64_t)width, (int)(int64_t)height,
|
||||
(int)(int64_t)min_width, (int)(int64_t)min_height);
|
||||
}
|
||||
|
||||
void __window_show(el_val_t h) { el_<platform>_window_show((int64_t)h); }
|
||||
void __window_set_title(el_val_t h, el_val_t t) { el_<platform>_window_set_title((int64_t)h, EL_CSTR(t)); }
|
||||
|
||||
/* ... continue for all 33 functions ... */
|
||||
|
||||
#endif /* EL_TARGET_<PLATFORM> */
|
||||
```
|
||||
|
||||
Key marshalling rules:
|
||||
- `el_val_t` → `int64_t` handle: `(int64_t)h`
|
||||
- `el_val_t` → `int`: `(int)(int64_t)value`
|
||||
- `el_val_t` → `const char*`: `EL_CSTR(value)`
|
||||
- `el_val_t` → `float`: `(float)el_to_float(value)` (for color channels)
|
||||
- `int64_t` handle → `el_val_t`: `(el_val_t)handle`
|
||||
- `const char*` → `el_val_t`: `EL_STR(str)` — but read the get_text note below
|
||||
|
||||
**`__widget_get_text` note:** The bridge returns `const char*`. The `el_seed.c` wrapper wraps it with `EL_STR(s)`. The returned pointer must remain valid until the El program is done with it. The AppKit implementation returns a `strdup`'d string — the caller (seed wrapper) stores it without tracking it in the arena. This is a known lifetime edge; be consistent with the platform's existing pattern.
|
||||
|
||||
---
|
||||
|
||||
## How el Strings Work
|
||||
|
||||
Inside El compiled C code, strings are `el_val_t` values where the value is the `uintptr_t` cast of a `const char*`:
|
||||
|
||||
```c
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
```
|
||||
|
||||
To construct a string result in `el_seed.c`:
|
||||
```c
|
||||
static char* s = strdup("hello");
|
||||
return EL_STR(s);
|
||||
```
|
||||
|
||||
To read a string argument passed from El:
|
||||
```c
|
||||
const char* text = EL_CSTR(some_el_val_t_argument);
|
||||
```
|
||||
|
||||
The `EL_STR` / `EL_CSTR` macros are defined in `el_seed.h` and are available in `el_seed.c`. Bridge files (`el_<platform>.c`) do not use these macros — they only deal in `const char*` at their API boundary.
|
||||
|
||||
---
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
### Duplicate symbols with `el_runtime.c`
|
||||
|
||||
`el_runtime.c` defines many of the same `__`-prefixed symbols as `el_seed.c`. When linking both (required for the native-hello example), the linker will reject duplicate definitions. The build system uses `nmedit` (macOS) to hide the `el_runtime.c` copies of symbols that `el_seed.c` already defines, keeping `el_seed.c` canonical.
|
||||
|
||||
If you are writing a build script for a new platform and see duplicate symbol link errors involving `__println`, `__print`, `__str_len`, etc., apply the same nmedit / `objcopy --weaken-symbol` / `strip --strip-symbol` trick from the macOS build to your platform's object file handling.
|
||||
|
||||
### The `nmedit` trick on macOS
|
||||
|
||||
```bash
|
||||
# Build a keep-list: symbols defined in el_seed.o but also in el_runtime.o
|
||||
nm el_seed.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .seed_T.txt
|
||||
nm el_runtime.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .rt_T.txt
|
||||
# Keep only symbols unique to el_runtime.o
|
||||
comm -23 .rt_T.txt .seed_T.txt > .rt_keep.txt
|
||||
nmedit -s .rt_keep.txt el_runtime.o
|
||||
```
|
||||
|
||||
On Linux, use `objcopy` with `--weaken-symbol` for each duplicate, or link `el_seed.o` before `el_runtime.o` and use `--allow-multiple-definition` if your use case permits it.
|
||||
|
||||
### ObjC bridges must use MRC, not ARC
|
||||
|
||||
Bridges that use Objective-C (AppKit, UIKit) **must** compile without ARC (`-fno-objc-arc`). The reason: the widget table stores `id` values in a plain C struct. ARC cannot insert retain/release through C struct boundaries, and will reject explicit `[obj retain]` / `[obj release]` calls in ARC mode. Use:
|
||||
|
||||
```bash
|
||||
clang -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m
|
||||
```
|
||||
|
||||
### Widget table struct — don't put `id` fields in C structs under ARC
|
||||
|
||||
If you ever add ARC-managed object fields to the `ElWidget` struct, you will get a compile error. Keep the struct C-only (pointers as `void*` if you must, cast when using) or compile as MRC.
|
||||
|
||||
### The `el_seed.c` `__manifest_read` is platform-independent
|
||||
|
||||
`__manifest_read` is already implemented in `el_seed.c` (in the section compiled unconditionally). You do not implement it in your bridge. You do need to declare it in the `el_native_target.h` block for your platform (matching the other platforms), but `el_seed.c` already has the implementation.
|
||||
|
||||
---
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
Before declaring your bridge ready for integration:
|
||||
|
||||
- [ ] All 33 `__*` functions implemented (including `__manifest_read` declaration, implementation is in seed)
|
||||
- [ ] Slot table with 4096 entries, scan starting at index 1
|
||||
- [ ] `el_widget_get` returns NULL for handle 0, negative, out-of-range, and FREE slots
|
||||
- [ ] All functions null-check `el_widget_get` result before use
|
||||
- [ ] `el_<platform>_init` is idempotent
|
||||
- [ ] `el_<platform>_run_loop` either never returns or is documented as a no-op (Android)
|
||||
- [ ] Callback dispatch via `dlsym` (or platform equivalent) implemented
|
||||
- [ ] All UI operations dispatched to the main/UI thread
|
||||
- [ ] `el_native_target.h` updated with `#ifdef EL_TARGET_<PLATFORM>` block
|
||||
- [ ] `el_seed.c` updated with `#ifdef EL_TARGET_<PLATFORM>` extern + wrapper block
|
||||
- [ ] Bridge compiles cleanly with no warnings: `cc -DEL_TARGET_<PLATFORM> -Wall -Wextra -c el_<platform>.c`
|
||||
- [ ] Bridge links cleanly with `el_seed.o` and `el_runtime.o` (duplicate symbol check)
|
||||
- [ ] `detect-platforms` script updated with detection logic for the new platform
|
||||
- [ ] Basic smoke test: create window → add label → show window → run loop
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# detect-platforms — probe available platform bridge dependencies
|
||||
#
|
||||
# Reports which el-native platform bridges can be built on this machine,
|
||||
# with install instructions for anything that is missing.
|
||||
#
|
||||
# Usage: ./detect-platforms
|
||||
# ./build.sh platforms (from native-hello)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
PASS="[+]"
|
||||
FAIL="[ ]"
|
||||
|
||||
_ok() { printf " ${PASS} %-22s %s\n" "$1" "$2"; }
|
||||
_miss() { printf " ${FAIL} %-22s %s\n" "$1" "$2"; }
|
||||
|
||||
# ── Header ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "==> el-native platform detection"
|
||||
echo ""
|
||||
echo " Checking build dependencies for each platform bridge..."
|
||||
echo ""
|
||||
|
||||
AVAILABLE=0
|
||||
MISSING=0
|
||||
|
||||
# ── macOS / AppKit ────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if xcrun --find clang &>/dev/null && xcrun --find xcodebuild &>/dev/null 2>/dev/null || \
|
||||
xcrun --find cc &>/dev/null; then
|
||||
CLT_INFO="Xcode CLT $(xcode-select -p 2>/dev/null | sed 's|/Developer||' || echo '')"
|
||||
_ok "macOS/AppKit" "-DEL_TARGET_MACOS ${CLT_INFO}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (Xcode CLT missing — xcode-select --install)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
else
|
||||
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (not on macOS)"
|
||||
fi
|
||||
|
||||
# ── iOS / UIKit ───────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if xcrun --sdk iphoneos --show-sdk-path &>/dev/null 2>&1; then
|
||||
SDK_VER=$(xcrun --sdk iphoneos --show-sdk-version 2>/dev/null || echo "")
|
||||
_ok "iOS/UIKit" "-DEL_TARGET_IOS SDK ${SDK_VER} (requires Xcode.app)"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "iOS/UIKit" "-DEL_TARGET_IOS (iOS SDK not found — install Xcode.app)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
else
|
||||
_miss "iOS/UIKit" "-DEL_TARGET_IOS (not on macOS — requires Xcode)"
|
||||
fi
|
||||
|
||||
# ── Linux / GTK4 ──────────────────────────────────────────────────────────────
|
||||
|
||||
if pkg-config --exists gtk4 2>/dev/null; then
|
||||
GTK_VER=$(pkg-config --modversion gtk4 2>/dev/null)
|
||||
_ok "Linux/GTK4" "-DEL_TARGET_LINUX gtk4 ${GTK_VER}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Linux/GTK4" "-DEL_TARGET_LINUX (gtk4 not found)"
|
||||
echo " Install: apt install libgtk-4-dev"
|
||||
echo " or: dnf install gtk4-devel"
|
||||
echo " or: brew install gtk4"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── SDL2 / Embedded Linux / Pi ────────────────────────────────────────────────
|
||||
|
||||
SDL2_OK=0
|
||||
if pkg-config --exists sdl2 2>/dev/null; then
|
||||
SDL2_VER=$(pkg-config --modversion sdl2 2>/dev/null)
|
||||
# Also check for SDL2_ttf (needed for text rendering)
|
||||
if pkg-config --exists SDL2_ttf 2>/dev/null; then
|
||||
TTF_VER=$(pkg-config --modversion SDL2_ttf 2>/dev/null)
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER}, SDL2_ttf ${TTF_VER}"
|
||||
else
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (SDL2_ttf missing — needed for text)"
|
||||
echo " Install: apt install libsdl2-ttf-dev"
|
||||
fi
|
||||
SDL2_OK=1
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
elif command -v sdl2-config &>/dev/null; then
|
||||
SDL2_VER=$(sdl2-config --version 2>/dev/null || echo "")
|
||||
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (via sdl2-config)"
|
||||
SDL2_OK=1
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
fi
|
||||
|
||||
if [[ $SDL2_OK -eq 0 ]]; then
|
||||
_miss "SDL2/Embedded" "-DEL_TARGET_SDL2 (sdl2 not found)"
|
||||
echo " Install: apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev"
|
||||
echo " or: brew install sdl2 sdl2_ttf sdl2_image"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── LVGL / Microcontrollers ───────────────────────────────────────────────────
|
||||
|
||||
LVGL_OK=0
|
||||
LVGL_WHERE=""
|
||||
|
||||
if [ -f "${SCRIPT_DIR}/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="./lvgl/lvgl.h"
|
||||
LVGL_OK=1
|
||||
elif [ -f "${SCRIPT_DIR}/../lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="adjacent lvgl/"
|
||||
LVGL_OK=1
|
||||
elif [ -f "/usr/include/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="/usr/include/lvgl"
|
||||
LVGL_OK=1
|
||||
elif [ -f "/usr/local/include/lvgl/lvgl.h" ]; then
|
||||
LVGL_WHERE="/usr/local/include/lvgl"
|
||||
LVGL_OK=1
|
||||
elif pkg-config --exists lvgl 2>/dev/null; then
|
||||
LVGL_WHERE="pkg-config ($(pkg-config --modversion lvgl 2>/dev/null))"
|
||||
LVGL_OK=1
|
||||
fi
|
||||
|
||||
if [[ $LVGL_OK -eq 1 ]]; then
|
||||
_ok "LVGL/MCU" "-DEL_TARGET_LVGL ${LVGL_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "LVGL/MCU" "-DEL_TARGET_LVGL (lvgl.h not found)"
|
||||
echo " Install: git clone https://github.com/lvgl/lvgl"
|
||||
echo " (place lvgl/ next to el-compiler/runtime/)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Android / JNI ─────────────────────────────────────────────────────────────
|
||||
|
||||
ANDROID_OK=0
|
||||
ANDROID_WHERE=""
|
||||
|
||||
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "${ANDROID_NDK_HOME}" ]; then
|
||||
NDK_VER=""
|
||||
if [ -f "${ANDROID_NDK_HOME}/source.properties" ]; then
|
||||
NDK_VER=$(grep "Pkg.Revision" "${ANDROID_NDK_HOME}/source.properties" \
|
||||
2>/dev/null | cut -d= -f2 | tr -d ' ' || echo "")
|
||||
fi
|
||||
ANDROID_WHERE="NDK ${NDK_VER:-(version unknown)} at \$ANDROID_NDK_HOME"
|
||||
ANDROID_OK=1
|
||||
elif command -v ndk-build &>/dev/null; then
|
||||
ANDROID_WHERE="ndk-build in PATH"
|
||||
ANDROID_OK=1
|
||||
elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/ndk" ]; then
|
||||
ANDROID_WHERE="NDK via \$ANDROID_HOME/ndk"
|
||||
ANDROID_OK=1
|
||||
fi
|
||||
|
||||
if [[ $ANDROID_OK -eq 1 ]]; then
|
||||
_ok "Android/JNI" "-DEL_TARGET_ANDROID ${ANDROID_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Android/JNI" "-DEL_TARGET_ANDROID (ANDROID_NDK_HOME not set)"
|
||||
echo " Install: https://developer.android.com/studio/releases/ndk"
|
||||
echo " Then: export ANDROID_NDK_HOME=/path/to/ndk"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Windows / Win32 (cross or native) ─────────────────────────────────────────
|
||||
|
||||
WIN32_OK=0
|
||||
WIN32_WHERE=""
|
||||
|
||||
if [[ "$(uname)" == MINGW* ]] || [[ "$(uname)" == CYGWIN* ]] || \
|
||||
[[ "$(uname)" == MSYS* ]]; then
|
||||
WIN32_WHERE="native Windows ($(uname))"
|
||||
WIN32_OK=1
|
||||
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||
MINGW_VER=$(x86_64-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "")
|
||||
WIN32_WHERE="mingw cross-compiler — ${MINGW_VER}"
|
||||
WIN32_OK=1
|
||||
elif command -v i686-w64-mingw32-gcc &>/dev/null; then
|
||||
WIN32_WHERE="mingw 32-bit cross-compiler"
|
||||
WIN32_OK=1
|
||||
fi
|
||||
|
||||
if [[ $WIN32_OK -eq 1 ]]; then
|
||||
_ok "Windows/Win32" "-DEL_TARGET_WIN32 ${WIN32_WHERE}"
|
||||
AVAILABLE=$((AVAILABLE + 1))
|
||||
else
|
||||
_miss "Windows/Win32" "-DEL_TARGET_WIN32 (mingw cross-compiler not found)"
|
||||
echo " Install: brew install mingw-w64"
|
||||
echo " or: apt install gcc-mingw-w64"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo " ${AVAILABLE} platform(s) available, ${MISSING} unavailable on this machine."
|
||||
echo ""
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/new-platform" ]]; then
|
||||
echo " Scaffold a new bridge: ${SCRIPT_DIR}/new-platform <name>"
|
||||
fi
|
||||
echo " Bridge contract: ${SCRIPT_DIR}/PLATFORM_BRIDGE_SPEC.md"
|
||||
echo ""
|
||||
@@ -1,949 +0,0 @@
|
||||
/*
|
||||
* el_android.c — Android JNI backend for the el native widget system.
|
||||
*
|
||||
* This file implements the Android widget layer that el_seed.c calls through
|
||||
* to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart
|
||||
* to el_appkit.m and presents the same C API surface.
|
||||
*
|
||||
* Architecture:
|
||||
* el program (el code)
|
||||
* → __widget_* C builtins in el_seed.c
|
||||
* → el_android_* C-callable functions declared here
|
||||
* → ElBridge static methods in Java via JNI
|
||||
* → android.view.View subclasses on the UI thread
|
||||
*
|
||||
* Widget handles: every widget (window root, view, control) is assigned an
|
||||
* int64_t slot index into view_slots[]. The el program holds these as opaque
|
||||
* Int values. Slot 0 is never valid (null handle = -1 convention).
|
||||
*
|
||||
* Threading: Android requires all UI operations to run on the main (UI) thread.
|
||||
* Every JNI call that mutates a View is dispatched through
|
||||
* Activity.runOnUiThread(Runnable) if the current thread is not the UI thread.
|
||||
* el_android_run_loop is a no-op — Android lifecycle is driven by the Activity.
|
||||
*
|
||||
* Callback mechanism: when a widget fires an event Java calls
|
||||
* nativeOnClick / nativeOnChange / nativeOnSubmit
|
||||
* The C side looks up the registered El function name for that slot, then:
|
||||
* dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
|
||||
* This matches the __thread_create pattern in el_seed.c exactly.
|
||||
*
|
||||
* Compile / link (as part of libelruntime.so):
|
||||
* Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID.
|
||||
* Link flags: -landroid -llog -ldl
|
||||
*
|
||||
* Java companion: ElBridge.java in the same directory must be compiled into
|
||||
* the Android application's APK (package com.neuron.el).
|
||||
*/
|
||||
|
||||
#ifdef EL_TARGET_ANDROID
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dlfcn.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Logging ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_TAG "ElAndroid"
|
||||
#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__)
|
||||
#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__)
|
||||
#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__)
|
||||
|
||||
/* ── JNI global state ────────────────────────────────────────────────────── */
|
||||
|
||||
static JavaVM *g_jvm = NULL;
|
||||
static jobject g_activity = NULL; /* global ref to Activity */
|
||||
static jclass g_bridge_class = NULL; /* global ref to ElBridge class */
|
||||
|
||||
/* Cached method IDs on ElBridge — filled in el_android_init(). */
|
||||
static jmethodID g_mid_createLinearLayout = NULL;
|
||||
static jmethodID g_mid_createFrameLayout = NULL;
|
||||
static jmethodID g_mid_createScrollView = NULL;
|
||||
static jmethodID g_mid_createTextView = NULL;
|
||||
static jmethodID g_mid_createButton = NULL;
|
||||
static jmethodID g_mid_createEditText = NULL;
|
||||
static jmethodID g_mid_createImageView = NULL;
|
||||
static jmethodID g_mid_setContentView = NULL;
|
||||
static jmethodID g_mid_setTitle = NULL;
|
||||
static jmethodID g_mid_addChild = NULL;
|
||||
static jmethodID g_mid_removeChild = NULL;
|
||||
static jmethodID g_mid_destroyView = NULL;
|
||||
static jmethodID g_mid_setText = NULL;
|
||||
static jmethodID g_mid_getText = NULL;
|
||||
static jmethodID g_mid_setTextColor = NULL;
|
||||
static jmethodID g_mid_setBackgroundColor = NULL;
|
||||
static jmethodID g_mid_setFont = NULL;
|
||||
static jmethodID g_mid_setPadding = NULL;
|
||||
static jmethodID g_mid_setWidth = NULL;
|
||||
static jmethodID g_mid_setHeight = NULL;
|
||||
static jmethodID g_mid_setFlex = NULL;
|
||||
static jmethodID g_mid_setCornerRadius = NULL;
|
||||
static jmethodID g_mid_setEnabled = NULL;
|
||||
static jmethodID g_mid_setVisibility = NULL;
|
||||
static jmethodID g_mid_setOnClickListener = NULL;
|
||||
static jmethodID g_mid_setOnChangeListener = NULL;
|
||||
static jmethodID g_mid_setOnSubmitListener = NULL;
|
||||
static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */
|
||||
|
||||
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_ANDROID_MAX_WIDGETS 4096
|
||||
|
||||
typedef enum {
|
||||
EL_WIDGET_FREE = 0,
|
||||
EL_WIDGET_WINDOW = 1,
|
||||
EL_WIDGET_VSTACK = 2,
|
||||
EL_WIDGET_HSTACK = 3,
|
||||
EL_WIDGET_ZSTACK = 4,
|
||||
EL_WIDGET_SCROLL = 5,
|
||||
EL_WIDGET_LABEL = 6,
|
||||
EL_WIDGET_BUTTON = 7,
|
||||
EL_WIDGET_TEXTFIELD = 8,
|
||||
EL_WIDGET_TEXTAREA = 9,
|
||||
EL_WIDGET_IMAGE = 10,
|
||||
} ElWidgetKind;
|
||||
|
||||
typedef struct {
|
||||
ElWidgetKind kind;
|
||||
jint slot; /* Java-side slot index (matches C index) */
|
||||
char *cb_click; /* El function name for click / submit events */
|
||||
char *cb_change; /* El function name for value-change events */
|
||||
} ElWidget;
|
||||
|
||||
static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS];
|
||||
|
||||
static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) {
|
||||
for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) {
|
||||
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
||||
_el_widgets[i].kind = kind;
|
||||
_el_widgets[i].slot = slot;
|
||||
_el_widgets[i].cb_click = NULL;
|
||||
_el_widgets[i].cb_change = NULL;
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
EL_LOGE("el_widget_alloc: slot table full");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ElWidget *el_widget_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL;
|
||||
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
||||
return &_el_widgets[handle];
|
||||
}
|
||||
|
||||
static void el_widget_free(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
w->kind = EL_WIDGET_FREE;
|
||||
w->slot = -1;
|
||||
free(w->cb_click); w->cb_click = NULL;
|
||||
free(w->cb_change); w->cb_change = NULL;
|
||||
}
|
||||
|
||||
/* ── JNI environment helpers ─────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if
|
||||
* needed (detaches in el_jni_detach_if_attached — call in pairs).
|
||||
*/
|
||||
static int g_was_attached = 0; /* thread-local would be cleaner but this is
|
||||
safe for single-threaded el programs */
|
||||
|
||||
static JNIEnv *el_jni_env(void) {
|
||||
if (!g_jvm) return NULL;
|
||||
JNIEnv *env = NULL;
|
||||
jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6);
|
||||
if (rc == JNI_OK) { g_was_attached = 0; return env; }
|
||||
if (rc == JNI_EDETACHED) {
|
||||
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
|
||||
g_was_attached = 1;
|
||||
return env;
|
||||
}
|
||||
}
|
||||
EL_LOGE("el_jni_env: failed to obtain JNIEnv");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void el_jni_detach_if_attached(void) {
|
||||
if (g_was_attached && g_jvm) {
|
||||
(*g_jvm)->DetachCurrentThread(g_jvm);
|
||||
g_was_attached = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── UI-thread dispatch ──────────────────────────────────────────────────── */
|
||||
/*
|
||||
* Most ElBridge static methods already dispatch to the UI thread internally
|
||||
* (they call Activity.runOnUiThread). The helper below is available for
|
||||
* cases where the caller needs to be sure the call has completed before
|
||||
* returning (ElBridge methods marked "sync" use a CountDownLatch internally).
|
||||
*
|
||||
* For the current implementation we call ElBridge methods directly; ElBridge
|
||||
* itself marshals to the UI thread via Activity.runOnUiThread + latch.
|
||||
* This keeps the C side simple and mirrors the AppKit dispatch_sync pattern.
|
||||
*/
|
||||
|
||||
/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
(void)reserved;
|
||||
g_jvm = vm;
|
||||
EL_LOGI("JNI_OnLoad: el Android bridge loaded");
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
/* ── el_android_init ─────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* Called from __native_init(). The Activity must have already called
|
||||
* ElBridge.registerActivity(activity) from Java before this runs, which sets
|
||||
* g_activity via the nativeRegisterActivity JNI method below.
|
||||
*
|
||||
* Caches all method IDs used later so individual widget calls avoid repeated
|
||||
* FindClass / GetStaticMethodID lookups.
|
||||
*/
|
||||
void el_android_init(void) {
|
||||
static int done = 0;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; }
|
||||
|
||||
jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge");
|
||||
if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; }
|
||||
g_bridge_class = (*env)->NewGlobalRef(env, cls);
|
||||
(*env)->DeleteLocalRef(env, cls);
|
||||
|
||||
#define CACHE_STATIC(var, name, sig) \
|
||||
var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \
|
||||
if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig)
|
||||
|
||||
CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I");
|
||||
CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I");
|
||||
CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I");
|
||||
CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I");
|
||||
CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I");
|
||||
CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V");
|
||||
CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V");
|
||||
CACHE_STATIC(g_mid_addChild, "addChild", "(II)V");
|
||||
CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V");
|
||||
CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V");
|
||||
CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V");
|
||||
CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;");
|
||||
CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V");
|
||||
CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V");
|
||||
CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V");
|
||||
CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V");
|
||||
CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V");
|
||||
CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V");
|
||||
CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V");
|
||||
CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V");
|
||||
CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V");
|
||||
CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V");
|
||||
CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V");
|
||||
CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V");
|
||||
CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V");
|
||||
#undef CACHE_STATIC
|
||||
|
||||
el_jni_detach_if_attached();
|
||||
EL_LOGI("el_android_init: complete");
|
||||
}
|
||||
|
||||
/* ── JNI: Activity registration ─────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Called from Java: ElBridge.registerActivity(activity) calls back here.
|
||||
* Stores a global reference to the Activity so C code can dispatch to it.
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls,
|
||||
jobject activity) {
|
||||
(void)cls;
|
||||
if (g_activity) {
|
||||
(*env)->DeleteGlobalRef(env, g_activity);
|
||||
g_activity = NULL;
|
||||
}
|
||||
if (activity) {
|
||||
g_activity = (*env)->NewGlobalRef(env, activity);
|
||||
EL_LOGI("nativeRegisterActivity: activity registered");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── El callback invocation ──────────────────────────────────────────────── */
|
||||
/*
|
||||
* Invoke an El callback by symbol name.
|
||||
* Signature matches AppKit: fn(handle: Int, data: String) -> Void
|
||||
* compiled to: void fn(el_val_t handle, el_val_t data)
|
||||
*/
|
||||
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
||||
|
||||
static void el_android_invoke_cb(const char *fn_name, int64_t handle,
|
||||
const char *data) {
|
||||
if (!fn_name || !*fn_name) return;
|
||||
void *sym = dlsym(RTLD_DEFAULT, fn_name);
|
||||
if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; }
|
||||
ElCb2 fn = (ElCb2)sym;
|
||||
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
||||
}
|
||||
|
||||
/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) {
|
||||
(void)env; (void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_click) {
|
||||
el_android_invoke_cb(w->cb_click, handle, "");
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls,
|
||||
jint slot, jstring text) {
|
||||
(void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_change) {
|
||||
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
||||
el_android_invoke_cb(w->cb_change, handle, ctext);
|
||||
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls,
|
||||
jint slot, jstring text) {
|
||||
(void)cls;
|
||||
int64_t handle = (int64_t)slot;
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */
|
||||
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
||||
el_android_invoke_cb(w->cb_click, handle, ctext);
|
||||
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Helper: jstring from C string ──────────────────────────────────────── */
|
||||
|
||||
static jstring el_jstr(JNIEnv *env, const char *s) {
|
||||
return (*env)->NewStringUTF(env, s ? s : "");
|
||||
}
|
||||
|
||||
/* ── Window ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_android_window_create — on Android a "window" is the root LinearLayout
|
||||
* set as the Activity's content view. We create a vertical LinearLayout and
|
||||
* store it. el_android_window_show calls setContentView on the Activity.
|
||||
*/
|
||||
int64_t el_android_window_create(const char *title, int width, int height,
|
||||
int min_width, int min_height) {
|
||||
(void)width; (void)height; (void)min_width; (void)min_height;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
|
||||
/* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)1 /* VERTICAL */, (jint)0);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
(*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1;
|
||||
}
|
||||
|
||||
/* Set activity title */
|
||||
if (g_mid_setTitle && title) {
|
||||
jstring jtitle = el_jstr(env, title);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
||||
(*env)->DeleteLocalRef(env, jtitle);
|
||||
}
|
||||
|
||||
int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return handle;
|
||||
}
|
||||
|
||||
void el_android_window_show(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w || w->kind != EL_WIDGET_WINDOW) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_window_set_title(int64_t handle, const char *title) {
|
||||
(void)handle;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jtitle = el_jstr(env, title);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
||||
(*env)->DeleteLocalRef(env, jtitle);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Layout containers ───────────────────────────────────────────────────── */
|
||||
|
||||
int64_t el_android_vstack_create(int spacing) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)1 /* VERTICAL */, (jint)spacing);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_hstack_create(int spacing) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createLinearLayout,
|
||||
(jint)0 /* HORIZONTAL */, (jint)spacing);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_zstack_create(void) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createFrameLayout);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_scroll_create(void) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createScrollView);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
||||
|
||||
int64_t el_android_label_create(const char *text) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jt = el_jstr(env, text);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createTextView, jt);
|
||||
(*env)->DeleteLocalRef(env, jt);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_button_create(const char *label) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jl = el_jstr(env, label);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createButton, jl);
|
||||
(*env)->DeleteLocalRef(env, jl);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_text_field_create(const char *placeholder) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, placeholder);
|
||||
/* singleLine = true */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createEditText, jp, (jboolean)JNI_TRUE);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_text_area_create(const char *placeholder) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, placeholder);
|
||||
/* singleLine = false → multiline EditText */
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createEditText, jp, (jboolean)JNI_FALSE);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
int64_t el_android_image_create(const char *path) {
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return -1;
|
||||
jstring jp = el_jstr(env, path);
|
||||
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
||||
g_mid_createImageView, jp);
|
||||
(*env)->DeleteLocalRef(env, jp);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
||||
int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot);
|
||||
el_jni_detach_if_attached();
|
||||
return h;
|
||||
}
|
||||
|
||||
/* ── Widget property setters ─────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_set_text(int64_t handle, const char *text) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jt = el_jstr(env, text);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText,
|
||||
(jint)w->slot, jt);
|
||||
(*env)->DeleteLocalRef(env, jt);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
const char *el_android_widget_get_text(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return "";
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return "";
|
||||
jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class,
|
||||
g_mid_getText,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; }
|
||||
const char *result = "";
|
||||
if (js) {
|
||||
const char *cstr = (*env)->GetStringUTFChars(env, js, NULL);
|
||||
result = cstr ? strdup(cstr) : "";
|
||||
if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr);
|
||||
(*env)->DeleteLocalRef(env, js);
|
||||
}
|
||||
el_jni_detach_if_attached();
|
||||
return result;
|
||||
}
|
||||
|
||||
void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor,
|
||||
(jint)w->slot, (jfloat)r, (jfloat)g,
|
||||
(jfloat)b, (jfloat)a);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor,
|
||||
(jint)w->slot, (jfloat)r, (jfloat)g,
|
||||
(jfloat)b, (jfloat)a);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
jstring jfam = el_jstr(env, family);
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont,
|
||||
(jint)w->slot, jfam, (jint)size,
|
||||
(jboolean)(bold ? JNI_TRUE : JNI_FALSE));
|
||||
(*env)->DeleteLocalRef(env, jfam);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding,
|
||||
(jint)w->slot, (jint)top, (jint)right,
|
||||
(jint)bottom, (jint)left);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_width(int64_t handle, int width) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth,
|
||||
(jint)w->slot, (jint)width);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_height(int64_t handle, int height) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight,
|
||||
(jint)w->slot, (jint)height);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_flex(int64_t handle, int flex) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex,
|
||||
(jint)w->slot, (jint)flex);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_corner_radius(int64_t handle, int radius) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius,
|
||||
(jint)w->slot, (jfloat)radius);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_disabled(int64_t handle, int disabled) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled,
|
||||
(jint)w->slot,
|
||||
(jboolean)(disabled ? JNI_FALSE : JNI_TRUE));
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_set_hidden(int64_t handle, int hidden) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
/* visible=true means NOT hidden */
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility,
|
||||
(jint)w->slot,
|
||||
(jboolean)(hidden ? JNI_FALSE : JNI_TRUE));
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Child management ─────────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_add_child(int64_t parent, int64_t child) {
|
||||
ElWidget *pw = el_widget_get(parent);
|
||||
ElWidget *cw = el_widget_get(child);
|
||||
if (!pw || !cw) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild,
|
||||
(jint)pw->slot, (jint)cw->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_remove_child(int64_t parent, int64_t child) {
|
||||
ElWidget *pw = el_widget_get(parent);
|
||||
ElWidget *cw = el_widget_get(child);
|
||||
if (!pw || !cw) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild,
|
||||
(jint)pw->slot, (jint)cw->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Event registration ───────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_on_click(int64_t handle, const char *fn_name) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_click);
|
||||
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_click) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_on_change(int64_t handle, const char *fn_name) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_change);
|
||||
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_change) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
void el_android_widget_on_submit(int64_t handle, const char *fn_name) {
|
||||
/* Submit stored in cb_click, same as AppKit. */
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
free(w->cb_click);
|
||||
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
||||
if (!w->cb_click) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (!env || !g_bridge_class) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Widget destroy ───────────────────────────────────────────────────────── */
|
||||
|
||||
void el_android_widget_destroy(int64_t handle) {
|
||||
ElWidget *w = el_widget_get(handle);
|
||||
if (!w) return;
|
||||
JNIEnv *env = el_jni_env();
|
||||
if (env && g_bridge_class) {
|
||||
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView,
|
||||
(jint)w->slot);
|
||||
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
||||
}
|
||||
el_widget_free(handle);
|
||||
el_jni_detach_if_attached();
|
||||
}
|
||||
|
||||
/* ── Manifest reader ─────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* __manifest_read: parse the app{} block from a manifest file.
|
||||
* Returns the raw file contents as an el_val_t (const char* cast).
|
||||
* The caller (el program) parses the returned string.
|
||||
* Reads from the filesystem; for APK assets use the AssetManager path instead.
|
||||
*/
|
||||
static char *el_read_file(const char *path) {
|
||||
if (!path || !*path) return NULL;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len <= 0) { fclose(f); return NULL; }
|
||||
char *buf = (char *)malloc((size_t)len + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
fread(buf, 1, (size_t)len, f);
|
||||
buf[len] = '\0';
|
||||
fclose(f);
|
||||
return buf;
|
||||
}
|
||||
|
||||
el_val_t el_android_manifest_read(const char *path) {
|
||||
char *contents = el_read_file(path);
|
||||
if (!contents) return (el_val_t)(uintptr_t)"";
|
||||
return (el_val_t)(uintptr_t)contents; /* caller owns allocation */
|
||||
}
|
||||
|
||||
/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */
|
||||
/*
|
||||
* These are the functions declared in el_native_target.h under EL_TARGET_ANDROID.
|
||||
* They forward to the el_android_* internal functions above.
|
||||
*
|
||||
* The el_val_t / int64_t ABI matches the AppKit functions exactly:
|
||||
* - Integer params passed as int64_t, extracted with (int)
|
||||
* - String params passed as int64_t, extracted with (const char*)(uintptr_t)
|
||||
* - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted
|
||||
* with el_to_float / bit-cast union
|
||||
*/
|
||||
|
||||
static inline float el_val_to_float(el_val_t v) {
|
||||
union { double d; int64_t i; } u;
|
||||
u.i = v;
|
||||
return (float)u.d;
|
||||
}
|
||||
|
||||
void __native_init(void) {
|
||||
el_android_init();
|
||||
}
|
||||
|
||||
void __native_run_loop(void) {
|
||||
/* No-op on Android — lifecycle is driven by the Activity. */
|
||||
}
|
||||
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height) {
|
||||
return (el_val_t)el_android_window_create(
|
||||
(const char *)(uintptr_t)title,
|
||||
(int)width, (int)height, (int)min_width, (int)min_height);
|
||||
}
|
||||
|
||||
void __window_show(el_val_t handle) {
|
||||
el_android_window_show((int64_t)handle);
|
||||
}
|
||||
|
||||
void __window_set_title(el_val_t handle, el_val_t title) {
|
||||
el_android_window_set_title((int64_t)handle,
|
||||
(const char *)(uintptr_t)title);
|
||||
}
|
||||
|
||||
el_val_t __vstack_create(el_val_t spacing) {
|
||||
return (el_val_t)el_android_vstack_create((int)spacing);
|
||||
}
|
||||
|
||||
el_val_t __hstack_create(el_val_t spacing) {
|
||||
return (el_val_t)el_android_hstack_create((int)spacing);
|
||||
}
|
||||
|
||||
el_val_t __zstack_create(void) {
|
||||
return (el_val_t)el_android_zstack_create();
|
||||
}
|
||||
|
||||
el_val_t __scroll_create(void) {
|
||||
return (el_val_t)el_android_scroll_create();
|
||||
}
|
||||
|
||||
el_val_t __label_create(el_val_t text) {
|
||||
return (el_val_t)el_android_label_create((const char *)(uintptr_t)text);
|
||||
}
|
||||
|
||||
el_val_t __button_create(el_val_t label) {
|
||||
return (el_val_t)el_android_button_create((const char *)(uintptr_t)label);
|
||||
}
|
||||
|
||||
el_val_t __text_field_create(el_val_t placeholder) {
|
||||
return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder);
|
||||
}
|
||||
|
||||
el_val_t __text_area_create(el_val_t placeholder) {
|
||||
return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder);
|
||||
}
|
||||
|
||||
el_val_t __image_create(el_val_t path_or_name) {
|
||||
return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name);
|
||||
}
|
||||
|
||||
void __widget_set_text(el_val_t handle, el_val_t text) {
|
||||
el_android_widget_set_text((int64_t)handle,
|
||||
(const char *)(uintptr_t)text);
|
||||
}
|
||||
|
||||
el_val_t __widget_get_text(el_val_t handle) {
|
||||
return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle);
|
||||
}
|
||||
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a) {
|
||||
el_android_widget_set_color((int64_t)handle,
|
||||
el_val_to_float(r), el_val_to_float(g),
|
||||
el_val_to_float(b), el_val_to_float(a));
|
||||
}
|
||||
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a) {
|
||||
el_android_widget_set_bg_color((int64_t)handle,
|
||||
el_val_to_float(r), el_val_to_float(g),
|
||||
el_val_to_float(b), el_val_to_float(a));
|
||||
}
|
||||
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold) {
|
||||
el_android_widget_set_font((int64_t)handle,
|
||||
(const char *)(uintptr_t)family,
|
||||
(int)size, (int)bold);
|
||||
}
|
||||
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left) {
|
||||
el_android_widget_set_padding((int64_t)handle,
|
||||
(int)top, (int)right, (int)bottom, (int)left);
|
||||
}
|
||||
|
||||
void __widget_set_width(el_val_t handle, el_val_t width) {
|
||||
el_android_widget_set_width((int64_t)handle, (int)width);
|
||||
}
|
||||
|
||||
void __widget_set_height(el_val_t handle, el_val_t height) {
|
||||
el_android_widget_set_height((int64_t)handle, (int)height);
|
||||
}
|
||||
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex) {
|
||||
el_android_widget_set_flex((int64_t)handle, (int)flex);
|
||||
}
|
||||
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius) {
|
||||
el_android_widget_set_corner_radius((int64_t)handle, (int)radius);
|
||||
}
|
||||
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled) {
|
||||
el_android_widget_set_disabled((int64_t)handle, (int)disabled);
|
||||
}
|
||||
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden) {
|
||||
el_android_widget_set_hidden((int64_t)handle, (int)hidden);
|
||||
}
|
||||
|
||||
void __widget_add_child(el_val_t parent, el_val_t child) {
|
||||
el_android_widget_add_child((int64_t)parent, (int64_t)child);
|
||||
}
|
||||
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child) {
|
||||
el_android_widget_remove_child((int64_t)parent, (int64_t)child);
|
||||
}
|
||||
|
||||
void __widget_destroy(el_val_t handle) {
|
||||
el_android_widget_destroy((int64_t)handle);
|
||||
}
|
||||
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_click((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_change((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name) {
|
||||
el_android_widget_on_submit((int64_t)handle,
|
||||
(const char *)(uintptr_t)fn_name);
|
||||
}
|
||||
|
||||
el_val_t __manifest_read(el_val_t path) {
|
||||
return el_android_manifest_read((const char *)(uintptr_t)path);
|
||||
}
|
||||
|
||||
#endif /* EL_TARGET_ANDROID */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,844 +0,0 @@
|
||||
/*
|
||||
* el_lvgl.c — LVGL v9 backend for the el native widget system.
|
||||
*
|
||||
* This file implements the microcontroller/embedded widget layer that el_seed.c
|
||||
* calls through to when EL_TARGET_LVGL is defined.
|
||||
*
|
||||
* Architecture:
|
||||
* el program (el code)
|
||||
* → __widget_* C builtins in el_seed.c
|
||||
* → el_lvgl_* C functions defined here
|
||||
* → lv_obj_t widgets via LVGL v9
|
||||
*
|
||||
* Target platforms: ESP32, STM32, industrial panels, any system with 256KB+
|
||||
* RAM and an LVGL-compatible display driver. No OS required.
|
||||
*
|
||||
* Widget handles: every widget is assigned an int64_t slot index into
|
||||
* g_widgets[]. The el program holds these as opaque Int values.
|
||||
* Slot 0 is reserved; -1 = invalid handle.
|
||||
*
|
||||
* Window model: on embedded there is one screen. __window_create configures
|
||||
* lv_scr_act() as the root container. __window_show is a no-op (the screen
|
||||
* is always visible). __native_run_loop calls lv_task_handler() in a tight
|
||||
* loop — on RTOS this runs inside a dedicated task; on bare metal it IS the
|
||||
* main loop. The host application is responsible for initialising the display
|
||||
* driver and calling lv_tick_inc() before calling __native_run_loop.
|
||||
*
|
||||
* Callback dispatch:
|
||||
* When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing):
|
||||
* dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime.
|
||||
* When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32):
|
||||
* The caller must provide:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
* which maps function names to function pointers via a compile-time table.
|
||||
*
|
||||
* Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font
|
||||
* maps the requested point size to the nearest available Montserrat variant.
|
||||
* Bold is approximated by stepping up two sizes (no separate bold face in the
|
||||
* default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select().
|
||||
*
|
||||
* Compile (hosted test build):
|
||||
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
* # Then link with lvgl.a / lvgl source tree.
|
||||
*
|
||||
* Compile (bare-metal, no dynamic linker):
|
||||
* arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \
|
||||
* -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
*/
|
||||
|
||||
#ifdef EL_TARGET_LVGL
|
||||
|
||||
#include "lvgl/lvgl.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef EL_LVGL_NO_DLSYM
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Callback dispatch macro ─────────────────────────────────────────────── */
|
||||
|
||||
#ifdef EL_LVGL_NO_DLSYM
|
||||
/*
|
||||
* Bare-metal path. The application provides this function:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
* It maps string names → function pointers, typically via a switch on a hash
|
||||
* or a sorted table of {name, fn_ptr} pairs generated by elc.
|
||||
*/
|
||||
extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b))
|
||||
#else
|
||||
/*
|
||||
* Hosted path. dlsym resolves the El symbol at call time.
|
||||
* We use a compound-statement expression (GCC/Clang extension) to avoid
|
||||
* executing dlsym more than once per call.
|
||||
*/
|
||||
#define EL_LVGL_CALL(fn_name, a, b) \
|
||||
({ \
|
||||
typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \
|
||||
_el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \
|
||||
_fn ? _fn((a), (b)) : (el_val_t)0; \
|
||||
})
|
||||
#endif
|
||||
|
||||
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_LVGL_MAX_WIDGETS 4096
|
||||
|
||||
/*
|
||||
* Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay
|
||||
* consistent across all targets.
|
||||
*/
|
||||
typedef enum {
|
||||
EL_LVGL_FREE = 0,
|
||||
EL_LVGL_WINDOW = 1,
|
||||
EL_LVGL_VSTACK = 2,
|
||||
EL_LVGL_HSTACK = 3,
|
||||
EL_LVGL_ZSTACK = 4,
|
||||
EL_LVGL_SCROLL = 5,
|
||||
EL_LVGL_LABEL = 6,
|
||||
EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */
|
||||
EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */
|
||||
EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */
|
||||
EL_LVGL_IMAGE = 10,
|
||||
EL_LVGL_DIVIDER = 11,
|
||||
EL_LVGL_SPACER = 12,
|
||||
} ElLvglKind;
|
||||
|
||||
/*
|
||||
* Per-slot state. Callback names are stored inline (256 bytes each) to avoid
|
||||
* heap allocation on targets with no malloc or fragmented heaps.
|
||||
*/
|
||||
typedef struct {
|
||||
ElLvglKind kind;
|
||||
lv_obj_t *obj; /* primary LVGL object */
|
||||
lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */
|
||||
char cb_click[256];
|
||||
char cb_change[256];
|
||||
char cb_submit[256];
|
||||
} ElLvglWidget;
|
||||
|
||||
static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS];
|
||||
|
||||
/* ── Slot helpers ─────────────────────────────────────────────────────────── */
|
||||
|
||||
static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) {
|
||||
for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) {
|
||||
if (g_widgets[i].kind == EL_LVGL_FREE) {
|
||||
g_widgets[i].kind = kind;
|
||||
g_widgets[i].obj = obj;
|
||||
g_widgets[i].btn_label = NULL;
|
||||
g_widgets[i].cb_click[0] = '\0';
|
||||
g_widgets[i].cb_change[0] = '\0';
|
||||
g_widgets[i].cb_submit[0] = '\0';
|
||||
return (int64_t)i;
|
||||
}
|
||||
}
|
||||
return -1; /* table full */
|
||||
}
|
||||
|
||||
static ElLvglWidget *lvgl_slot_get(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL;
|
||||
if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL;
|
||||
return &g_widgets[handle];
|
||||
}
|
||||
|
||||
static void lvgl_slot_free(int64_t handle) {
|
||||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return;
|
||||
ElLvglWidget *w = &g_widgets[handle];
|
||||
w->kind = EL_LVGL_FREE;
|
||||
w->obj = NULL;
|
||||
w->btn_label = NULL;
|
||||
w->cb_click[0] = '\0';
|
||||
w->cb_change[0] = '\0';
|
||||
w->cb_submit[0] = '\0';
|
||||
}
|
||||
|
||||
/* ── Font selection ───────────────────────────────────────────────────────── */
|
||||
/*
|
||||
* LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h):
|
||||
* 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
|
||||
*
|
||||
* We map the requested size to the nearest available size. Bold is
|
||||
* approximated by stepping up two sizes (no separate bold face in the default
|
||||
* font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely.
|
||||
*/
|
||||
#ifndef EL_LVGL_CUSTOM_FONT
|
||||
|
||||
static const lv_font_t *font_select(int size, int bold) {
|
||||
/* Step up two sizes for bold approximation. */
|
||||
if (bold) size += 4;
|
||||
|
||||
/* Clamp to available range. */
|
||||
if (size < 8) size = 8;
|
||||
if (size > 48) size = 48;
|
||||
|
||||
/* Round to nearest even size >= 8. */
|
||||
if (size % 2 != 0) size++;
|
||||
|
||||
switch (size) {
|
||||
#if LV_FONT_MONTSERRAT_8
|
||||
case 8: return &lv_font_montserrat_8;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_10
|
||||
case 10: return &lv_font_montserrat_10;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_12
|
||||
case 12: return &lv_font_montserrat_12;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_14
|
||||
case 14: return &lv_font_montserrat_14;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_16
|
||||
case 16: return &lv_font_montserrat_16;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_18
|
||||
case 18: return &lv_font_montserrat_18;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_20
|
||||
case 20: return &lv_font_montserrat_20;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_22
|
||||
case 22: return &lv_font_montserrat_22;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_24
|
||||
case 24: return &lv_font_montserrat_24;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_26
|
||||
case 26: return &lv_font_montserrat_26;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_28
|
||||
case 28: return &lv_font_montserrat_28;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_30
|
||||
case 30: return &lv_font_montserrat_30;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_32
|
||||
case 32: return &lv_font_montserrat_32;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_34
|
||||
case 34: return &lv_font_montserrat_34;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_36
|
||||
case 36: return &lv_font_montserrat_36;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_38
|
||||
case 38: return &lv_font_montserrat_38;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_40
|
||||
case 40: return &lv_font_montserrat_40;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_42
|
||||
case 42: return &lv_font_montserrat_42;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_44
|
||||
case 44: return &lv_font_montserrat_44;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_46
|
||||
case 46: return &lv_font_montserrat_46;
|
||||
#endif
|
||||
#if LV_FONT_MONTSERRAT_48
|
||||
case 48: return &lv_font_montserrat_48;
|
||||
#endif
|
||||
default:
|
||||
/*
|
||||
* Requested size is not compiled in. Fall back to the default
|
||||
* theme font, which is guaranteed to be present.
|
||||
*/
|
||||
return LV_FONT_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* EL_LVGL_CUSTOM_FONT */
|
||||
|
||||
/* ── Event callback ───────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Single LVGL event callback used for all widget events. The user_data is
|
||||
* the slot index cast to (void*) via intptr_t — avoids heap allocation.
|
||||
*
|
||||
* Three event codes are handled:
|
||||
* LV_EVENT_CLICKED → cb_click (buttons, any tappable widget)
|
||||
* LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.)
|
||||
* LV_EVENT_READY → cb_submit (Enter pressed in textarea one-line mode)
|
||||
*/
|
||||
static void el_lvgl_event_cb(lv_event_t *e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
intptr_t slot = (intptr_t)lv_event_get_user_data(e);
|
||||
ElLvglWidget *w = lvgl_slot_get((int64_t)slot);
|
||||
if (!w) return;
|
||||
|
||||
if (code == LV_EVENT_CLICKED && w->cb_click[0]) {
|
||||
EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0);
|
||||
}
|
||||
|
||||
if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) {
|
||||
/*
|
||||
* Retrieve current text for textarea/textfield widgets so the handler
|
||||
* receives the updated value as its second argument.
|
||||
*/
|
||||
const char *txt = "";
|
||||
lv_obj_t *target = lv_event_get_target(e);
|
||||
if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) {
|
||||
txt = lv_textarea_get_text(target);
|
||||
if (!txt) txt = "";
|
||||
}
|
||||
EL_LVGL_CALL(w->cb_change, (el_val_t)slot,
|
||||
(el_val_t)(uintptr_t)txt);
|
||||
}
|
||||
|
||||
if (code == LV_EVENT_READY && w->cb_submit[0]) {
|
||||
/* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */
|
||||
EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Initialisation ───────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_init — call lv_init(). The host must have already initialised the
|
||||
* display driver and input driver before this, or immediately after. Idempotent.
|
||||
*/
|
||||
void el_lvgl_init(void) {
|
||||
static int done = 0;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
lv_init();
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_run_loop — drive lv_task_handler() indefinitely.
|
||||
*
|
||||
* On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task.
|
||||
* On bare metal: call this as the last statement of main().
|
||||
*
|
||||
* The 5 ms delay between handler calls matches the LVGL documentation
|
||||
* recommendation for a ~200 Hz refresh budget.
|
||||
*
|
||||
* On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS
|
||||
* targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc.
|
||||
*/
|
||||
void el_lvgl_run_loop(void) {
|
||||
for (;;) {
|
||||
lv_task_handler();
|
||||
#if defined(EL_LVGL_RTOS_DELAY)
|
||||
EL_LVGL_RTOS_DELAY(5);
|
||||
#elif defined(__linux__) || defined(__APPLE__)
|
||||
{
|
||||
/* Hosted test build — usleep available. */
|
||||
#include <unistd.h>
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
/* Bare-metal without a delay macro: the HAL tick increment loop
|
||||
* is the caller's responsibility. No sleep needed if lv_tick_inc()
|
||||
* is driven from a hardware timer ISR. */
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Window ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_window_create — configure lv_scr_act() as a vertical flex container
|
||||
* and return a slot handle wrapping it. The title is stored for informational
|
||||
* purposes (e.g., a status bar widget the host might create). Width/height
|
||||
* are ignored on embedded targets because the screen size is fixed by the
|
||||
* display driver; they are accepted for API compatibility with other backends.
|
||||
*/
|
||||
int64_t el_lvgl_window_create(const char *title, int width, int height,
|
||||
int min_width, int min_height) {
|
||||
(void)width; (void)height; (void)min_width; (void)min_height;
|
||||
|
||||
lv_obj_t *scr = lv_scr_act();
|
||||
|
||||
/* Configure the active screen as a vertical flex container so that
|
||||
* widgets added via __widget_add_child stack naturally. */
|
||||
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100));
|
||||
|
||||
/* Store the window title in a user-data string on the screen object
|
||||
* so host code can retrieve it if it wants to render a title bar. */
|
||||
if (title && *title) {
|
||||
/* lv_obj_set_user_data stores a void* — we cast the string pointer.
|
||||
* The string must outlive the screen object; for literals this is
|
||||
* always true. For dynamic titles use el_lvgl_window_set_title. */
|
||||
lv_obj_set_user_data(scr, (void *)(uintptr_t)title);
|
||||
}
|
||||
|
||||
/* Allocate a slot for the screen object. */
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr);
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_window_show — no-op on embedded. The screen is always visible.
|
||||
*/
|
||||
void el_lvgl_window_show(int64_t handle) {
|
||||
(void)handle;
|
||||
/* On multi-screen setups, load the screen: */
|
||||
/* ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
* if (w) lv_scr_load(w->obj); */
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_window_set_title — update the user_data pointer on the screen object.
|
||||
* On embedded, "title" is typically only used by a custom host title bar.
|
||||
*/
|
||||
void el_lvgl_window_set_title(int64_t handle, const char *title) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w || w->kind != EL_LVGL_WINDOW) return;
|
||||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : ""));
|
||||
}
|
||||
|
||||
/* ── Layout containers ────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing.
|
||||
*/
|
||||
int64_t el_lvgl_vstack_create(int spacing) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0);
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
/* Remove default LVGL border and background so containers are transparent
|
||||
* by default, matching the AppKit/GTK4 backends. */
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_VSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing.
|
||||
*/
|
||||
int64_t el_lvgl_hstack_create(int spacing) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0);
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_HSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_zstack_create — plain container, children positioned absolutely.
|
||||
* No flex flow is set; callers use lv_obj_set_pos() on children directly,
|
||||
* or rely on their natural 0,0 origin.
|
||||
*/
|
||||
int64_t el_lvgl_zstack_create(void) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_scroll_create — vertically scrollable container.
|
||||
*/
|
||||
int64_t el_lvgl_scroll_create(void) {
|
||||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(obj, 0, 0);
|
||||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||
return lvgl_slot_alloc(EL_LVGL_SCROLL, obj);
|
||||
}
|
||||
|
||||
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_label_create — static text label.
|
||||
*/
|
||||
int64_t el_lvgl_label_create(const char *text) {
|
||||
lv_obj_t *obj = lv_label_create(lv_scr_act());
|
||||
lv_label_set_text(obj, text ? text : "");
|
||||
return lvgl_slot_alloc(EL_LVGL_LABEL, obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_button_create — pressable button with a child label.
|
||||
*
|
||||
* LVGL buttons are containers; text is placed in an inner lv_label child.
|
||||
* We store the child label pointer in btn_label so set_text / get_text can
|
||||
* reach it without searching the object tree at runtime.
|
||||
*/
|
||||
int64_t el_lvgl_button_create(const char *label) {
|
||||
lv_obj_t *btn = lv_btn_create(lv_scr_act());
|
||||
lv_obj_t *lbl = lv_label_create(btn);
|
||||
lv_label_set_text(lbl, label ? label : "");
|
||||
lv_obj_center(lbl);
|
||||
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn);
|
||||
if (h >= 0) {
|
||||
g_widgets[h].btn_label = lbl;
|
||||
/* Register click callback immediately so button responds when a
|
||||
* callback name is registered later via __widget_on_click. */
|
||||
lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_text_field_create — single-line text input.
|
||||
*/
|
||||
int64_t el_lvgl_text_field_create(const char *placeholder) {
|
||||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||||
lv_textarea_set_one_line(obj, true);
|
||||
if (placeholder && *placeholder) {
|
||||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||||
}
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj);
|
||||
if (h >= 0) {
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)h);
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_text_area_create — multi-line text input.
|
||||
*/
|
||||
int64_t el_lvgl_text_area_create(const char *placeholder) {
|
||||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||||
lv_textarea_set_one_line(obj, false);
|
||||
if (placeholder && *placeholder) {
|
||||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||||
}
|
||||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj);
|
||||
if (h >= 0) {
|
||||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)h);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_image_create — image widget.
|
||||
*
|
||||
* On hosted Linux (SDL backend), path is a filesystem path.
|
||||
* On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin".
|
||||
* LVGL image decoders are registered separately by the host application.
|
||||
*/
|
||||
int64_t el_lvgl_image_create(const char *path_or_name) {
|
||||
lv_obj_t *obj = lv_img_create(lv_scr_act());
|
||||
if (path_or_name && *path_or_name) {
|
||||
lv_img_set_src(obj, path_or_name);
|
||||
}
|
||||
return lvgl_slot_alloc(EL_LVGL_IMAGE, obj);
|
||||
}
|
||||
|
||||
/* ── Widget property setters ─────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_text — update visible text.
|
||||
*
|
||||
* Dispatch per kind:
|
||||
* LABEL → lv_label_set_text
|
||||
* BUTTON → lv_label_set_text on inner btn_label child
|
||||
* TEXTFIELD / TEXTAREA → lv_textarea_set_text
|
||||
* WINDOW → lv_obj_set_user_data (stores title string)
|
||||
*/
|
||||
void el_lvgl_widget_set_text(int64_t handle, const char *text) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
const char *t = text ? text : "";
|
||||
switch (w->kind) {
|
||||
case EL_LVGL_LABEL:
|
||||
lv_label_set_text(w->obj, t);
|
||||
break;
|
||||
case EL_LVGL_BUTTON:
|
||||
if (w->btn_label) lv_label_set_text(w->btn_label, t);
|
||||
break;
|
||||
case EL_LVGL_TEXTFIELD:
|
||||
case EL_LVGL_TEXTAREA:
|
||||
lv_textarea_set_text(w->obj, t);
|
||||
break;
|
||||
case EL_LVGL_WINDOW:
|
||||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_get_text — retrieve visible text.
|
||||
*
|
||||
* Returns a pointer into LVGL's internal storage — valid until the next LVGL
|
||||
* operation that modifies the widget. Callers that need to hold the value
|
||||
* across LVGL calls must strdup() it.
|
||||
*/
|
||||
const char *el_lvgl_widget_get_text(int64_t handle) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return "";
|
||||
switch (w->kind) {
|
||||
case EL_LVGL_LABEL:
|
||||
return lv_label_get_text(w->obj);
|
||||
case EL_LVGL_BUTTON:
|
||||
return w->btn_label ? lv_label_get_text(w->btn_label) : "";
|
||||
case EL_LVGL_TEXTFIELD:
|
||||
case EL_LVGL_TEXTAREA:
|
||||
return lv_textarea_get_text(w->obj);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_color — foreground (text) colour.
|
||||
*
|
||||
* r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h).
|
||||
* LVGL lv_color_make takes uint8_t 0–255 components.
|
||||
*/
|
||||
void el_lvgl_widget_set_color(int64_t handle,
|
||||
float r, float g, float b, float a) {
|
||||
(void)a; /* LVGL text colour has no per-glyph alpha channel */
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_color_t c = lv_color_make(
|
||||
(uint8_t)(r * 255.0f + 0.5f),
|
||||
(uint8_t)(g * 255.0f + 0.5f),
|
||||
(uint8_t)(b * 255.0f + 0.5f));
|
||||
lv_obj_set_style_text_color(w->obj, c, 0);
|
||||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||||
lv_obj_set_style_text_color(w->btn_label, c, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_bg_color — background fill colour + opacity.
|
||||
*/
|
||||
void el_lvgl_widget_set_bg_color(int64_t handle,
|
||||
float r, float g, float b, float a) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_color_t c = lv_color_make(
|
||||
(uint8_t)(r * 255.0f + 0.5f),
|
||||
(uint8_t)(g * 255.0f + 0.5f),
|
||||
(uint8_t)(b * 255.0f + 0.5f));
|
||||
lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f);
|
||||
lv_obj_set_style_bg_color(w->obj, c, 0);
|
||||
lv_obj_set_style_bg_opa(w->obj, opa, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_font — apply font to text-bearing widget.
|
||||
*
|
||||
* The `family` parameter is accepted for API compatibility but LVGL uses
|
||||
* compiled-in fonts only. Only the size and bold flag have effect unless
|
||||
* EL_LVGL_CUSTOM_FONT is defined by the host.
|
||||
*/
|
||||
void el_lvgl_widget_set_font(int64_t handle,
|
||||
const char *family, int size, int bold) {
|
||||
(void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
const lv_font_t *font = font_select(size, bold);
|
||||
if (!font) return;
|
||||
lv_obj_set_style_text_font(w->obj, font, 0);
|
||||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||||
lv_obj_set_style_text_font(w->btn_label, font, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left).
|
||||
*/
|
||||
void el_lvgl_widget_set_padding(int64_t handle,
|
||||
int top, int right, int bottom, int left) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0);
|
||||
lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0);
|
||||
lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0);
|
||||
lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_width — set explicit pixel width.
|
||||
*/
|
||||
void el_lvgl_widget_set_width(int64_t handle, int width) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_width(w->obj, (lv_coord_t)width);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_height — set explicit pixel height.
|
||||
*/
|
||||
void el_lvgl_widget_set_height(int64_t handle, int height) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_height(w->obj, (lv_coord_t)height);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_flex — set flex grow factor.
|
||||
*
|
||||
* flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill
|
||||
* remaining space proportional to its grow factor.
|
||||
* flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size.
|
||||
*/
|
||||
void el_lvgl_widget_set_flex(int64_t handle, int flex) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0));
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_corner_radius — set border radius.
|
||||
*/
|
||||
void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_disabled — enable/disable interactive state.
|
||||
*
|
||||
* LV_STATE_DISABLED greys out the widget and prevents input events.
|
||||
*/
|
||||
void el_lvgl_widget_set_disabled(int64_t handle, int disabled) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
if (disabled) {
|
||||
lv_obj_add_state(w->obj, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_clear_state(w->obj, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_set_hidden — show/hide widget.
|
||||
*
|
||||
* LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow.
|
||||
*/
|
||||
void el_lvgl_widget_set_hidden(int64_t handle, int hidden) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
if (hidden) {
|
||||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tree operations ──────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_add_child — attach child widget to parent.
|
||||
*
|
||||
* lv_obj_set_parent() reparents the child object inside the LVGL tree.
|
||||
* For WINDOW parents we use the screen object itself as the parent, since
|
||||
* lv_scr_act() IS the root container.
|
||||
*/
|
||||
void el_lvgl_widget_add_child(int64_t parent, int64_t child) {
|
||||
ElLvglWidget *pw = lvgl_slot_get(parent);
|
||||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||||
if (!pw || !cw) return;
|
||||
lv_obj_set_parent(cw->obj, pw->obj);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_remove_child — detach child from its current parent.
|
||||
*
|
||||
* LVGL has no explicit "remove from parent without deleting" operation.
|
||||
* We reparent the child back to the active screen (making it a root-level
|
||||
* floating widget) and then hide it. The widget still occupies a slot and
|
||||
* can be re-attached or destroyed later.
|
||||
*/
|
||||
void el_lvgl_widget_remove_child(int64_t parent, int64_t child) {
|
||||
(void)parent;
|
||||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||||
if (!cw) return;
|
||||
/* Move to screen root and hide. */
|
||||
lv_obj_set_parent(cw->obj, lv_scr_act());
|
||||
lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
/*
|
||||
* el_lvgl_widget_destroy — delete widget and its children from the LVGL tree,
|
||||
* then free the slot.
|
||||
*
|
||||
* lv_obj_del() recursively deletes the object and all children. After this
|
||||
* call the handle is invalid and must not be used.
|
||||
*/
|
||||
void el_lvgl_widget_destroy(int64_t handle) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
lv_obj_del(w->obj);
|
||||
lvgl_slot_free(handle);
|
||||
}
|
||||
|
||||
/* ── Event registration ───────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Event registration stores the El function name in the widget slot. The
|
||||
* actual lv_obj_add_event_cb() call is made here (or was made in the factory
|
||||
* for buttons/textfields where we know the relevant event codes upfront).
|
||||
*
|
||||
* For widgets that did not register their event callback in the factory (e.g.
|
||||
* labels receiving a click handler), we add the LVGL event binding now.
|
||||
*/
|
||||
|
||||
void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_click, fn_name ? fn_name : "", 255);
|
||||
w->cb_click[255] = '\0';
|
||||
/*
|
||||
* Buttons already have LV_EVENT_CLICKED registered in the factory.
|
||||
* For other widget kinds (labels, containers used as tap targets), add
|
||||
* the click flag and register the callback.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_BUTTON) {
|
||||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_change, fn_name ? fn_name : "", 255);
|
||||
w->cb_change[255] = '\0';
|
||||
/*
|
||||
* Textfield/textarea factories already register VALUE_CHANGED.
|
||||
* For other kinds (e.g. a custom toggle), add the binding now.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) {
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) {
|
||||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||||
if (!w) return;
|
||||
strncpy(w->cb_submit, fn_name ? fn_name : "", 255);
|
||||
w->cb_submit[255] = '\0';
|
||||
/*
|
||||
* LV_EVENT_READY fires when Enter is pressed in a one-line textarea.
|
||||
* Textfield factories already register READY. For other kinds, add it.
|
||||
*/
|
||||
if (w->kind != EL_LVGL_TEXTFIELD) {
|
||||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||||
(void *)(intptr_t)handle);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* EL_TARGET_LVGL */
|
||||
@@ -1,574 +0,0 @@
|
||||
/*
|
||||
* el_native_target.h — Native widget declarations for el programs targeting
|
||||
* native desktop UI (AppKit / GTK4 / Win32).
|
||||
*
|
||||
* This header is designed to be included AFTER el_runtime.h without conflict:
|
||||
* - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h
|
||||
* static inlines.
|
||||
* - It does NOT redeclare __println, __print, or other functions whose
|
||||
* return types differ between el_seed.h and el_runtime.h.
|
||||
* - It adds: native widget builtins + float arithmetic helpers that the
|
||||
* current el_runtime.h omits but elc still emits calls to.
|
||||
*
|
||||
* Usage:
|
||||
* Inject via -include at compile time, OR #include it after el_runtime.h.
|
||||
*
|
||||
* clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ...
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* el_val_t must already be defined by el_runtime.h or el_seed.h. */
|
||||
#ifndef EL_VAL_T_DEFINED
|
||||
typedef int64_t el_val_t;
|
||||
#endif
|
||||
|
||||
/* ── Float arithmetic helpers ───────────────────────────────────────────────
|
||||
* elc emits calls to float_div / float_mul etc. for Float-typed expressions.
|
||||
* These were in el_runtime.c through v1.0.0-20260501 but are missing from the
|
||||
* current el_runtime.h. Redeclared here as static inline to avoid link deps.
|
||||
* Only defined if not already declared (old runtimes that still have them). */
|
||||
|
||||
#ifndef EL_FLOAT_OPS_DEFINED
|
||||
#define EL_FLOAT_OPS_DEFINED
|
||||
|
||||
/* el_to_float / el_from_float — bit-cast between el_val_t and double.
|
||||
* Defined as static inline in both el_runtime.h and el_seed.h; we do NOT
|
||||
* redefine them here. We rely on one of those headers being included first. */
|
||||
|
||||
static inline el_val_t float_div(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b;
|
||||
ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_mul(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d * ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_add(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d + ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_sub(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub, ur;
|
||||
ua.i = a; ub.i = b; ur.d = ua.d - ub.d;
|
||||
return ur.i;
|
||||
}
|
||||
|
||||
static inline el_val_t float_lt(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d < ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_gt(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d > ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_lte(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d <= ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_gte(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d >= ub.d);
|
||||
}
|
||||
|
||||
static inline el_val_t float_eq(el_val_t a, el_val_t b) {
|
||||
union { double d; int64_t i; } ua, ub;
|
||||
ua.i = a; ub.i = b;
|
||||
return (el_val_t)(ua.d == ub.d);
|
||||
}
|
||||
|
||||
#endif /* EL_FLOAT_OPS_DEFINED */
|
||||
|
||||
/* ── Native widget system (macOS AppKit) ────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid. */
|
||||
|
||||
#ifdef EL_TARGET_MACOS
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_set_data(el_val_t handle, el_val_t data_str);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_MACOS */
|
||||
|
||||
/* ── Native widget system (Linux GTK4) ──────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
* All functions have the same signatures as EL_TARGET_MACOS above. */
|
||||
|
||||
#ifdef EL_TARGET_LINUX
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader — same JSON output as EL_TARGET_MACOS */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_LINUX */
|
||||
|
||||
/* ── Native widget system (Windows Win32) ───────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
* Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */
|
||||
|
||||
#ifdef EL_TARGET_WIN32
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_WIN32 */
|
||||
|
||||
/* ── Native widget system (iOS UIKit) ───────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* iOS lifecycle note: UIApplicationMain never returns. The el program must
|
||||
* store its UI-build logic in a void(*)(void) function pointer, assign it to
|
||||
* el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes
|
||||
* el_main_entry_fn inside didFinishLaunchingWithOptions.
|
||||
* Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */
|
||||
|
||||
#ifdef EL_TARGET_IOS
|
||||
|
||||
/* Lifecycle entry-function hook — set before calling __native_run_loop. */
|
||||
extern void (*el_main_entry_fn)(void);
|
||||
|
||||
/* Forward argc/argv from main() to UIApplicationMain. */
|
||||
void el_uikit_set_args(int argc, char** argv);
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_IOS */
|
||||
|
||||
/* ── Native widget system (Android JNI) ─────────────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_ANDROID and linked with
|
||||
* libelruntime.so (which includes el_android.c compiled by the NDK build).
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Java companion: ElBridge.java (package com.neuron.el) must be compiled into
|
||||
* the APK. The Activity must call ElBridge.init(this) before any widget ops.
|
||||
*
|
||||
* Link flags (in Android.mk or CMakeLists.txt):
|
||||
* -landroid -llog -ldl */
|
||||
|
||||
#ifdef EL_TARGET_ANDROID
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void); /* no-op on Android */
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_ANDROID */
|
||||
|
||||
/* ── Native widget system (LVGL v9 — embedded / microcontroller) ─────────────
|
||||
* Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c
|
||||
* and the LVGL library (lvgl.a or lvgl source tree).
|
||||
*
|
||||
* Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+
|
||||
* RAM and an LVGL-compatible display driver. No OS required.
|
||||
*
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Bare-metal / no dynamic linker:
|
||||
* Compile with -DEL_LVGL_NO_DLSYM and provide:
|
||||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||||
*
|
||||
* Compile:
|
||||
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||||
* # Then link with lvgl.a. */
|
||||
|
||||
#ifdef EL_TARGET_LVGL
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader — same JSON output as all other native targets */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_LVGL */
|
||||
|
||||
/* ── Native widget system (SDL2 — embedded / Pi) ────────────────────────────
|
||||
* Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c.
|
||||
* Widget handles are opaque int64_t slot indices; -1 = invalid.
|
||||
*
|
||||
* Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer
|
||||
* and SDL2 available. No GTK, no desktop environment required.
|
||||
*
|
||||
* Compile:
|
||||
* gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o
|
||||
* Link:
|
||||
* $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */
|
||||
|
||||
#ifdef EL_TARGET_SDL2
|
||||
|
||||
/* Initialisation */
|
||||
void __native_init(void);
|
||||
void __native_run_loop(void);
|
||||
|
||||
/* Window */
|
||||
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
||||
el_val_t min_width, el_val_t min_height);
|
||||
void __window_show(el_val_t handle);
|
||||
void __window_set_title(el_val_t handle, el_val_t title);
|
||||
|
||||
/* Layout containers */
|
||||
el_val_t __vstack_create(el_val_t spacing);
|
||||
el_val_t __hstack_create(el_val_t spacing);
|
||||
el_val_t __zstack_create(void);
|
||||
el_val_t __scroll_create(void);
|
||||
|
||||
/* Widgets */
|
||||
el_val_t __label_create(el_val_t text);
|
||||
el_val_t __button_create(el_val_t label);
|
||||
el_val_t __text_field_create(el_val_t placeholder);
|
||||
el_val_t __text_area_create(el_val_t placeholder);
|
||||
el_val_t __image_create(el_val_t path_or_name);
|
||||
|
||||
/* Widget properties */
|
||||
void __widget_set_text(el_val_t handle, el_val_t text);
|
||||
el_val_t __widget_get_text(el_val_t handle);
|
||||
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
||||
el_val_t b, el_val_t a);
|
||||
void __widget_set_font(el_val_t handle, el_val_t family,
|
||||
el_val_t size, el_val_t bold);
|
||||
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
||||
el_val_t bottom, el_val_t left);
|
||||
void __widget_set_width(el_val_t handle, el_val_t width);
|
||||
void __widget_set_height(el_val_t handle, el_val_t height);
|
||||
void __widget_set_flex(el_val_t handle, el_val_t flex);
|
||||
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
|
||||
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
|
||||
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
|
||||
|
||||
/* Layout / tree */
|
||||
void __widget_add_child(el_val_t parent, el_val_t child);
|
||||
void __widget_remove_child(el_val_t parent, el_val_t child);
|
||||
void __widget_destroy(el_val_t handle);
|
||||
|
||||
/* Events */
|
||||
void __widget_on_click(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_change(el_val_t handle, el_val_t fn_name);
|
||||
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
|
||||
|
||||
/* Manifest reader */
|
||||
el_val_t __manifest_read(el_val_t path);
|
||||
|
||||
#endif /* EL_TARGET_SDL2 */
|
||||
@@ -1,118 +0,0 @@
|
||||
#ifndef EL_PLATFORM_WIN_H
|
||||
#define EL_PLATFORM_WIN_H
|
||||
/*
|
||||
* el_platform_win.h — Windows OS-boundary shim for el_runtime.c.
|
||||
*
|
||||
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
|
||||
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
|
||||
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
|
||||
*
|
||||
* What it maps:
|
||||
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
|
||||
* Sockets close with closesocket() (see el_closesocket), and the stack must be
|
||||
* started once with WSAStartup — done automatically via a load-time constructor.
|
||||
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
|
||||
* exported by the main module. Windows equivalent: GetProcAddress on the process
|
||||
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
|
||||
* - popen : mapped to _popen/_pclose.
|
||||
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
|
||||
*/
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <process.h>
|
||||
|
||||
/* Portable headers mingw-w64 provides (verified present). */
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp */
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ── socket close ─────────────────────────────────────────────────────────── */
|
||||
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
|
||||
build defines the same helper as close() so the call sites are identical across platforms. */
|
||||
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
|
||||
|
||||
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
|
||||
static void el__win_net_init(void) {
|
||||
static int inited = 0;
|
||||
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
|
||||
}
|
||||
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
|
||||
|
||||
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
|
||||
#ifndef RTLD_DEFAULT
|
||||
#define RTLD_DEFAULT ((void*)0)
|
||||
#endif
|
||||
static inline void* el_win_dlsym(void* handle, const char* name) {
|
||||
(void)handle;
|
||||
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
|
||||
}
|
||||
#define dlsym(h, n) el_win_dlsym((h), (n))
|
||||
|
||||
/* ── popen / pclose ───────────────────────────────────────────────────────── */
|
||||
#define popen _popen
|
||||
#define pclose _pclose
|
||||
|
||||
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
|
||||
#include <direct.h> /* _mkdir */
|
||||
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
|
||||
#define timegm _mkgmtime /* UTC tm → time_t */
|
||||
#define fsync(fd) _commit(fd) /* no fsync() on Windows; _commit() (<io.h>) is the equiv */
|
||||
|
||||
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
|
||||
static inline int setenv(const char* name, const char* value, int overwrite) {
|
||||
(void)overwrite;
|
||||
return _putenv_s(name, value ? value : "");
|
||||
}
|
||||
static inline int unsetenv(const char* name) {
|
||||
/* _putenv_s(name, "") sets VAR="" rather than removing it.
|
||||
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
|
||||
* env block; then we sync the CRT cache with _putenv("NAME="). */
|
||||
SetEnvironmentVariableA(name, NULL);
|
||||
size_t len = strlen(name);
|
||||
char *buf = (char*)malloc(len + 2);
|
||||
if (!buf) return -1;
|
||||
memcpy(buf, name, len);
|
||||
buf[len] = '=';
|
||||
buf[len + 1] = '\0';
|
||||
_putenv(buf);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
|
||||
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
|
||||
(void)rem;
|
||||
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
|
||||
Sleep(ms ? ms : 1);
|
||||
return 0;
|
||||
}
|
||||
#define nanosleep(req, rem) el_nanosleep((req), (rem))
|
||||
|
||||
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
|
||||
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
|
||||
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||
}
|
||||
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
|
||||
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||
}
|
||||
|
||||
#endif /* EL_PLATFORM_WIN_H */
|
||||
+162
-1425
File diff suppressed because it is too large
Load Diff
@@ -52,12 +52,6 @@
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
|
||||
* register handlers across translation units; previously defined only inside el_runtime.c, which
|
||||
* made cross-module references (and the Windows build) fail. Home in the shared header. */
|
||||
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
|
||||
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
@@ -87,8 +81,8 @@ extern "C" {
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t println(el_val_t s);
|
||||
el_val_t print(el_val_t s);
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
@@ -101,7 +95,6 @@ el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t native_str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
@@ -156,11 +149,10 @@ el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler(el_val_t name);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
@@ -181,9 +173,8 @@ el_val_t http_set_handler(el_val_t name);
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_serve_async(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler_v2(el_val_t name);
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
@@ -234,8 +225,6 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
el_val_t html_escape(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -537,15 +526,9 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t exit_program(el_val_t code);
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||
* during long compilation loops (e.g. after each function is compiled).
|
||||
* Returns 0 when memory is within bounds. */
|
||||
el_val_t el_mem_check(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
@@ -632,7 +615,6 @@ el_val_t engram_load(el_val_t path);
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_get_node_by_label(el_val_t label);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
@@ -646,12 +628,6 @@ el_val_t engram_list_layers_json(void);
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── Working memory ──────────────────────────────────────────────────────────*/
|
||||
el_val_t engram_wm_count(void);
|
||||
el_val_t engram_wm_avg_weight(void);
|
||||
el_val_t engram_wm_top_json(el_val_t n);
|
||||
el_val_t engram_load_merge(el_val_t path);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
@@ -803,95 +779,6 @@ el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
|
||||
* The El self-hosting compiler emits calls to __-prefixed names. These are
|
||||
* forwarding wrappers around the existing el_runtime functions above. */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s);
|
||||
el_val_t __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f);
|
||||
el_val_t __str_to_float(el_val_t s);
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b);
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t __str_alloc(el_val_t n);
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code);
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
el_val_t __fs_list_raw(el_val_t path);
|
||||
|
||||
/* HTTP server */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
|
||||
|
||||
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
|
||||
el_val_t __http_conn_fd(void);
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path);
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_len(el_val_t json);
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
|
||||
el_val_t __json_parse_map(el_val_t json_str);
|
||||
el_val_t __json_stringify_val(el_val_t val);
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
el_val_t __state_set(el_val_t key, el_val_t val);
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user