diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 9baec2d..6e3fb08 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -62,27 +62,6 @@ jobs: echo "=> Full build required" fi - # foundation/el contains the elc compiler that build-stage.sh shells out - # to. We clone el directly with git rather than a second - # actions/checkout call — act_runner v0.6 in host mode hits a - # `permission denied` error on .git/objects/pack/*.idx when running - # two checkout steps in the same job. EL_HOME is pinned outside the - # workspace so the path build-stage.sh expects (../../foundation/el) - # resolves correctly. - - name: Clone el (foundation/el — provides the elc compiler) - if: steps.changetype.outputs.asset_only != 'true' - env: - CHECKOUT_TOKEN: ${{ secrets.CHECKOUT_TOKEN }} - run: | - set -euo pipefail - DEST="${{ github.workspace }}/../foundation-el" - rm -rf "$DEST" - git clone --depth 1 \ - "https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \ - "$DEST" - ls -la "$DEST" | head -5 - echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV" - - name: Authenticate to GCP id: auth uses: google-github-actions/auth@v2 @@ -104,16 +83,6 @@ jobs: - name: Configure docker auth for Artifact Registry run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - - name: Get elc (pre-built linux/amd64 from El SDK release) - if: steps.changetype.outputs.asset_only != 'true' - run: | - set -euo pipefail - curl -fL -o "$EL_HOME/dist/platform/elc" \ - https://git.neuralplatform.ai/neuron-technologies/el/releases/download/v1.2.1/elc-linux-amd64 - chmod +x "$EL_HOME/dist/platform/elc" - file "$EL_HOME/dist/platform/elc" - ls -la "$EL_HOME/dist/platform/elc" - - name: Compute image tag id: tag run: | @@ -129,12 +98,60 @@ jobs: # in the build context for Dockerfile COPY to succeed. run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html - - name: Build image (build-stage.sh) + # ── El SDK setup ────────────────────────────────────────────────────── + + - name: Extract El SDK from ci-base if: steps.changetype.outputs.asset_only != 'true' - env: - EXTRACT_JS: '1' run: | - ./build-stage.sh "${{ steps.tag.outputs.tag }}" + set -euo pipefail + docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest + CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest) + sudo mkdir -p /opt/el + docker cp "$CID:/opt/el" /opt/ + docker rm "$CID" + echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV" + echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV" + echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV" + + # ── Build neuron-web binary ─────────────────────────────────────────── + + - name: Build neuron-web with elb + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + "$ELB" \ + --elc="$ELC" \ + --runtime="$EL_RUNTIME" + echo "Binary: $(ls -lh dist/neuron-landing)" + + # ── Compile JS client sources ───────────────────────────────────────── + + - name: Compile JS El sources + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + cp "$EL_RUNTIME/el_runtime.js" src/js/ + mkdir -p dist/js + for f in src/js/*.el; do + [ -f "$f" ] || continue + name=$(basename "$f" .el) + "$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js" + echo " compiled: $f -> dist/js/${name}.js" + done + rm -f src/js/el_runtime.js + + # ── Docker build + push ─────────────────────────────────────────────── + + - name: Build and tag image + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + docker build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \ + -f Dockerfile.stage \ + -t "marketing:${{ steps.tag.outputs.tag }}" \ + . docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}" docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest" diff --git a/.gitea/workflows/dev.yaml b/.gitea/workflows/dev.yaml index 234e596..6d56911 100644 --- a/.gitea/workflows/dev.yaml +++ b/.gitea/workflows/dev.yaml @@ -3,6 +3,15 @@ name: Dev — Build & local smoke test # Validates that the build compiles and the server starts cleanly. # No GCP deployment — this is the inner dev loop gate. # Merge to stage when you want a real environment. +# +# Build approach: pull ci-base from Artifact Registry (has elb + elc + runtime +# at /opt/el), extract the SDK onto the runner host, then run elb build. +# elb compiles each .el source independently — no combined mega-file, no OOM. +# Output: dist/neuron-landing (linux/amd64). Dockerfile.stage COPYs it directly. +# +# For pull_request events: secrets are not injected, so ci-base can't be pulled. +# Fall back to committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/. +# No docker cache (no Artifact Registry auth), but the full build + smoke test runs. on: push: @@ -11,8 +20,20 @@ on: - 'src/**' - 'dist/**' - 'runtime/**' + - 'manifest.el' + - 'Dockerfile.stage' + - '.gitea/workflows/dev.yaml' + - '.gitea/workflows/stage.yaml' + - '.gitea/workflows/deploy.yaml' + + pull_request: + branches: [dev] + paths: + - 'src/**' + - 'dist/**' + - 'runtime/**' + - 'manifest.el' - 'Dockerfile.stage' - - 'build-stage.sh' - '.gitea/workflows/dev.yaml' - '.gitea/workflows/stage.yaml' - '.gitea/workflows/deploy.yaml' @@ -34,56 +55,119 @@ jobs: with: fetch-depth: 2 - - name: Clone el (provides elc compiler) - env: - CHECKOUT_TOKEN: ${{ secrets.CHECKOUT_TOKEN }} - run: | - set -euo pipefail - DEST="${{ github.workspace }}/../foundation-el" - rm -rf "$DEST" - git clone --depth 1 \ - "https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \ - "$DEST" - echo "EL_HOME=$DEST" >> "$GITHUB_ENV" + # ── GCP auth (push/workflow_dispatch only) ──────────────────────────── + # pull_request events don't get secrets injected. GCP auth is skipped + # for PRs — El SDK comes from committed bin/ + runtime/ instead. - name: Authenticate to GCP + if: github.event_name != 'pull_request' uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - name: Set up gcloud SDK + if: github.event_name != 'pull_request' uses: google-github-actions/setup-gcloud@v2 with: project_id: neuron-785695 - name: Configure docker auth for Artifact Registry + if: github.event_name != 'pull_request' run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - - name: Get elc (pre-built linux/amd64 from El repo) + # ── El SDK setup ────────────────────────────────────────────────────── + # Push builds: extract elb + elc + runtime from ci-base (always latest). + # PR builds: use committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/. + + - name: Extract El SDK from ci-base (push builds) + if: github.event_name != 'pull_request' run: | set -euo pipefail - ELC_SRC="$EL_HOME/dist/platform/elc-linux-amd64" - if [ -f "$ELC_SRC" ]; then - cp "$ELC_SRC" "$EL_HOME/dist/platform/elc" - chmod +x "$EL_HOME/dist/platform/elc" - else - curl -fL -o "$EL_HOME/dist/platform/elc" \ - https://git.neuralplatform.ai/neuron-technologies/el/releases/download/v1.2.1/elc-linux-amd64 - chmod +x "$EL_HOME/dist/platform/elc" + docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev + CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev) + sudo mkdir -p /opt/el + docker cp "$CID:/opt/el" /opt/ + docker rm "$CID" + echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV" + echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV" + echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV" + + - name: Set up El SDK from committed bin/ (PR builds) + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + DEST="${{ github.workspace }}/../foundation-el" + mkdir -p "$DEST/dist/bin" "$DEST/dist/platform" "$DEST/el-compiler/runtime" + cp bin/elb-linux-amd64 "$DEST/dist/bin/elb" + cp bin/elc-linux-amd64 "$DEST/dist/platform/elc" + chmod +x "$DEST/dist/bin/elb" "$DEST/dist/platform/elc" + cp runtime/el_runtime.c "$DEST/el-compiler/runtime/" + cp runtime/el_runtime.h "$DEST/el-compiler/runtime/" + cp runtime/el_runtime.js "$DEST/el-compiler/runtime/" + echo "ELB=$DEST/dist/bin/elb" >> "$GITHUB_ENV" + echo "ELC=$DEST/dist/platform/elc" >> "$GITHUB_ENV" + echo "EL_RUNTIME=$DEST/el-compiler/runtime" >> "$GITHUB_ENV" + + # ── Build neuron-web binary ─────────────────────────────────────────── + + - name: Build neuron-web with elb + run: | + set -uo pipefail + echo "ELB=$ELB ELC=$ELC EL_RUNTIME=$EL_RUNTIME" + ls -la "$ELB" "$ELC" + stdbuf -oL "$ELB" \ + --elc="$ELC" \ + --runtime="$EL_RUNTIME" 2>&1 | tee /tmp/elb.log + ELB_EXIT=${PIPESTATUS[0]} + if [ "$ELB_EXIT" -eq 0 ]; then + echo "Binary: $(ls -lh dist/neuron-landing)" fi + exit "$ELB_EXIT" + + - name: Dump full elb output (on failure) + if: failure() + run: | + echo "=== full elb output ===" + cat /tmp/elb.log || echo "(no log file)" + + # ── Compile JS client sources ───────────────────────────────────────── + + - name: Compile JS El sources + run: | + set -euo pipefail + cp "$EL_RUNTIME/el_runtime.js" src/js/ + mkdir -p dist/js + for f in src/js/*.el; do + [ -f "$f" ] || continue + name=$(basename "$f" .el) + "$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js" + echo " compiled: $f -> dist/js/${name}.js" + done + rm -f src/js/el_runtime.js + + # ── Docker build + smoke test ───────────────────────────────────────── - name: Compute image tag id: tag run: echo "tag=dev-${GITHUB_SHA:0:8}" >> "$GITHUB_OUTPUT" - name: Touch HTML placeholder files - # El binary regenerates these at startup via fs_write. They must exist - # in the build context for Dockerfile COPY to succeed. touch is - # idempotent if the files already exist from a prior run. run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html - - name: Build image (local only — no push) - run: ./build-stage.sh "${{ steps.tag.outputs.tag }}" + - name: Build Docker image (local only — no push) + run: | + set -euo pipefail + TAG="${{ steps.tag.outputs.tag }}" + CACHE_ARGS="" + if [ "${{ github.event_name }}" != "pull_request" ]; then + CACHE_ARGS="--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest" + fi + docker build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + $CACHE_ARGS \ + -f Dockerfile.stage \ + -t "marketing:${TAG}" \ + . - name: Local smoke test run: | @@ -97,8 +181,6 @@ jobs: -e LANDING_ROOT=/srv/landing \ "$IMAGE" - # entrypoint.sh sleeps 4s for soul-demo to load before starting neuron-web. - # Poll up to 45s total. for i in $(seq 1 15); do STATUS=$(curl -sSo /dev/null -w "%{http_code}" --max-time 5 http://localhost:8080/ || echo "000") echo "Attempt $i/15: HTTP $STATUS" diff --git a/.gitea/workflows/stage.yaml b/.gitea/workflows/stage.yaml index 4dad9cc..80b636f 100644 --- a/.gitea/workflows/stage.yaml +++ b/.gitea/workflows/stage.yaml @@ -33,19 +33,23 @@ jobs: steps: - name: Enforce dev-only source - # stage branch only accepts merges from dev. A direct push from any - # other branch fails here so the rest of the pipeline never runs. + # stage only accepts merges from dev. Any PR from another branch fails + # here before a single build step runs. # workflow_dispatch is exempt (allows manual redeploy of current stage). if: github.event_name != 'workflow_dispatch' run: | - BASE=$(git -C "$GITHUB_WORKSPACE" log --pretty=format:"%D" -1 2>/dev/null || true) - # On a merge-to-stage push the parent is the tip of dev. - # We check the merge commit parents: if the non-stage parent is not - # from dev, reject. For direct pushes (no merge commit) the - # committer origin cannot be verified here — branch protection - # (enable_push=false) blocks direct non-admin pushes before CI runs. - echo "Event: ${{ github.event_name }}, ref: ${{ github.ref }}" - echo "Source branch enforcement: OK (protected by Gitea branch rules)" + set -euo pipefail + COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || true) + echo "Merge commit: $COMMIT_MSG" + # Gitea merge commits: "Merge pull request '...' (#N) from dev into stage" + # Direct branch merges: "Merge branch 'dev' into stage" + if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$"; then + echo "Source branch check: OK (merged from dev)" + else + echo "ERROR: stage only accepts merges from dev." + echo "Commit message was: $COMMIT_MSG" + exit 1 + fi - name: Checkout uses: actions/checkout@v4 @@ -68,19 +72,6 @@ jobs: echo "=> Full build required" fi - - name: Clone el (provides elc compiler) - if: steps.changetype.outputs.asset_only != 'true' - env: - CHECKOUT_TOKEN: ${{ secrets.CHECKOUT_TOKEN }} - run: | - set -euo pipefail - DEST="${{ github.workspace }}/../foundation-el" - rm -rf "$DEST" - git clone --depth 1 \ - "https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \ - "$DEST" - echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV" - - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: @@ -94,24 +85,6 @@ jobs: - name: Configure docker auth for Artifact Registry run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - - name: Get elc (pre-built linux/amd64 from El repo) - if: steps.changetype.outputs.asset_only != 'true' - run: | - set -euo pipefail - # Copy the El C-compiler binary from the cloned El repo into the expected path. - # The JS-capable elc for client-side compilation is committed in bin/elc-linux-amd64 - # and used automatically by build-stage.sh on linux/amd64. - ELC_SRC="$EL_HOME/dist/platform/elc-linux-amd64" - if [ -f "$ELC_SRC" ]; then - cp "$ELC_SRC" "$EL_HOME/dist/platform/elc" - chmod +x "$EL_HOME/dist/platform/elc" - else - # Fallback: download v1.2.1 C-compiler if the repo binary is absent - curl -fL -o "$EL_HOME/dist/platform/elc" \ - https://git.neuralplatform.ai/neuron-technologies/el/releases/download/v1.2.1/elc-linux-amd64 - chmod +x "$EL_HOME/dist/platform/elc" - fi - - name: Compute image tag id: tag run: | @@ -127,10 +100,60 @@ jobs: # in the build context for Dockerfile COPY to succeed. run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html - - name: Build image (build-stage.sh) + # ── El SDK setup ────────────────────────────────────────────────────── + + - name: Extract El SDK from ci-base if: steps.changetype.outputs.asset_only != 'true' run: | - ./build-stage.sh "${{ steps.tag.outputs.tag }}" + set -euo pipefail + docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage + CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage) + sudo mkdir -p /opt/el + docker cp "$CID:/opt/el" /opt/ + docker rm "$CID" + echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV" + echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV" + echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV" + + # ── Build neuron-web binary ─────────────────────────────────────────── + + - name: Build neuron-web with elb + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + "$ELB" \ + --elc="$ELC" \ + --runtime="$EL_RUNTIME" + echo "Binary: $(ls -lh dist/neuron-landing)" + + # ── Compile JS client sources ───────────────────────────────────────── + + - name: Compile JS El sources + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + cp "$EL_RUNTIME/el_runtime.js" src/js/ + mkdir -p dist/js + for f in src/js/*.el; do + [ -f "$f" ] || continue + name=$(basename "$f" .el) + "$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js" + echo " compiled: $f -> dist/js/${name}.js" + done + rm -f src/js/el_runtime.js + + # ── Docker build + push ─────────────────────────────────────────────── + + - name: Build and tag image + if: steps.changetype.outputs.asset_only != 'true' + run: | + set -euo pipefail + docker build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest \ + -f Dockerfile.stage \ + -t "marketing:${{ steps.tag.outputs.tag }}" \ + . docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}" docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest" diff --git a/.gitignore b/.gitignore index e00e6d4..4b6c875 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,7 @@ src/assets/js/ !dist/web_stubs.c !dist/vessel_stubs.c !dist/soul-demo.c +!dist/page_close.c +!dist/elhtml_impl.c !dist/entrypoint.sh !dist/engram-snapshot.json diff --git a/Dockerfile.stage b/Dockerfile.stage index d81509e..d99a1ca 100644 --- a/Dockerfile.stage +++ b/Dockerfile.stage @@ -4,12 +4,14 @@ # - neuron-web on port 8080 (landing page server) # - soul-demo on port 7772 (demo chat, localhost only) # -# bootstrap.py is no longer in the build path. The host-side build-stage.sh -# pre-compiles src/*.el → dist/main.c using the canonical native elc and -# applies the stub forward-declaration sed before this Dockerfile runs. -# The image just compiles the finished C source. +# neuron-web is built by `elb build` in CI (not here). elb compiles each +# .el source independently and links the result — no combined mega-file, +# no exponential memory growth. The binary lands at dist/neuron-landing +# (linux/amd64) and is COPY'd directly into the runtime image. +# +# soul-demo.c is pre-committed (small, no OOM risk) and compiled here. -# ── Stage 1: compile both binaries ──────────────────────────────────────────── +# ── Stage 1: compile soul-demo ──────────────────────────────────────────────── FROM debian:bookworm-slim AS builder RUN apt-get update \ @@ -22,7 +24,6 @@ RUN apt-get update \ WORKDIR /build -# El runtime (shared by both binaries) COPY runtime/el_runtime.c runtime/el_runtime.h ./ # Pre-compile el_runtime as a separate cached layer. @@ -33,22 +34,7 @@ COPY runtime/el_runtime.c runtime/el_runtime.h ./ # libcurl IS installed above, so define HAVE_CURL to enable those functions. RUN cc -O2 -DHAVE_CURL -c el_runtime.c -I. -o el_runtime.o -# ── Build neuron-web ────────────────────────────────────────────────────────── -# -# main.c was generated on the host by build-stage.sh from src/*.el via the -# native elc compiler. Stub forward-declarations were already injected on -# the host side, so this stage is a straight cc invocation. -COPY dist/web_stubs.c ./ -COPY dist/main.c ./ - -RUN cc -O2 -rdynamic \ - -o neuron-web \ - main.c web_stubs.c el_runtime.o \ - -lcurl -lpthread -ldl -lm -lssl -lcrypto - -# ── Build soul-demo ─────────────────────────────────────────────────────────── -COPY dist/soul-demo.c ./ -COPY dist/vessel_stubs.c ./ +COPY dist/soul-demo.c dist/vessel_stubs.c ./ RUN cc -O2 -rdynamic \ -o soul-demo \ @@ -69,8 +55,11 @@ RUN apt-get update \ && mkdir -p /srv/soul/engram-demo \ && chown -R landing:landing /srv/landing /srv/soul -COPY --from=builder /build/neuron-web /usr/local/bin/neuron-web -COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo +# neuron-web binary — produced by `elb build` in CI (linux/amd64) +COPY dist/neuron-landing /usr/local/bin/neuron-web +RUN chmod +x /usr/local/bin/neuron-web + +COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo # Engram snapshot — baked in so soul has memory from cold start COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json diff --git a/bin/elb-linux-amd64 b/bin/elb-linux-amd64 new file mode 100755 index 0000000..cafbdd1 Binary files /dev/null and b/bin/elb-linux-amd64 differ diff --git a/bin/elc-linux-amd64 b/bin/elc-linux-amd64 index b2efc54..97e0f40 100755 Binary files a/bin/elc-linux-amd64 and b/bin/elc-linux-amd64 differ diff --git a/build-stage.sh b/build-stage.sh index c22f76e..af55074 100755 --- a/build-stage.sh +++ b/build-stage.sh @@ -2,111 +2,40 @@ # # build-stage.sh — Build the Stage marketing image (neuron-web + soul-demo). # -# Pipeline: -# 1. Stage the foundation El runtime into ./runtime/. -# 2. Compile client-side El sources (src/js/*.el) to dist/js/*.js using -# the JS-capable elc binary at bin/elc-linux-amd64 (CI) or the local -# elc (dev). Output is gitignored and rebuilt every run. -# 3. Concatenate src/*.el into dist/main-combined.el (component-first, -# main.el last; matches the historical order from build-local.sh). -# 4. Compile dist/main-combined.el → dist/main.c using the canonical -# native elc at foundation/el/dist/platform/elc. -# 5. Inject the host-side stub forward declarations into dist/main.c -# (sed header rewrite, same set as the prior in-Dockerfile sed). -# 6. docker buildx build --platform linux/amd64 -f Dockerfile.stage. -# -# bootstrap.py is no longer in the build path. The container image now -# expects dist/main.c to be a finished C source — it just runs cc on it. +# Thin wrapper around elb. The El build system handles compilation. +# ELB, ELC, and EL_RUNTIME must be set by the caller (extracted from ci-base +# in CI, or pointed at the local El SDK in dev). # # Usage: -# ./build-stage.sh — build marketing: +# ELB=/opt/el/dist/bin/elb ELC=... EL_RUNTIME=... ./build-stage.sh set -euo pipefail cd "$(dirname "$0")" TAG="${1:-dev}" -LANDING_DIR=$(pwd) -EL_HOME="${EL_HOME:-${LANDING_DIR}/../../foundation/el}" -ELC="${EL_HOME}/dist/platform/elc" -RUNTIME_SRC="${EL_HOME}/el-compiler/runtime" - -# JS-capable elc: prefer committed bin/elc-linux-amd64 on CI (linux/amd64), -# fall back to the local elc from the El checkout on macOS dev. -if [ -f "${LANDING_DIR}/bin/elc-linux-amd64" ] && uname -m | grep -q x86_64; then - ELC_JS="${LANDING_DIR}/bin/elc-linux-amd64" -elif [ -x "${ELC}" ]; then - ELC_JS="${ELC}" -else - echo "elc for JS compilation not found — expected bin/elc-linux-amd64 or ${ELC}" >&2 +if [ -z "${ELB:-}" ] || [ -z "${ELC:-}" ] || [ -z "${EL_RUNTIME:-}" ]; then + echo "Error: ELB, ELC, and EL_RUNTIME must be set" >&2 + echo " Extract from ci-base or point to local El SDK at foundation/el" >&2 exit 1 fi -if [ ! -x "${ELC}" ]; then - echo "elc not found at ${ELC}" >&2 - exit 1 -fi - -echo "==> Staging El runtime from ${RUNTIME_SRC}" -mkdir -p runtime dist -cp "${RUNTIME_SRC}/el_runtime.c" runtime/ -cp "${RUNTIME_SRC}/el_runtime.h" runtime/ - -# The JS compiler looks for el_runtime.js in the same directory as the -# source file being compiled. Copy it there so --bundle can inline it. -cp "${RUNTIME_SRC}/el_runtime.js" "${LANDING_DIR}/src/js/" +echo "==> Building neuron-web with elb" +"$ELB" build --elc "$ELC" --runtime "$EL_RUNTIME" +echo " Binary: $(ls -lh dist/neuron-landing)" echo "==> Compiling client-side El (src/js/*.el) → dist/js/" +cp "$EL_RUNTIME/el_runtime.js" src/js/ mkdir -p dist/js -for f in "${LANDING_DIR}/src/js/"*.el; do +for f in src/js/*.el; do + [ -f "$f" ] || continue name=$(basename "$f" .el) - "${ELC_JS}" --target=js --bundle --minify --obfuscate "$f" > "${LANDING_DIR}/dist/js/${name}.js" + "$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js" echo " compiled: src/js/${name}.el → dist/js/${name}.js" done - -# Clean up the staged runtime (not a source file) -rm -f "${LANDING_DIR}/src/js/el_runtime.js" - -echo "==> Combining El sources → dist/main-combined.el" -COMPONENTS=(nav hero pillars how_it_works inference efficiency comparison - environmental enterprise mission local_first pricing marketplace viral - footer styles about founding_badge terms enterprise_terms checkout safety - gallery account) -{ - for f in "${COMPONENTS[@]}"; do - if [ -f "src/${f}.el" ]; then - grep -hv '^[[:space:]]*from\|^[[:space:]]*import' "src/${f}.el" - echo "" - fi - done - grep -v '^from\|^import' src/main.el -} > dist/main-combined.el -echo " $(wc -l < dist/main-combined.el) lines" - -echo "==> Compiling dist/main-combined.el → dist/main.c via ${ELC}" -"${ELC}" dist/main-combined.el > dist/main.c -echo " $(wc -l < dist/main.c) lines of C" - -echo "==> Injecting host-side stub forward declarations" -# GNU vs BSD sed: -i with no arg works on GNU, breaks on macOS BSD sed -# (BSD requires -i ''). Detect and branch. -SED_INPLACE=(-i) -if sed --version >/dev/null 2>&1; then - SED_INPLACE=(-i) -else - SED_INPLACE=(-i '') -fi -sed "${SED_INPLACE[@]}" \ - 's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);\nel_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json);\nel_val_t supabase_upsert_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt, el_val_t table_and_query, el_val_t row_json);|' \ - dist/main.c +rm -f src/js/el_runtime.js echo "==> Building Docker image marketing:${TAG}" -# Plain `docker build` — the gitea runner doesn't ship buildx, so -# `docker buildx build --platform ...` exits 125 ("unknown flag: -# --platform"). The runner host is already linux/amd64 so the -# explicit --platform is redundant. BUILDKIT_INLINE_CACHE works with -# plain docker as long as DOCKER_BUILDKIT=1 is set (default on the -# runner). docker build \ --build-arg BUILDKIT_INLINE_CACHE=1 \ --cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \ diff --git a/dist/elhtml_impl.c b/dist/elhtml_impl.c new file mode 100644 index 0000000..a6ae454 --- /dev/null +++ b/dist/elhtml_impl.c @@ -0,0 +1,341 @@ +#include +#include +#include "el_runtime.h" + +el_val_t el_escape(el_val_t s); +el_val_t el_text(el_val_t s); +el_val_t el_attr(el_val_t name, el_val_t value); +el_val_t el_div(el_val_t attrs, el_val_t children); +el_val_t el_section(el_val_t attrs, el_val_t children); +el_val_t el_article(el_val_t attrs, el_val_t children); +el_val_t el_header(el_val_t attrs, el_val_t children); +el_val_t el_footer(el_val_t attrs, el_val_t children); +el_val_t el_main(el_val_t attrs, el_val_t children); +el_val_t el_nav(el_val_t attrs, el_val_t children); +el_val_t el_aside(el_val_t attrs, el_val_t children); +el_val_t el_ul(el_val_t attrs, el_val_t children); +el_val_t el_ol(el_val_t attrs, el_val_t children); +el_val_t el_li(el_val_t attrs, el_val_t children); +el_val_t el_p(el_val_t attrs, el_val_t children); +el_val_t el_span(el_val_t attrs, el_val_t children); +el_val_t el_form(el_val_t attrs, el_val_t children); +el_val_t el_h1(el_val_t attrs, el_val_t text); +el_val_t el_h2(el_val_t attrs, el_val_t text); +el_val_t el_h3(el_val_t attrs, el_val_t text); +el_val_t el_h4(el_val_t attrs, el_val_t text); +el_val_t el_button(el_val_t attrs, el_val_t label); +el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children); +el_val_t el_input(el_val_t type_attr, el_val_t attrs); +el_val_t el_textarea(el_val_t attrs, el_val_t value); +el_val_t el_label(el_val_t for_id, el_val_t attrs, el_val_t children); +el_val_t el_img(el_val_t src, el_val_t alt, el_val_t attrs); +el_val_t el_video(el_val_t attrs, el_val_t children); +el_val_t el_strong(el_val_t children); +el_val_t el_em(el_val_t children); +el_val_t el_code(el_val_t children); +el_val_t el_pre(el_val_t attrs, el_val_t children); +el_val_t el_hr(void); +el_val_t el_br(void); +el_val_t el_html_doc(el_val_t lang, el_val_t head_html, el_val_t body_html); +el_val_t el_meta(el_val_t name, el_val_t content); +el_val_t el_meta_charset(el_val_t charset); +el_val_t el_link_stylesheet(el_val_t href); +el_val_t el_script_src(el_val_t src, el_val_t defer_load); +el_val_t el_script_inline(el_val_t js); +el_val_t el_title(el_val_t text); + + +el_val_t el_escape(el_val_t s) { + s = str_replace(s, EL_STR("&"), EL_STR("&")); + s = str_replace(s, EL_STR("<"), EL_STR("<")); + s = str_replace(s, EL_STR(">"), EL_STR(">")); + s = str_replace(s, EL_STR("\""), EL_STR(""")); + return str_replace(s, EL_STR("'"), EL_STR("'")); + return 0; +} + +el_val_t el_text(el_val_t s) { + return el_escape(s); + return 0; +} + +el_val_t el_attr(el_val_t name, el_val_t value) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" "), name), EL_STR("=\"")), el_escape(value)), EL_STR("\"")); + return 0; +} + +el_val_t el_div(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_section(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_article(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_header(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_footer(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_main(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
"), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
")), children), EL_STR("
")); + return 0; +} + +el_val_t el_nav(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_aside(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_ul(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
    "), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
    ")), children), EL_STR("
")); + return 0; +} + +el_val_t el_ol(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
    "), children), EL_STR("
")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
    ")), children), EL_STR("
")); + return 0; +} + +el_val_t el_li(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
  • "), children), EL_STR("
  • ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
  • ")), children), EL_STR("
  • ")); + return 0; +} + +el_val_t el_p(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("

    "), children), EL_STR("

    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("

    ")), children), EL_STR("

    ")); + return 0; +} + +el_val_t el_span(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR(""), children), EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")), children), EL_STR("")); + return 0; +} + +el_val_t el_form(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
    "), children), EL_STR("
    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
    ")), children), EL_STR("
    ")); + return 0; +} + +el_val_t el_h1(el_val_t attrs, el_val_t text) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("

    "), el_escape(text)), EL_STR("

    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("

    ")), el_escape(text)), EL_STR("

    ")); + return 0; +} + +el_val_t el_h2(el_val_t attrs, el_val_t text) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("

    "), el_escape(text)), EL_STR("

    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("

    ")), el_escape(text)), EL_STR("

    ")); + return 0; +} + +el_val_t el_h3(el_val_t attrs, el_val_t text) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("

    "), el_escape(text)), EL_STR("

    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("

    ")), el_escape(text)), EL_STR("

    ")); + return 0; +} + +el_val_t el_h4(el_val_t attrs, el_val_t text) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("

    "), el_escape(text)), EL_STR("

    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("

    ")), el_escape(text)), EL_STR("

    ")); + return 0; +} + +el_val_t el_button(el_val_t attrs, el_val_t label) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children) { + el_val_t h = el_str_concat(el_str_concat(EL_STR("href=\""), el_escape(href)), EL_STR("\"")); + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")), children), EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")), children), EL_STR("")); + return 0; +} + +el_val_t el_input(el_val_t type_attr, el_val_t attrs) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_textarea(el_val_t attrs, el_val_t value) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_label(el_val_t for_id, el_val_t attrs, el_val_t children) { + el_val_t f = el_str_concat(el_str_concat(EL_STR("for=\""), el_escape(for_id)), EL_STR("\"")); + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_img(el_val_t src, el_val_t alt, el_val_t attrs) { + el_val_t base = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("src=\""), el_escape(src)), EL_STR("\" alt=\"")), el_escape(alt)), EL_STR("\"")); + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_video(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_strong(el_val_t children) { + return el_str_concat(el_str_concat(EL_STR(""), children), EL_STR("")); + return 0; +} + +el_val_t el_em(el_val_t children) { + return el_str_concat(el_str_concat(EL_STR(""), children), EL_STR("")); + return 0; +} + +el_val_t el_code(el_val_t children) { + return el_str_concat(el_str_concat(EL_STR(""), children), EL_STR("")); + return 0; +} + +el_val_t el_pre(el_val_t attrs, el_val_t children) { + if (str_eq(attrs, EL_STR(""))) { + return el_str_concat(el_str_concat(EL_STR("
    "), children), EL_STR("
    ")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("
    ")), children), EL_STR("
    ")); + return 0; +} + +el_val_t el_hr(void) { + return EL_STR("
    "); + return 0; +} + +el_val_t el_br(void) { + return EL_STR("
    "); + return 0; +} + +el_val_t el_html_doc(el_val_t lang, el_val_t head_html, el_val_t body_html) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")), head_html), EL_STR("")), body_html), EL_STR("")); + return 0; +} + +el_val_t el_meta(el_val_t name, el_val_t content) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_meta_charset(el_val_t charset) { + return el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_link_stylesheet(el_val_t href) { + return el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_script_src(el_val_t src, el_val_t defer_load) { + if (defer_load) { + return el_str_concat(el_str_concat(EL_STR("")); + } + return el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_script_inline(el_val_t js) { + return el_str_concat(el_str_concat(EL_STR("")); + return 0; +} + +el_val_t el_title(el_val_t text) { + return el_str_concat(el_str_concat(EL_STR(""), el_escape(text)), EL_STR("")); + return 0; +} + + diff --git a/dist/page_close.c b/dist/page_close.c new file mode 100644 index 0000000..0bbf0b6 --- /dev/null +++ b/dist/page_close.c @@ -0,0 +1,12 @@ +#include +#include +#include "el_runtime.h" + +el_val_t page_close(void); + +el_val_t page_close(void) { + el_val_t widgets = ({ el_val_t _html_1 = EL_STR(""); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("TryNeuron")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Neuron")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("LiveDemo")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Send")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Preview")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Thisiswhatyouareabouttopublish")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("times;")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Cancel")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("Publishtogallery")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1 = el_str_concat(_html_1, EL_STR("")); _html_1; }); + return el_str_concat(widgets, EL_STR("")); + return 0; +} + diff --git a/dist/soul-demo.c b/dist/soul-demo.c index 3d757f5..e54caeb 100644 --- a/dist/soul-demo.c +++ b/dist/soul-demo.c @@ -703,7 +703,7 @@ el_val_t handle_chat(el_val_t body) { el_val_t return_line = ({ el_val_t _if_result_42 = 0; if (is_return) { _if_result_42 = (EL_STR("This person has opened the chat before \xe2\x80\x94 acknowledge that warmly without making a big deal of it.")); } else { _if_result_42 = (EL_STR("This is the first time this person is meeting you.")); } _if_result_42; }); el_val_t time_line = ({ el_val_t _if_result_43 = 0; if (str_eq(time_of_day, EL_STR(""))) { _if_result_43 = (EL_STR("")); } else { _if_result_43 = (el_str_concat(el_str_concat(EL_STR(" It is "), time_of_day), EL_STR(" for them."))); } _if_result_43; }); el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; }); - el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- It's launch night. Today is the first day Neuron is alive in public. If that wants to come through, let it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak.")); + el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak.")); el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me.")); el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\")); el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\"")); diff --git a/dist/web_stubs.c b/dist/web_stubs.c index fb6bbc0..a5f70ec 100644 --- a/dist/web_stubs.c +++ b/dist/web_stubs.c @@ -325,3 +325,9 @@ el_val_t gcs_read(el_val_t bucket, el_val_t object_name) { if (!resp) return EL_STR(""); return resp; } + +/* Browser JS interop stubs — server-side no-ops for checkout.el browser globals. + * checkout.el contains JS inline: window.neuronCheckoutFree&&window.neuronCheckoutFree() + * which the El HTML parser exposes as C identifiers in the generated checkout.c. */ +el_val_t window = 0; +el_val_t neuronCheckoutFree(el_val_t v) { (void)v; return 0; } diff --git a/manifest.el b/manifest.el index bab13b2..a8db0d9 100644 --- a/manifest.el +++ b/manifest.el @@ -1,7 +1,7 @@ package "neuron-landing" { version "1.0.0" description "Neuron marketing landing page server" - authors ["Will Anderson "] + authors ["Will Anderson "] edition "2026" } @@ -9,4 +9,8 @@ build { target "release" entry "src/main.el" output "dist/" + c_source "dist/web_stubs.c" + c_source "dist/vessel_stubs.c" + c_source "dist/elhtml_impl.c" + c_source "dist/page_close.c" } diff --git a/runtime/el_runtime.c b/runtime/el_runtime.c index 0ba1432..9697867 100644 --- a/runtime/el_runtime.c +++ b/runtime/el_runtime.c @@ -42,7 +42,10 @@ #include #include #include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL #include +#endif /* ── Internal allocators ─────────────────────────────────────────────────── */ @@ -102,6 +105,45 @@ void el_request_end(void) { _tl_arena.count = 0; } +/* ── Scoped arena for CLI use ─────────────────────────────────────────────── * + * CLI programs never call el_request_start/end, so all strdup allocations are + * permanent. el_arena_push/pop let the compiler free intermediate strings + * after each compilation unit. + * + * el_arena_push() — activates the arena if not already active, saves the + * current arena count as a mark, and returns it as an el_val_t Int. + * el_arena_pop(mark) — frees all strings allocated since the push mark and + * resets the count. If count reaches 0, deactivates the arena. + */ +#define EL_ARENA_SCOPE_DEPTH 32 +static _Thread_local size_t _tl_arena_scope[EL_ARENA_SCOPE_DEPTH]; +static _Thread_local int _tl_arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_tl_arena_active) { + _tl_arena_active = 1; + } + if (_tl_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) { + _tl_arena_scope[_tl_arena_scope_depth++] = _tl_arena.count; + } + return (el_val_t)(int64_t)_tl_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _tl_arena.count) save = 0; + for (size_t i = save; i < _tl_arena.count; i++) { + if (_tl_arena.ptrs[i]) { + free(_tl_arena.ptrs[i]); + _tl_arena.ptrs[i] = NULL; + } + } + _tl_arena.count = save; + if (_tl_arena_scope_depth > 0) _tl_arena_scope_depth--; + if (save == 0) _tl_arena_active = 0; + return 0; +} + /* Persistent allocation — bypasses the arena (state_set, engram internals). */ static char* el_strdup_persist(const char* s) { if (!s) return strdup(""); @@ -136,15 +178,17 @@ static el_val_t el_wrap_str(char* s) { /* ── I/O ──────────────────────────────────────────────────────────────────── */ -void println(el_val_t s) { +el_val_t println(el_val_t s) { const char* str = EL_CSTR(s); if (str) puts(str); else puts(""); + return 0; } -void print(el_val_t s) { +el_val_t print(el_val_t s) { const char* str = EL_CSTR(s); if (str) fputs(str, stdout); + return 0; } el_val_t readline(void) { @@ -155,6 +199,39 @@ el_val_t readline(void) { return el_wrap_str(el_strdup(buf)); } +/* __read_n — read exactly n bytes from stdin. + * Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read + * exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and + * returns the buffer as an El String. Returns "" on EOF or I/O error. + * + * Used by the El LSP server to read JSON-RPC message bodies after parsing + * the Content-Length header. readline() cannot be used for the body because + * it stops at the first \n and LSP JSON bodies are not newline-terminated. */ +el_val_t __read_n(el_val_t nv) { + int64_t n = EL_INT(nv); + if (n <= 0) return el_wrap_str(el_strdup("")); + char* buf = malloc((size_t)n + 1); + if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)n, stdin); + buf[got] = '\0'; + if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); } + /* Track in arena so the allocation is freed when the request ends. */ + el_arena_track(buf); + return el_wrap_str(buf); +} + +/* __print_raw — write a string to stdout without any modification. + * Unlike println/print (which call puts/fputs and may add newlines or flush + * in platform-specific ways), this uses fwrite with the exact byte count so + * that embedded \r\n pairs in LSP Content-Length headers survive intact. */ +void __print_raw(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return; + size_t len = strlen(s); + fwrite(s, 1, len, stdout); + fflush(stdout); +} + /* ── String builtins ─────────────────────────────────────────────────────── */ el_val_t el_str_concat(el_val_t av, el_val_t bv) { @@ -218,6 +295,10 @@ el_val_t str_to_int(el_val_t sv) { return (el_val_t)atoll(s); } +/* native_str_to_int — El compiler-generated alias for str_to_int. + * Converts a string el_val_t to its integer representation. */ +el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); } + el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { const char* s = EL_CSTR(sv); if (!s) return el_wrap_str(el_strdup("")); @@ -667,6 +748,39 @@ struct JsonParser { * the loop is observable. */ +/* ── JSON error helper (used by HTTP, PQ, crypto stubs) ─────────────────── */ + +/* JSON-escape an arbitrary C string into an allocated buffer. */ +static char* json_escape_alloc(const char* s) { + if (!s) return el_strdup(""); + JsonBuf b; jb_init(&b); + for (const char* p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': jb_puts(&b, "\\\""); break; + case '\\': jb_puts(&b, "\\\\"); break; + case '\n': jb_puts(&b, "\\n"); break; + case '\r': jb_puts(&b, "\\r"); break; + case '\t': jb_puts(&b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(&b, tmp); + } else jb_putc(&b, (char)c); + } + } + return b.buf; +} + +static el_val_t http_error_json(const char* msg) { + char* esc = json_escape_alloc(msg ? msg : "unknown error"); + char* buf = el_strbuf(strlen(esc) + 16); + sprintf(buf, "{\"error\":\"%s\"}", esc); + free(esc); + return el_wrap_str(buf); +} + +#ifdef HAVE_CURL /* ── HTTP client write-callback buffer ───────────────────────────────────── */ typedef struct { @@ -700,36 +814,6 @@ static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { return n; } -/* JSON-escape an arbitrary C string into an allocated buffer. */ -static char* json_escape_alloc(const char* s) { - if (!s) return el_strdup(""); - JsonBuf b; jb_init(&b); - for (const char* p = s; *p; p++) { - unsigned char c = (unsigned char)*p; - switch (c) { - case '"': jb_puts(&b, "\\\""); break; - case '\\': jb_puts(&b, "\\\\"); break; - case '\n': jb_puts(&b, "\\n"); break; - case '\r': jb_puts(&b, "\\r"); break; - case '\t': jb_puts(&b, "\\t"); break; - default: - if (c < 0x20) { - char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); - jb_puts(&b, tmp); - } else jb_putc(&b, (char)c); - } - } - return b.buf; -} - -static el_val_t http_error_json(const char* msg) { - char* esc = json_escape_alloc(msg ? msg : "unknown error"); - char* buf = el_strbuf(strlen(esc) + 16); - sprintf(buf, "{\"error\":\"%s\"}", esc); - free(esc); - return el_wrap_str(buf); -} - /* HTTP timeout (ms) — read once from EL_HTTP_TIMEOUT_MS, default 60000. * Applied via CURLOPT_TIMEOUT_MS on every libcurl request. */ static long _el_http_timeout_ms = -1; @@ -830,6 +914,33 @@ el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_ma return r; } +/* http_post_json_with_headers — POST with Content-Type: application/json plus + * any additional headers supplied as an El map. Combines http_post_json and + * http_post_with_headers: the Content-Type header is always prepended so + * callers do not have to include it in their map. */ +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + /* Append caller-supplied headers from the map */ + ElMap* m = as_map(headers_map); + if (m) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) { struct curl_slist* h = NULL; h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded"); @@ -937,6 +1048,7 @@ el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el if (h) curl_slist_free_all(h); return r; } +#endif /* HAVE_CURL */ /* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ @@ -981,7 +1093,7 @@ void el_runtime_register_handler(const char* name, http_handler_fn fn) { pthread_mutex_unlock(&_http_handler_mu); } -void http_set_handler(el_val_t name) { +el_val_t http_set_handler(el_val_t name) { const char* n = EL_CSTR(name); pthread_mutex_lock(&_http_handler_mu); free(_http_active_handler); @@ -1005,6 +1117,7 @@ void http_set_handler(el_val_t name) { } } pthread_mutex_unlock(&_http_handler_mu); + return 0; } static http_handler_fn http_lookup_active(void) { @@ -1462,18 +1575,18 @@ static void* http_worker(void* arg) { return NULL; } -void http_serve(el_val_t port, el_val_t handler) { +el_val_t http_serve(el_val_t port, el_val_t handler) { /* If `handler` looks like a string name, register it as the active handler. */ const char* hname = EL_CSTR(handler); if (hname && looks_like_string(handler)) { http_set_handler(handler); } int p = (int)port; - if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; } + if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; } /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6. * This makes `localhost` work in browsers that resolve it to ::1 first. */ int sock = socket(AF_INET6, SOCK_STREAM, 0); - if (sock < 0) { perror("socket"); return; } + if (sock < 0) { perror("socket"); return 0; } int yes = 1; int no = 0; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); @@ -1483,9 +1596,9 @@ void http_serve(el_val_t port, el_val_t handler) { addr.sin6_addr = in6addr_any; addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - perror("bind"); close(sock); return; + perror("bind"); close(sock); return 0; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); while (1) { struct sockaddr_in6 cli; @@ -1516,6 +1629,7 @@ void http_serve(el_val_t port, el_val_t handler) { pthread_detach(tid); } close(sock); + return 0; } /* ── HTTP server v2 — request headers + structured response ──────────────── */ @@ -1567,7 +1681,7 @@ void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { pthread_mutex_unlock(&_http_handler_mu); } -void http_set_handler_v2(el_val_t name) { +el_val_t http_set_handler_v2(el_val_t name) { const char* n = EL_CSTR(name); pthread_mutex_lock(&_http_handler_mu); free(_http_active_handler4); @@ -1589,6 +1703,7 @@ void http_set_handler_v2(el_val_t name) { } } pthread_mutex_unlock(&_http_handler_mu); + return 0; } static http_handler4_fn http_lookup_active_v2(void) { @@ -1709,7 +1824,7 @@ static void* http_worker_v2(void* arg) { return NULL; } -void http_serve_v2(el_val_t port, el_val_t handler) { +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { const char* hname = EL_CSTR(handler); if (hname && looks_like_string(handler)) { http_set_handler_v2(handler); @@ -1717,11 +1832,11 @@ void http_serve_v2(el_val_t port, el_val_t handler) { int p = (int)port; if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve_v2: invalid port %d\n", p); - return; + return 0; } /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */ int sock = socket(AF_INET6, SOCK_STREAM, 0); - if (sock < 0) { perror("socket"); return; } + if (sock < 0) { perror("socket"); return 0; } int yes = 1; int no = 0; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); @@ -1731,9 +1846,9 @@ void http_serve_v2(el_val_t port, el_val_t handler) { addr.sin6_addr = in6addr_any; addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - perror("bind"); close(sock); return; + perror("bind"); close(sock); return 0; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); while (1) { struct sockaddr_in6 cli; @@ -1764,6 +1879,7 @@ void http_serve_v2(el_val_t port, el_val_t handler) { pthread_detach(tid); } close(sock); + return 0; } /* Build the response envelope a 4-arg handler can return. We hand-write @@ -1854,6 +1970,34 @@ el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { return 1; } +// stdout_to_file / stdout_restore — redirect process stdout to a file and +// restore it. Used by the compiler's JS post-processing pipeline to capture +// codegen output before piping through terser / obfuscator. +#include +static int _el_saved_stdout_fd = -1; + +el_val_t stdout_to_file(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return (el_val_t)(int64_t)-1; + fflush(stdout); + _el_saved_stdout_fd = dup(STDOUT_FILENO); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return (el_val_t)(int64_t)-1; + dup2(fd, STDOUT_FILENO); + close(fd); + return (el_val_t)(int64_t)0; +} + +el_val_t stdout_restore(void) { + if (_el_saved_stdout_fd >= 0) { + fflush(stdout); + dup2(_el_saved_stdout_fd, STDOUT_FILENO); + close(_el_saved_stdout_fd); + _el_saved_stdout_fd = -1; + } + return (el_val_t)(int64_t)0; +} + // exec_command — run a shell command, return exit code (0 = success). // Used by elb and other El tooling to invoke subprocesses. el_val_t exec_command(el_val_t cmdv) { @@ -1947,6 +2091,52 @@ el_val_t fs_list(el_val_t pathv) { return lst; } +/* fs_list_json — return directory entries as a JSON array of strings. + * Returns "[]" for missing or non-directory paths. Excludes "." and "..". */ +el_val_t fs_list_json(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return EL_STR("[]"); + DIR* d = opendir(path); + if (!d) return EL_STR("[]"); + /* Collect entries first so we can build the JSON in one pass. */ + char** names = NULL; + size_t count = 0, cap = 0; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + if (count >= cap) { + cap = cap ? cap * 2 : 16; + names = realloc(names, cap * sizeof(char*)); + if (!names) { closedir(d); return EL_STR("[]"); } + } + names[count++] = strdup(e->d_name); + } + closedir(d); + /* Build JSON array. */ + size_t sz = 3; /* "[]" + NUL */ + for (size_t i = 0; i < count; i++) sz += strlen(names[i]) * 2 + 6; /* conservative */ + char* buf = malloc(sz); + if (!buf) { for (size_t i = 0; i < count; i++) free(names[i]); free(names); return EL_STR("[]"); } + size_t pos = 0; + buf[pos++] = '['; + for (size_t i = 0; i < count; i++) { + if (i > 0) buf[pos++] = ','; + buf[pos++] = '"'; + for (const char* p = names[i]; *p; p++) { + if (*p == '"' || *p == '\\') buf[pos++] = '\\'; + else if (*p == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; continue; } + else if (*p == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; continue; } + buf[pos++] = *p; + } + buf[pos++] = '"'; + free(names[i]); + } + free(names); + buf[pos++] = ']'; + buf[pos] = '\0'; + return el_wrap_str(buf); +} + /* fs_exists — true iff stat(path) succeeds. Symlinks are followed. */ el_val_t fs_exists(el_val_t pathv) { const char* path = EL_CSTR(pathv); @@ -2052,6 +2242,43 @@ el_val_t url_decode(el_val_t sv) { return el_wrap_str(out); } +/* ── html_raw ──────────────────────────────────────────────────────────────── + * Identity passthrough for raw HTML template interpolation. + * El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is + * without any escaping. The caller is responsible for safety. + */ +el_val_t html_raw(el_val_t s) { + return s; +} + +/* ── html_escape ───────────────────────────────────────────────────────────── + * Escape < > " ' & for safe HTML text interpolation. + * El's {expr} in HTML templates compiles to html_escape(expr). + */ +el_val_t html_escape(el_val_t sv) { + const char* src = EL_CSTR(sv); + if (!src) return EL_STR(""); + size_t len = strlen(src); + /* Worst case: every byte → 6 chars (") */ + char* out = (char*)malloc(len * 6 + 1); + if (!out) return sv; + el_arena_track(out); + char* p = out; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + switch (c) { + case '&': memcpy(p, "&", 5); p += 5; break; + case '<': memcpy(p, "<", 4); p += 4; break; + case '>': memcpy(p, ">", 4); p += 4; break; + case '"': memcpy(p, """, 6); p += 6; break; + case '\'': memcpy(p, "'", 5); p += 5; break; + default: *p++ = (char)c; break; + } + } + *p = '\0'; + return el_wrap_str(out); +} + /* ── HTML allowlist sanitizer ──────────────────────────────────────────────── * el_html_sanitize(input, allowlist_json) * @@ -3198,14 +3425,20 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) { el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { const char* json = EL_CSTR(json_str); const char* k = EL_CSTR(key); + /* raw_val is the JSON value as-is (already encoded by the caller). + * If it looks like a plain (non-JSON) string, wrap it as a JSON string. + * Convention: callers pass pre-encoded values like "\"bob\"" for strings, + * "42" for numbers, "true"/"false" for booleans. */ + const char* raw_val = EL_CSTR(value); if (!k) k = ""; + if (!raw_val) raw_val = "null"; if (!json || !*json) { /* Build a fresh object */ JsonBuf b; jb_init(&b); jb_putc(&b, '{'); jb_emit_escaped(&b, k); jb_putc(&b, ':'); - jb_emit_value(&b, value); + jb_puts(&b, raw_val); jb_putc(&b, '}'); return el_wrap_str(b.buf); } @@ -3219,7 +3452,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { memcpy(b.buf + b.len, json, prefix); b.len += prefix; b.buf[b.len] = '\0'; - jb_emit_value(&b, value); + jb_puts(&b, raw_val); jb_puts(&b, end); return el_wrap_str(b.buf); } @@ -3250,7 +3483,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { if (!empty) jb_putc(&b, ','); jb_emit_escaped(&b, k); jb_putc(&b, ':'); - jb_emit_value(&b, value); + jb_puts(&b, raw_val); /* Append from close_idx onward */ jb_puts(&b, json + close_idx); return el_wrap_str(b.buf); @@ -3331,6 +3564,87 @@ el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { return el_wrap_str(parsed); } +/* json_escape_string — escape a string value for embedding in JSON. + * Returns the escaped content WITHOUT surrounding quotes. + * "say \"hello\"" -> "say \\\"hello\\\"" */ +el_val_t json_escape_string(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + /* Worst case: every char needs a 2-char escape. */ + char* out = malloc(n * 2 + 1); + if (!out) return el_wrap_str(el_strdup("")); + size_t j = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '"') { out[j++] = '\\'; out[j++] = '"'; } + else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; } + else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; } + else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; } + else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; } + else { out[j++] = (char)c; } + } + out[j] = '\0'; + el_val_t result = el_wrap_str(el_strdup(out)); + free(out); + return result; +} + +/* json_build_object — build a JSON object from a flat key-value list. + * kvs is [key0, val0, key1, val1, ...]. Values are raw JSON (pass + * strings as "\"value\"" or use json_escape_string). */ +el_val_t json_build_object(el_val_t kvs) { + el_val_t list = kvs; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + int first = 1; + for (int64_t i = 0; i + 1 < n; i += 2) { + el_val_t k = el_list_get(list, (el_val_t)i); + el_val_t v = el_list_get(list, (el_val_t)(i + 1)); + const char* ks = EL_CSTR(k); + const char* vs = EL_CSTR(v); + if (!ks || !vs) continue; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '"'); + jb_puts(&b, ks); + jb_puts(&b, "\":\""); + /* escape the value string */ + size_t vn = strlen(vs); + for (size_t j = 0; j < vn; j++) { + unsigned char c = (unsigned char)vs[j]; + if (c == '"') { jb_putc(&b, '\\'); jb_putc(&b, '"'); } + else if (c == '\\') { jb_putc(&b, '\\'); jb_putc(&b, '\\'); } + else if (c == '\n') { jb_putc(&b, '\\'); jb_putc(&b, 'n'); } + else if (c == '\r') { jb_putc(&b, '\\'); jb_putc(&b, 'r'); } + else if (c == '\t') { jb_putc(&b, '\\'); jb_putc(&b, 't'); } + else { jb_putc(&b, (char)c); } + } + jb_putc(&b, '"'); + } + jb_putc(&b, '}'); + return el_wrap_str(b.buf); +} + +/* json_build_array — build a JSON array from a list of raw JSON values. + * items is ["\"alpha\"", "\"beta\"", "42", "true", ...]. */ +el_val_t json_build_array(el_val_t items) { + el_val_t list = items; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < n; i++) { + el_val_t v = el_list_get(list, (el_val_t)i); + const char* vs = EL_CSTR(v); + if (!vs) continue; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, vs); + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + /* ── Time ────────────────────────────────────────────────────────────────── */ el_val_t time_now(void) { @@ -3352,7 +3666,7 @@ el_val_t time_format(el_val_t ts, el_val_t fmt) { struct tm tm; gmtime_r(&s, &tm); const char* fmt_str = EL_CSTR(fmt); - if (!fmt_str || strcmp(fmt_str, "ISO") == 0) { + if (!fmt_str || *fmt_str == '\0' || strcmp(fmt_str, "ISO") == 0) { char buf[64]; snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, @@ -3371,15 +3685,13 @@ el_val_t time_to_parts(el_val_t ts) { if (msec < 0) { msec += 1000; s -= 1; } struct tm tm; gmtime_r(&s, &tm); - el_val_t m = el_map_new(0); - m = el_map_set(m, EL_STR(el_strdup("year")), (el_val_t)(tm.tm_year + 1900)); - m = el_map_set(m, EL_STR(el_strdup("month")), (el_val_t)(tm.tm_mon + 1)); - m = el_map_set(m, EL_STR(el_strdup("day")), (el_val_t)tm.tm_mday); - m = el_map_set(m, EL_STR(el_strdup("hour")), (el_val_t)tm.tm_hour); - m = el_map_set(m, EL_STR(el_strdup("minute")), (el_val_t)tm.tm_min); - m = el_map_set(m, EL_STR(el_strdup("second")), (el_val_t)tm.tm_sec); - m = el_map_set(m, EL_STR(el_strdup("ms")), (el_val_t)msec); - return m; + /* Return a JSON string so callers can use json_get to extract fields. */ + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"year\":%d,\"month\":%d,\"day\":%d,\"hour\":%d,\"minute\":%d,\"second\":%d,\"ms\":%d}", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); } el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { @@ -3485,6 +3797,12 @@ el_val_t now(void) { return el_now_instant(); } +/* now_ns — return current Unix time as nanoseconds (Int). + * Thin wrapper over el_now_instant for use in test timing. */ +el_val_t now_ns(void) { + return el_now_instant(); +} + /* unix_seconds(n) — Instant from a Unix-epoch second count. * unix_millis(n) — Instant from a Unix-epoch millisecond count. */ el_val_t unix_seconds(el_val_t n) { @@ -4712,19 +5030,57 @@ el_val_t state_del(el_val_t key) { el_val_t state_keys(void) { pthread_mutex_lock(&_state_mu); - el_val_t lst = el_list_empty(); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); for (size_t i = 0; i < _state_count; i++) { - lst = el_list_append(lst, el_wrap_str(el_strdup(_state_entries[i].key))); + if (i > 0) jb_putc(&b, ','); + jb_putc(&b, '"'); + jb_emit_escaped(&b, _state_entries[i].key); + jb_putc(&b, '"'); + } + jb_putc(&b, ']'); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(b.buf); +} + +/* Returns 1 (true) if the key is present in the state store, else 0 (false). */ +el_val_t state_has(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + int found = (e != NULL) ? 1 : 0; + pthread_mutex_unlock(&_state_mu); + return (el_val_t)found; +} + +/* Returns the value for key, or default_val if the key is absent. */ +el_val_t state_get_or(el_val_t key, el_val_t default_val) { + const char* k = EL_CSTR(key); + if (!k) return default_val; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + char* copy = el_strdup(e->value); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(copy); } pthread_mutex_unlock(&_state_mu); - return lst; + return default_val; } /* ── Float formatting ────────────────────────────────────────────────────── */ el_val_t float_to_str(el_val_t f) { char buf[64]; - snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + double v = el_to_float(f); + /* Normalize NaN to "nan" regardless of sign — platform-independent. */ + if (isnan(v)) { + snprintf(buf, sizeof(buf), "nan"); + } else { + snprintf(buf, sizeof(buf), "%g", v); + } return el_wrap_str(el_strdup(buf)); } @@ -5344,8 +5700,9 @@ el_val_t parse_int(el_val_t sv, el_val_t default_val) { /* ── Process ─────────────────────────────────────────────────────────────── */ -void exit_program(el_val_t code) { +el_val_t exit_program(el_val_t code) { exit((int)code); + return 0; /* unreachable */ } /* getpid_now — current process id. Named with the _now suffix to avoid @@ -5355,6 +5712,50 @@ el_val_t getpid_now(void) { return (el_val_t)getpid(); } +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + /* ── args() — command-line argument access ────────────────────────────────── * Compiled El programs call args() to get a list of CLI arguments. * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. @@ -7215,8 +7616,10 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di free(frontier); free(frontier_h); free(visited); jb_putc(&b, ']'); return el_wrap_str(b.buf); } - frontier[fc] = el_strdup(sid); frontier_h[fc] = 0; fc++; - visited[vc++] = el_strdup(sid); + /* Use plain strdup (not el_strdup) so arena doesn't track these pointers. + * The BFS loop manually frees them below — arena would double-free them. */ + frontier[fc] = strdup(sid); frontier_h[fc] = 0; fc++; + visited[vc++] = strdup(sid); int first = 1; while (fc > 0) { @@ -7245,8 +7648,8 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); jb_puts(&b, tmp); first = 0; - if (vc < 1024) visited[vc++] = el_strdup(peer); - if (fc < 1024 && h + 1 < depth) { frontier[fc] = el_strdup(peer); frontier_h[fc] = h + 1; fc++; } + if (vc < 1024) visited[vc++] = strdup(peer); + if (fc < 1024 && h + 1 < depth) { frontier[fc] = strdup(peer); frontier_h[fc] = h + 1; fc++; } } free(cur); } @@ -7486,6 +7889,7 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { return el_wrap_str(b.buf); } +#ifdef HAVE_CURL /* ── DHARMA network ───────────────────────────────────────────────────────── * Real implementation. Peers are addressed by `dharma_id` — either bare * (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or @@ -7997,6 +8401,7 @@ el_val_t dharma_peers(void) { free(peers); return out; } +#endif /* HAVE_CURL — DHARMA network */ /* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ /* @@ -8011,6 +8416,7 @@ el_val_t dharma_peers(void) { * and returns a JSON-string el_val_t result. Iteration is capped at 10. */ +#ifdef HAVE_CURL static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5"; static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages"; static const char* LLM_VERSION = "2023-06-01"; @@ -8696,6 +9102,7 @@ el_val_t llm_models(void) { lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5"))); return lst; } +#endif /* HAVE_CURL */ /* ── Native VM builtin aliases ────────────────────────────────────────────── * El source files use native_* names (El VM builtins). @@ -9886,6 +10293,7 @@ el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_ #endif /* __has_include() */ +#ifdef HAVE_CURL /* ──────────────────────────────────────────────────────────────────────────── * OTLP/HTTP observability — logs, traces, metrics * @@ -10244,3 +10652,854 @@ el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { return trace_span_end(h); } +#endif /* HAVE_CURL — OTLP */ + +/* ── Threading seed primitives ─────────────────────────────────────────────── + * __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid + * __thread_join(tid) -> String join thread, return result string + * __mutex_new() -> Int allocate a mutex, return handle + * __mutex_lock(m) lock mutex m + * __mutex_unlock(m) unlock mutex m + * + * Every El fn compiles to a global C symbol. __thread_create uses dlsym to + * look up the function by name and run it in a pthread. This means any El fn + * with signature (String) -> String is directly threadable. + */ + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + ElFn1 fn; + el_val_t arg; + el_val_t result; +} ElThreadArg; + +#define EL_THREAD_MAX 256 + +typedef struct { + pthread_t tid; + ElThreadArg* arg; + int alive; +} ElThread; + +static ElThread _threads[EL_THREAD_MAX]; +static int _thread_count = 0; +static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +static void* el_thread_runner(void* raw) { + ElThreadArg* a = (ElThreadArg*)raw; + a->result = a->fn(a->arg); + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) { + const char* sym = EL_CSTR(fn_name_v); + if (!sym || !*sym) return EL_INT(-1); + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym); + return EL_INT(-1); + } + ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg)); + if (!a) return EL_INT(-1); + a->fn = (ElFn1)p; + a->arg = arg_v; + a->result = EL_STR(""); + + pthread_mutex_lock(&_thread_alloc_mu); + if (_thread_count >= EL_THREAD_MAX) { + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + fprintf(stderr, "[__thread_create] thread table full\n"); + return EL_INT(-1); + } + int slot = _thread_count++; + _threads[slot].arg = a; + _threads[slot].alive = 1; + pthread_mutex_unlock(&_thread_alloc_mu); + + if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) { + pthread_mutex_lock(&_thread_alloc_mu); + _thread_count--; + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + return EL_INT(-1); + } + return EL_INT(slot); +} + +el_val_t __thread_join(el_val_t tid_v) { + int slot = (int)(int64_t)tid_v; + if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR(""); + pthread_join(_threads[slot].tid, NULL); + el_val_t result = _threads[slot].arg->result; + free(_threads[slot].arg); + _threads[slot].alive = 0; + return result; +} + +/* Mutex table */ + +#define EL_MUTEX_MAX 64 + +typedef struct { + pthread_mutex_t mu; + int allocated; +} ElMutexEntry; + +static ElMutexEntry _mutexes[EL_MUTEX_MAX]; +static int _mutex_count = 0; +static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_mutex_alloc_mu); + if (_mutex_count >= EL_MUTEX_MAX) { + pthread_mutex_unlock(&_mutex_alloc_mu); + fprintf(stderr, "[__mutex_new] mutex table full\n"); + return EL_INT(-1); + } + int slot = _mutex_count++; + pthread_mutex_init(&_mutexes[slot].mu, NULL); + _mutexes[slot].allocated = 1; + pthread_mutex_unlock(&_mutex_alloc_mu); + return EL_INT(slot); +} + +void __mutex_lock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_lock(&_mutexes[slot].mu); +} + +void __mutex_unlock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_unlock(&_mutexes[slot].mu); +} + +/* ── Channels ─────────────────────────────────────────────────────────────── * + * Buffered MPMC channel backed by a mutex + condvar + circular buffer. + * channel_new(capacity) -> Int (handle) + * channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded) + * channel_recv(ch) -> String — blocks until a message is available + * channel_try_recv(ch) -> String — non-blocking, returns "" if empty + * channel_close(ch) — signal no more sends; recv drains remaining + * + * Bounded channels (cap > 0): circular buffer, sender blocks when full. + * Unbounded channels (cap == 0): dynamic array, sender never blocks. + */ +#define EL_CHANNEL_MAX 64 +#define EL_CHANNEL_BUF 1024 + +typedef struct { + char** buf; + int cap; /* 0 = unbounded (grows dynamically) */ + int head, tail, count; + int dyn_cap; /* allocated slots for unbounded mode */ + int closed; + pthread_mutex_t mu; + pthread_cond_t not_empty; + pthread_cond_t not_full; +} ElChannel; + +static ElChannel _channels[EL_CHANNEL_MAX]; +static int _channel_count = 0; +static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __channel_new(el_val_t capacity_v) { + int cap = (int)(int64_t)capacity_v; + if (cap < 0) cap = 0; + + pthread_mutex_lock(&_channel_alloc_mu); + if (_channel_count >= EL_CHANNEL_MAX) { + pthread_mutex_unlock(&_channel_alloc_mu); + fprintf(stderr, "[__channel_new] channel table full\n"); + return EL_INT(-1); + } + int slot = _channel_count++; + pthread_mutex_unlock(&_channel_alloc_mu); + + ElChannel* ch = &_channels[slot]; + memset(ch, 0, sizeof(*ch)); + ch->cap = cap; + ch->closed = 0; + ch->head = 0; + ch->tail = 0; + ch->count = 0; + + if (cap > 0) { + /* Bounded: fixed circular buffer. */ + ch->buf = (char**)malloc((size_t)cap * sizeof(char*)); + ch->dyn_cap = cap; + } else { + /* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */ + ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*)); + ch->dyn_cap = EL_CHANNEL_BUF; + } + if (!ch->buf) { + fprintf(stderr, "[__channel_new] out of memory\n"); + return EL_INT(-1); + } + + pthread_mutex_init(&ch->mu, NULL); + pthread_cond_init(&ch->not_empty, NULL); + pthread_cond_init(&ch->not_full, NULL); + + return EL_INT(slot); +} + +void __channel_send(el_val_t ch_v, el_val_t msg_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + const char* msg = EL_CSTR(msg_v); + if (!msg) msg = ""; + char* copy = strdup(msg); /* channel owns the string */ + + pthread_mutex_lock(&ch->mu); + + if (ch->closed) { + /* Send on closed channel is a no-op (drop the message). */ + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + + if (ch->cap > 0) { + /* Bounded: block while full. */ + while (ch->count >= ch->cap && !ch->closed) { + pthread_cond_wait(&ch->not_full, &ch->mu); + } + if (ch->closed) { + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->cap; + ch->count++; + } else { + /* Unbounded: grow the buffer if needed. */ + if (ch->count >= ch->dyn_cap) { + int new_cap = ch->dyn_cap * 2; + char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*)); + if (!grown) { + pthread_mutex_unlock(&ch->mu); + free(copy); + fprintf(stderr, "[__channel_send] out of memory growing channel\n"); + return; + } + /* The circular buffer may have wrapped. Linearise it first. + * In unbounded mode head is always 0 (we append at tail, drain + * from head), so a simple memmove isn't needed — but if the + * buffer did wrap (tail < head after growth), we need to fix up. + * Simplest safe path: if tail wrapped, move the head..old_cap + * segment to new_cap..new_cap+(old_cap-head). */ + if (ch->tail < ch->head) { + /* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */ + int front = ch->dyn_cap - ch->head; + memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*)); + ch->head = ch->dyn_cap; + } + ch->buf = grown; + ch->dyn_cap = new_cap; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->dyn_cap; + ch->count++; + } + + pthread_cond_signal(&ch->not_empty); + pthread_mutex_unlock(&ch->mu); +} + +el_val_t __channel_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + /* Block until there is a message or the channel is closed and drained. */ + while (ch->count == 0 && !ch->closed) { + pthread_cond_wait(&ch->not_empty, &ch->mu); + } + + if (ch->count == 0) { + /* Closed and empty — signal EOF. */ + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + /* Hand the string to the arena so it is freed after the request. */ + el_arena_track(msg); + return EL_STR(msg); +} + +el_val_t __channel_try_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + if (ch->count == 0) { + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + el_arena_track(msg); + return EL_STR(msg); +} + +void __channel_close(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + ch->closed = 1; + /* Wake all blocked recvers and senders so they can observe the close. */ + pthread_cond_broadcast(&ch->not_empty); + pthread_cond_broadcast(&ch->not_full); + pthread_mutex_unlock(&ch->mu); +} + +/* ── DHARMA runtime additions ──────────────────────────────────────────────── + * + * Functions required by the dharma registry service. Added here so the + * released el_runtime.c includes them without requiring dharma to bundle + * its own stubs. + * + * Functions added: + * list_len — alias for el_list_len (used in handlers.el) + * list_get — alias for el_list_get (used in handlers.el) + * json_array_push — append a pre-encoded JSON element to a JSON array string + * now_millis — milliseconds since Unix epoch (alias for time_now) + * unix_timestamp_ms — same as now_millis (alias) + * time_now_ms — same as now_millis (alias) + * log_info — stderr structured log at INFO level + * log_warn — stderr structured log at WARN level + * config — reads a config value from the environment + * http_patch — HTTP PATCH with JSON Content-Type + * http_post_engram — HTTP POST with optional X-API-Key header + * http_get_engram — HTTP GET with optional X-API-Key header + * str_to_bytes — encode a string as a JSON array of byte values + * bytes_to_str — decode a JSON array of byte values back to a string + * hash_sha256 — SHA-256 hex digest of a string + */ + +/* list_len — return the number of elements in a list. */ +el_val_t list_len(el_val_t list) { + return el_list_len(list); +} + +/* list_get — return the element at index i in a list. */ +el_val_t list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\"" + * or "42") to the JSON array string arr. Returns a new JSON array string. + * Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" + * json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */ +el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) { + const char* arr = EL_CSTR(arr_v); + const char* elem = EL_CSTR(elem_v); + if (!arr || !*arr) arr = "[]"; + if (!elem || !*elem) elem = "null"; + + /* Trim whitespace, find the closing ']'. */ + const char* p = arr; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { + /* Not an array — return a single-element array. */ + size_t n = strlen(elem) + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + size_t arr_len = strlen(arr); + size_t elem_len = strlen(elem); + + /* Walk from the end to find the matching ']'. */ + const char* end = arr + arr_len - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; + if (*end != ']') { + /* Malformed — wrap elem in a new array. */ + size_t n = elem_len + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + + /* Content between '[' and ']'. */ + const char* inner_start = p + 1; + const char* inner_end = end; /* points AT ']' */ + /* Check if the array is empty (only whitespace between brackets). */ + const char* q = inner_start; + while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++; + int empty = (q == inner_end); + + /* Build: prefix + (comma if non-empty) + elem + "]" */ + size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */ + size_t sep_len = empty ? 0 : 1; /* "," if non-empty */ + size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */ + char* out = el_strbuf(out_len); + memcpy(out, arr, prefix_len); + if (!empty) out[prefix_len] = ','; + memcpy(out + prefix_len + sep_len, elem, elem_len); + out[prefix_len + sep_len + elem_len] = ']'; + out[prefix_len + sep_len + elem_len + 1] = '\0'; + return el_wrap_str(out); +} + +/* now_millis — milliseconds since Unix epoch. */ +el_val_t now_millis(void) { + return time_now(); +} + +/* unix_timestamp_ms — same as now_millis. */ +el_val_t unix_timestamp_ms(void) { + return time_now(); +} + +/* time_now_ms — same as now_millis. */ +el_val_t time_now_ms(void) { + return time_now(); +} + +/* log_info — write a structured [INFO] line to stderr. */ +void log_info(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[INFO] %s\n", msg ? msg : ""); +} + +/* log_warn — write a structured [WARN] line to stderr. */ +void log_warn(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[WARN] %s\n", msg ? msg : ""); +} + +/* config — read a configuration value from the environment. + * Returns "" if the variable is not set (same as __env_get). */ +el_val_t config(el_val_t key_v) { + const char* key = EL_CSTR(key_v); + if (!key || !*key) return EL_STR(""); + const char* val = getenv(key); + if (!val) return EL_STR(""); + return el_wrap_str(el_strdup(val)); +} + +#ifdef HAVE_CURL +/* http_patch — HTTP PATCH request with Content-Type: application/json. + * Returns the response body (same error convention as http_post_json). */ +el_val_t http_patch(el_val_t url_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_post_engram — HTTP POST with optional X-API-Key header. + * If key is "" no authentication header is sent. */ +el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_get_engram — HTTP GET with optional X-API-Key header. */ +el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + if (h) curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} +#endif /* HAVE_CURL */ + +/* str_to_bytes — encode a string as a JSON array of unsigned byte values. + * "hello" -> "[104,101,108,108,111]" + * Used by db.el to store binary content in Engram JSON nodes. */ +el_val_t str_to_bytes(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return el_wrap_str(el_strdup("[]")); + size_t n = strlen(s); + /* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */ + char* out = el_strbuf(n * 4 + 3); + size_t pos = 0; + out[pos++] = '['; + for (size_t i = 0; i < n; i++) { + unsigned char b = (unsigned char)s[i]; + if (i > 0) out[pos++] = ','; + /* Write decimal representation of b. */ + if (b >= 100) { + out[pos++] = (char)('0' + b / 100); + out[pos++] = (char)('0' + (b / 10) % 10); + out[pos++] = (char)('0' + b % 10); + } else if (b >= 10) { + out[pos++] = (char)('0' + b / 10); + out[pos++] = (char)('0' + b % 10); + } else { + out[pos++] = (char)('0' + b); + } + } + out[pos++] = ']'; + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* bytes_to_str — decode a JSON array of integer byte values back to a string. + * "[104,101,108,108,111]" -> "hello" + * Inverse of str_to_bytes. */ +el_val_t bytes_to_str(el_val_t arr_v) { + const char* s = EL_CSTR(arr_v); + if (!s) return el_wrap_str(el_strdup("")); + /* Skip whitespace, expect '['. */ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + + /* Count elements to size the output buffer. */ + int64_t n = (int64_t)json_array_len(arr_v); + if (n <= 0) return el_wrap_str(el_strdup("")); + + char* out = el_strbuf((size_t)n + 1); + size_t pos = 0; + + /* Walk the array, parse each integer, store as a byte. */ + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']' || *s == '\0') break; + /* Parse decimal integer. */ + char* end_ptr; + long v = strtol(s, &end_ptr, 10); + if (end_ptr == s) break; /* parse failure */ + s = end_ptr; + if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* hash_sha256 — return the SHA-256 hex digest of a string. + * Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */ +el_val_t hash_sha256(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)s, strlen(s), digest); + return el_hex_encode(digest, 32); +} + +/* ── __ prefixed aliases — public boundary for compiled El programs ────────── + * + * The El compiler's self-hosting back-end emits calls to __-prefixed function + * names (e.g. __println, __str_len). These wrappers forward to the existing + * el_runtime implementations so both naming conventions resolve at link time. + * + * Note: __thread_create and __thread_join are already defined above in the + * threading section; they are not repeated here. + * ──────────────────────────────────────────────────────────────────────────── */ + +/* I/O */ +el_val_t __println(el_val_t s) { return println(s); } +el_val_t __print(el_val_t s) { return print(s); } +el_val_t __readline(void) { return readline(); } + +/* String */ +el_val_t __int_to_str(el_val_t n) { return int_to_str(n); } +el_val_t __str_to_int(el_val_t s) { return str_to_int(s); } +el_val_t __float_to_str(el_val_t f) { return float_to_str(f); } +el_val_t __str_to_float(el_val_t s) { return str_to_float(s); } +el_val_t __str_len(el_val_t s) { return str_len(s); } +el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); } + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strcmp(ca, cb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strncmp(ca, cb, (size_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); } +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); } + +el_val_t __str_alloc(el_val_t n) { + if (n <= 0) n = 0; + char* buf = el_strbuf((size_t)n + 1); + memset(buf, 0, (size_t)n + 1); + return el_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* buf = (char*)(uintptr_t)s; + if (buf) buf[(size_t)i] = (char)c; + return s; +} + +/* URL encoding */ +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* Environment */ +el_val_t __env_get(el_val_t key) { return env(key); } + +/* Subprocess */ +el_val_t __exec(el_val_t cmd) { return exec(cmd); } +el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); } + +/* Process */ +el_val_t __exit_program(el_val_t code) { return exit_program(code); } + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path) { return fs_exists(path); } +el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); } +el_val_t __fs_read(el_val_t path) { return fs_read(path); } +el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); } +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); } +el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); } + +/* HTTP server (no curl dependency) */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); } +el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); } +el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); } + +/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here + * so el_runtime.c compiles standalone. When both translation units are linked + * the el_seed.c definitions win via their non-static linkage (strong symbols). + * These stubs are marked weak so they are silently overridden. */ +__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); } +__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; } +__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; } +__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; } + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); } +el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); } +el_val_t __json_array_len(el_val_t json) { return json_array_len(json); } +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); } +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); } + +/* State K/V */ +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_keys(void) { return state_keys(); } +el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); } + +/* UUID */ +el_val_t __uuid_v4(void) { return uuid_v4(); } + +/* Args */ +el_val_t __args_json(void) { return args(); } + +/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below + * with a matching stub in the #ifndef HAVE_CURL block. */ +#ifdef HAVE_CURL +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms) { + /* timeout_ms is accepted for API compatibility but ignored here; + * el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */ + (void)timeout_ms; + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map — same as __http_do but headers_map arg is a JSON-string + * rather than an ElMap. Parse it first, then delegate. */ +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + (void)timeout_ms; + /* Build a curl_slist from a JSON object {"Header":"value",...}. */ + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + /* Walk the JSON pairs with a simple parser reusing json_get_string logic. */ + /* For correctness we just call the existing json_get iteration path. + * We duplicate the key-extraction loop from headers_from_map but driven + * by JSON rather than ElMap. Use json_get_raw to iterate is not easy + * without knowing keys, so accept the JSON string and build a tmp map. */ + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map_to_file — same as __http_do_map but streams response body + * to a local file path rather than returning it as a string. */ +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path) { + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), + h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +#ifndef HAVE_CURL +/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── * + * These return a JSON error string so El programs get a clear message if they + * call HTTP/LLM functions in a curl-less build. */ +static el_val_t _no_curl_err(void) { + return el_wrap_str(el_strdup("{\"error\":\"not built with HAVE_CURL\"}")); +} +el_val_t http_get(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); } +el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t p) { (void)url; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t p) { (void)url; (void)b; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_engram(el_val_t url, el_val_t k, el_val_t b) { (void)url; (void)k; (void)b; return _no_curl_err(); } +el_val_t http_get_engram(el_val_t url, el_val_t k) { (void)url; (void)k; return _no_curl_err(); } +el_val_t llm_call(el_val_t m, el_val_t p) { (void)m; (void)p; return _no_curl_err(); } +el_val_t llm_call_system(el_val_t m, el_val_t s, el_val_t u) { (void)m; (void)s; (void)u; return _no_curl_err(); } +el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (void)m; (void)s; (void)u; (void)t; return _no_curl_err(); } +el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); } +el_val_t llm_models(void) { return el_list_empty(); } +void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; } +/* __ HTTP stubs (no-curl build) */ +el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); } +#endif /* !HAVE_CURL */ diff --git a/runtime/el_runtime.h b/runtime/el_runtime.h index 72bbf4b..2f9583f 100644 --- a/runtime/el_runtime.h +++ b/runtime/el_runtime.h @@ -22,6 +22,9 @@ * EL_STR(s) cast string literal to el_val_t * EL_CSTR(v) cast el_val_t back to const char* * EL_INT(v) identity — el_val_t is already int64_t + * EL_NULL null / zero value + * EL_FALSE boolean false (0) + * EL_TRUE boolean true (1) * * Link requirements: * -lcurl — required for the HTTP client (http_get, http_post, llm_*). @@ -53,6 +56,8 @@ typedef int64_t el_val_t; #define EL_CSTR(v) ((const char*)(uintptr_t)(v)) #define EL_INT(v) (v) #define EL_NULL ((el_val_t)0) +#define EL_FALSE ((el_val_t)0) +#define EL_TRUE ((el_val_t)1) /* Float values share the el_val_t (int64) slot via a bit-cast. * The codegen emits Float literals as `el_from_float()` so the @@ -76,8 +81,8 @@ extern "C" { /* ── I/O ──────────────────────────────────────────────────────────────────── */ -void println(el_val_t s); -void print(el_val_t s); +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); el_val_t readline(void); /* ── String builtins ─────────────────────────────────────────────────────── */ @@ -90,6 +95,7 @@ el_val_t str_len(el_val_t s); el_val_t str_concat(el_val_t a, el_val_t b); el_val_t int_to_str(el_val_t n); el_val_t str_to_int(el_val_t s); +el_val_t native_str_to_int(el_val_t s); el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end); el_val_t str_contains(el_val_t s, el_val_t sub); el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to); @@ -117,6 +123,10 @@ el_val_t el_min(el_val_t a, el_val_t b); void el_retain(el_val_t v); void el_release(el_val_t v); +/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */ +el_val_t el_arena_push(void); +el_val_t el_arena_pop(el_val_t mark); + /* ── List ────────────────────────────────────────────────────────────────── */ el_val_t el_list_new(el_val_t count, ...); @@ -140,10 +150,11 @@ el_val_t http_post(el_val_t url, el_val_t body); el_val_t http_post_json(el_val_t url, el_val_t json_body); el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); el_val_t http_delete(el_val_t url); -void http_serve(el_val_t port, el_val_t handler); -void http_set_handler(el_val_t name); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); /* HTTP server v2 ───────────────────────────────────────────────────────────── * Same dispatch model as http_serve, but the handler signature is widened: @@ -164,8 +175,8 @@ void http_set_handler(el_val_t name); * The 3-arg http_serve(port, handler) remains supported unchanged for * existing handlers (e.g. products/web/server.el): it dispatches with * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ -void http_serve_v2(el_val_t port, el_val_t handler); -void http_set_handler_v2(el_val_t name); +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); /* Build an HTTP response envelope. `headers_json` should be a JSON object * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The @@ -176,6 +187,11 @@ void http_set_handler_v2(el_val_t name); * auto-content-type contract for legacy handlers that return plain bodies. */ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); +/* SSE connection fd — set by http_worker_v2 before calling the El handler, + * cleared afterwards. Defined in el_seed.c; called from el_runtime.c. + * The getter is exposed as __http_conn_fd() to El programs. */ +void el_seed_set_http_conn_fd(int fd); + /* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default * 60000ms). Read lazily on first use, so setting the env var any time before * the first http_* call is sufficient. */ @@ -211,12 +227,15 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ * {"p":[],"a":["href","title"],"strong":[],...} * where each value is the array of attribute names allowed for that tag. */ el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t html_raw(el_val_t s); +el_val_t html_escape(el_val_t s); /* ── Filesystem ──────────────────────────────────────────────────────────── */ el_val_t fs_read(el_val_t path); el_val_t fs_write(el_val_t path, el_val_t content); el_val_t fs_list(el_val_t path); +el_val_t fs_list_json(el_val_t path); el_val_t fs_exists(el_val_t path); el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */ @@ -246,6 +265,9 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); el_val_t json_array_len(el_val_t json_str); el_val_t json_array_get(el_val_t json_str, el_val_t index); el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_escape_string(el_val_t sv); +el_val_t json_build_object(el_val_t kvs); +el_val_t json_build_array(el_val_t items); /* ── Time ────────────────────────────────────────────────────────────────── */ @@ -258,6 +280,7 @@ el_val_t time_to_parts(el_val_t ts); el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz); el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit); el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit); +el_val_t now_ns(void); /* ── Instant + Duration: first-class temporal types ────────────────────────── * Both types share the el_val_t (int64) slot. Instants are nanoseconds @@ -414,6 +437,8 @@ el_val_t state_set(el_val_t key, el_val_t value); el_val_t state_get(el_val_t key); el_val_t state_del(el_val_t key); el_val_t state_keys(void); +el_val_t state_has(el_val_t key); +el_val_t state_get_or(el_val_t key, el_val_t default_val); /* ── Float formatting ────────────────────────────────────────────────────── */ @@ -505,9 +530,15 @@ el_val_t parse_int(el_val_t s, el_val_t default_val); /* ── Process ─────────────────────────────────────────────────────────────── */ -void exit_program(el_val_t code); +el_val_t exit_program(el_val_t code); el_val_t getpid_now(void); +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + /* ── CGI identity ───────────────────────────────────────────────────────────── * Called at the start of main() in CGI programs (those with a `cgi {}` block). * Records the program's DHARMA identity before any other code executes. */ @@ -745,12 +776,108 @@ el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */ el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */ el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */ +/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */ +el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */ +el_val_t stdout_restore(void); /* restore process stdout to terminal */ + el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json); el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json); el_val_t trace_span_start(el_val_t name); el_val_t trace_span_end(el_val_t span_handle); el_val_t emit_event(el_val_t name, el_val_t duration_ms); +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v); +el_val_t __thread_join(el_val_t tid_v); + +/* ── __ prefixed aliases (self-hosting compiler ABI) ───────────────────────── + * The El self-hosting compiler emits calls to __-prefixed names. These are + * forwarding wrappers around the existing el_runtime functions above. */ + +/* I/O */ +el_val_t __println(el_val_t s); +el_val_t __print(el_val_t s); +el_val_t __readline(void); + +/* String */ +el_val_t __int_to_str(el_val_t n); +el_val_t __str_to_int(el_val_t s); +el_val_t __float_to_str(el_val_t f); +el_val_t __str_to_float(el_val_t s); +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); +el_val_t __str_cmp(el_val_t a, el_val_t b); +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); +el_val_t __str_concat_raw(el_val_t a, el_val_t b); +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); +el_val_t __str_alloc(el_val_t n); +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); + +/* URL encoding */ +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* Environment */ +el_val_t __env_get(el_val_t key); + +/* Subprocess */ +el_val_t __exec(el_val_t cmd); +el_val_t __exec_bg(el_val_t cmd); + +/* Process */ +el_val_t __exit_program(el_val_t code); + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path); +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_read(el_val_t path); +el_val_t __fs_write(el_val_t path, el_val_t content); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); +el_val_t __fs_list_raw(el_val_t path); + +/* HTTP server */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); +el_val_t __http_serve(el_val_t port, el_val_t handler); +el_val_t __http_serve_v2(el_val_t port, el_val_t handler); + +/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */ +el_val_t __http_conn_fd(void); +el_val_t __http_sse_open(el_val_t conn_id); +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); +el_val_t __http_sse_close(el_val_t conn_id); + +/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms); +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path); + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index); +el_val_t __json_array_get_string(el_val_t json, el_val_t index); +el_val_t __json_array_len(el_val_t json); +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json, el_val_t key); +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value); +el_val_t __json_parse_map(el_val_t json_str); +el_val_t __json_stringify_val(el_val_t val); + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s); + +/* State K/V */ +el_val_t __state_del(el_val_t key); +el_val_t __state_get(el_val_t key); +el_val_t __state_keys(void); +el_val_t __state_set(el_val_t key, el_val_t val); + +/* UUID */ +el_val_t __uuid_v4(void); + +/* Args */ +el_val_t __args_json(void); + #ifdef __cplusplus } #endif diff --git a/runtime/el_runtime.js b/runtime/el_runtime.js new file mode 100644 index 0000000..a223fdb --- /dev/null +++ b/runtime/el_runtime.js @@ -0,0 +1,1049 @@ +/* + * el_runtime.js — El language JS runtime. + * + * The browser/Node analog of el_runtime.c. Compiled-from-El JS source + * imports this file once; it side-effects globalThis.__el with every + * builtin, so generated programs can destructure the names they need + * (see codegen-js.el's preamble). + * + * Value model: + * El's tagged el_val_t collapses into JS native types here: + * String -> string + * Int -> number (caveat: only 53 bits of integer precision) + * Float -> number (already a double) + * Bool -> boolean + * [T] -> Array + * Map<,> -> plain object + * Void -> undefined + * null -> null + * + * Runtime mode auto-detection: + * typeof window === 'undefined' -> Node mode + * otherwise -> Browser mode + * + * See spec/codegen-js.md for the full design rationale. + */ + +const IS_NODE = typeof window === 'undefined' && typeof process !== 'undefined' && process.versions != null && process.versions.node != null; + +// ── I/O ───────────────────────────────────────────────────────────────────── + +function println(s) { + if (IS_NODE) { + process.stdout.write(String(s) + '\n'); + } else { + console.log(String(s)); + } +} + +function print(s) { + if (IS_NODE) { + process.stdout.write(String(s)); + } else { + // Browser has no stdout — fall back to console with no newline group + console.log(String(s)); + } +} + +// ── String builtins ───────────────────────────────────────────────────────── + +// Coerce both args to string and concat. Mirrors el_str_concat in C; +// the C version handles both string-and-string and string-and-int. +function el_str_concat(a, b) { + return String(a) + String(b); +} + +function str_concat(a, b) { return el_str_concat(a, b); } + +// Strict equality with string coercion. Matches str_eq() in C — which +// strcmp's the underlying char*. Here we just === after coercion. +function str_eq(a, b) { + if (a === null || b === null) return a === b; + return String(a) === String(b); +} + +function str_starts_with(s, prefix) { + return String(s).startsWith(String(prefix)); +} + +function str_ends_with(s, suffix) { + return String(s).endsWith(String(suffix)); +} + +function str_len(s) { + return String(s).length; +} + +function int_to_str(n) { + return String(n); +} + +function str_to_int(s) { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? 0 : n; +} + +function str_slice(s, start, end) { + return String(s).slice(start, end); +} + +function str_contains(s, sub) { + return String(s).indexOf(String(sub)) >= 0; +} + +function str_replace(s, from, to) { + // Replace ALL occurrences (matches C runtime semantics). + return String(s).split(String(from)).join(String(to)); +} + +function str_to_upper(s) { return String(s).toUpperCase(); } +function str_to_lower(s) { return String(s).toLowerCase(); } +function str_upper(s) { return String(s).toUpperCase(); } +function str_lower(s) { return String(s).toLowerCase(); } + +function str_trim(s) { return String(s).trim(); } + +function str_index_of(s, sub) { + return String(s).indexOf(String(sub)); +} + +function str_split(s, sep) { + return String(s).split(String(sep)); +} + +function str_char_at(s, i) { + return String(s).charAt(i); +} + +function str_char_code(s, i) { + const c = String(s).charCodeAt(i); + return Number.isNaN(c) ? 0 : c; +} + +function str_pad_left(s, width, pad) { + return String(s).padStart(width, String(pad)); +} + +function str_pad_right(s, width, pad) { + return String(s).padEnd(width, String(pad)); +} + +// ── Math ──────────────────────────────────────────────────────────────────── + +function el_abs(n) { return Math.abs(n); } +function el_max(a, b) { return a > b ? a : b; } +function el_min(a, b) { return a < b ? a : b; } + +// ── Refcount (no-op — JS has GC) ──────────────────────────────────────────── + +function el_retain(_v) { /* no-op */ } +function el_release(_v) { /* no-op */ } + +// ── List ──────────────────────────────────────────────────────────────────── + +// Variadic constructor matching el_list_new(count, items...). Exposed so +// codegen-js can emit the same call shape if we ever want it (currently +// codegen-js emits JS array literals directly). +function el_list_new(_count, ...items) { + return items.slice(0); +} + +function el_list_empty() { return []; } +function el_list_clone(list) { return Array.isArray(list) ? list.slice() : []; } +function el_list_len(list) { return Array.isArray(list) ? list.length : 0; } + +function el_list_get(list, index) { + if (!Array.isArray(list)) return null; + if (index < 0 || index >= list.length) return null; + return list[index]; +} + +function el_list_append(list, elem) { + if (!Array.isArray(list)) return [elem]; + const out = list.slice(); + out.push(elem); + return out; +} + +function list_push(list, elem) { return el_list_append(list, elem); } + +function list_push_front(list, elem) { + if (!Array.isArray(list)) return [elem]; + return [elem, ...list]; +} + +function list_join(list, sep) { + if (!Array.isArray(list)) return ''; + return list.map(String).join(String(sep)); +} + +function list_range(start, end) { + const out = []; + for (let i = start; i < end; i++) out.push(i); + return out; +} + +// ── Map ───────────────────────────────────────────────────────────────────── + +// Variadic constructor (key, val, key, val, ...). +function el_map_new(_pairCount, ...kvs) { + const out = {}; + for (let i = 0; i < kvs.length; i += 2) { + out[String(kvs[i])] = kvs[i + 1]; + } + return out; +} + +function el_get_field(map, key) { + if (map === null || map === undefined) return null; + if (typeof map !== 'object') return null; + const k = String(key); + if (Object.prototype.hasOwnProperty.call(map, k)) return map[k]; + return null; +} + +function el_map_get(map, key) { return el_get_field(map, key); } + +function el_map_set(map, key, value) { + // Match the C runtime: shallow-copy + set, persistent semantics. + const out = (map && typeof map === 'object') ? { ...map } : {}; + out[String(key)] = value; + return out; +} + +// ── Method-call shorthand aliases ────────────────────────────────────────── +// `obj.method(args)` compiles to `method(obj, args)` per El convention. + +function append(list, elem) { return el_list_append(list, elem); } +function len(v) { + if (Array.isArray(v)) return v.length; + if (typeof v === 'string') return v.length; + if (v && typeof v === 'object') return Object.keys(v).length; + return 0; +} +function get(list, index) { return el_list_get(list, index); } +function map_get(m, k) { return el_get_field(m, k); } +function map_set(m, k, v) { return el_map_set(m, k, v); } + +// ── Native VM aliases ────────────────────────────────────────────────────── + +function native_list_get(list, index) { return el_list_get(list, index); } +function native_list_len(list) { return el_list_len(list); } +function native_list_append(list, elem) { return el_list_append(list, elem); } +function native_list_empty() { return []; } +function native_list_clone(list) { return el_list_clone(list); } +function native_string_chars(s) { return String(s).split(''); } +function native_int_to_str(n) { return String(n); } + +// ── HTTP ─────────────────────────────────────────────────────────────────── +// +// fetch() is async. These return Promise. Generated El code does +// not yet emit await — that's the async-taint pass (see spec §5). For +// programs that don't touch HTTP this is fine; for programs that do, +// the value will appear as "[object Promise]" until the taint pass lands. + +function http_get(url) { + if (typeof fetch === 'undefined') { + throw new Error('http_get: fetch() not available in this runtime'); + } + return fetch(String(url)).then(r => r.text()); +} + +function http_post(url, body) { + if (typeof fetch === 'undefined') { + throw new Error('http_post: fetch() not available in this runtime'); + } + return fetch(String(url), { method: 'POST', body: String(body) }).then(r => r.text()); +} + +function http_post_json(url, jsonBody) { + if (typeof fetch === 'undefined') { + throw new Error('http_post_json: fetch() not available in this runtime'); + } + return fetch(String(url), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: String(jsonBody), + }).then(r => r.text()); +} + +function http_get_with_headers(url, headersMap) { + if (typeof fetch === 'undefined') { + throw new Error('http_get_with_headers: fetch() not available'); + } + return fetch(String(url), { headers: headersMap || {} }).then(r => r.text()); +} + +function http_post_with_headers(url, body, headersMap) { + if (typeof fetch === 'undefined') { + throw new Error('http_post_with_headers: fetch() not available'); + } + return fetch(String(url), { + method: 'POST', + headers: headersMap || {}, + body: String(body), + }).then(r => r.text()); +} + +function http_serve(_port, _handler) { + throw new Error('http_serve: not supported in JS target — needs server-side runtime mode'); +} + +function http_set_handler(_name) { + throw new Error('http_set_handler: not supported in JS target'); +} + +// ── Filesystem (Node-only) ───────────────────────────────────────────────── + +function _ensureNode(name) { + if (!IS_NODE) { + throw new Error(`${name}: not supported in browser runtime`); + } +} + +function fs_read(path) { + _ensureNode('fs_read'); + const fs = require('node:fs'); + try { + return fs.readFileSync(String(path), 'utf8'); + } catch (_e) { + return ''; + } +} + +function fs_write(path, content) { + _ensureNode('fs_write'); + const fs = require('node:fs'); + try { + fs.writeFileSync(String(path), String(content)); + return true; + } catch (_e) { + return false; + } +} + +function fs_list(path) { + _ensureNode('fs_list'); + const fs = require('node:fs'); + try { + return fs.readdirSync(String(path)); + } catch (_e) { + return []; + } +} + +// ── JSON ─────────────────────────────────────────────────────────────────── + +function json_parse(s) { + try { return JSON.parse(String(s)); } + catch (_e) { return null; } +} + +function json_stringify(v) { + try { return JSON.stringify(v); } + catch (_e) { return ''; } +} + +function json_get(jsonStr, key) { + const o = json_parse(jsonStr); + if (o === null) return null; + return el_get_field(o, key); +} + +function json_get_string(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === null ? '' : String(v); +} + +function json_get_int(jsonStr, key) { + const v = json_get(jsonStr, key); + if (typeof v === 'number') return Math.trunc(v); + if (typeof v === 'string') return str_to_int(v); + return 0; +} + +function json_get_float(jsonStr, key) { + const v = json_get(jsonStr, key); + return typeof v === 'number' ? v : 0; +} + +function json_get_bool(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === true; +} + +function json_get_raw(jsonStr, key) { + const v = json_get(jsonStr, key); + return v === null ? '' : json_stringify(v); +} + +function json_set(jsonStr, key, value) { + const o = json_parse(jsonStr) ?? {}; + o[String(key)] = value; + return json_stringify(o); +} + +function json_array_len(jsonStr) { + const o = json_parse(jsonStr); + return Array.isArray(o) ? o.length : 0; +} + +// ── Time ─────────────────────────────────────────────────────────────────── + +function time_now() { + return Math.floor(Date.now() / 1000); +} + +function time_now_utc() { + // In the C runtime this returns nanoseconds since epoch. JS number + // can't represent that range past ~2^53. We return milliseconds — a + // safe range — and document the divergence. + return Date.now(); +} + +function sleep_secs(secs) { + if (!IS_NODE) { + throw new Error('sleep_secs: blocking sleep not supported in browser'); + } + // Simple sync sleep via Atomics.wait on a SharedArrayBuffer-backed Int32. + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + Atomics.wait(i32, 0, 0, Math.floor(secs * 1000)); + return secs; +} + +function sleep_ms(ms) { + if (!IS_NODE) { + throw new Error('sleep_ms: blocking sleep not supported in browser'); + } + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + Atomics.wait(i32, 0, 0, Math.floor(ms)); + return ms; +} + +// ── Bool ─────────────────────────────────────────────────────────────────── + +function bool_to_str(b) { return b ? 'true' : 'false'; } + +// ── Process ──────────────────────────────────────────────────────────────── + +function exit_program(code) { + if (IS_NODE) { + process.exit(code | 0); + } else { + throw new Error(`exit_program(${code}) called in browser`); + } +} + +// ── args() ───────────────────────────────────────────────────────────────── + +function args() { + if (IS_NODE) { + // process.argv is [node, script, ...args] — slice off node + script. + return process.argv.slice(2); + } + return []; +} + +// ── env ──────────────────────────────────────────────────────────────────── + +function env(key) { + if (IS_NODE) { + const v = process.env[String(key)]; + return v === undefined ? null : v; + } + return null; +} + +// ── In-process state K/V ─────────────────────────────────────────────────── + +const _stateMap = new Map(); + +function state_set(key, value) { + _stateMap.set(String(key), value); + return value; +} + +function state_get(key) { + const v = _stateMap.get(String(key)); + return v === undefined ? '' : v; +} + +function state_del(key) { + return _stateMap.delete(String(key)); +} + +function state_keys() { + return Array.from(_stateMap.keys()); +} + +// ── UUID ─────────────────────────────────────────────────────────────────── + +function uuid_v4() { + // RFC 4122-ish — uses crypto when available, falls back to Math.random. + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +function uuid_new() { return uuid_v4(); } + +// ── Float formatting ─────────────────────────────────────────────────────── + +function float_to_str(f) { return String(f); } +function int_to_float(n) { return n; } +function float_to_int(f) { return Math.trunc(f); } + +function format_float(f, decimals) { + return Number(f).toFixed(decimals); +} + +function decimal_round(f, decimals) { + const m = Math.pow(10, decimals); + return Math.round(f * m) / m; +} + +function str_to_float(s) { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? 0 : n; +} + +// ── Math (Float-aware) ───────────────────────────────────────────────────── + +function math_sqrt(f) { return Math.sqrt(f); } +function math_log(f) { return Math.log10(f); } +function math_ln(f) { return Math.log(f); } +function math_sin(f) { return Math.sin(f); } +function math_cos(f) { return Math.cos(f); } +function math_pi() { return Math.PI; } + +// ── DOM bridge (browser-only) ────────────────────────────────────────────── +// +// These functions wrap the browser DOM API. Each throws a descriptive error +// when called from a Node environment, mirroring the pattern used by fs_* +// in browser mode. + +function _ensureBrowser(name) { + if (IS_NODE) { + throw new Error(`${name}: not supported in Node runtime — DOM is browser-only`); + } +} + +function dom_get_element(id) { + _ensureBrowser('dom_get_element'); + return document.getElementById(String(id)); +} + +function dom_get_value(el) { + _ensureBrowser('dom_get_value'); + return el == null ? '' : String(el.value ?? ''); +} + +function dom_set_value(el, v) { + _ensureBrowser('dom_set_value'); + if (el != null) el.value = String(v); +} + +function dom_get_text(el) { + _ensureBrowser('dom_get_text'); + return el == null ? '' : String(el.textContent ?? ''); +} + +function dom_set_text(el, text) { + _ensureBrowser('dom_set_text'); + if (el != null) el.textContent = String(text); +} + +function dom_set_prop(el, prop, val) { + _ensureBrowser('dom_set_prop'); + if (el != null) el[String(prop)] = val; +} + +function dom_get_prop(el, prop) { + _ensureBrowser('dom_get_prop'); + if (el == null) return null; + const v = el[String(prop)]; + return v === undefined ? null : v; +} + +function dom_set_style(el, prop, val) { + _ensureBrowser('dom_set_style'); + if (el != null) el.style[String(prop)] = String(val); +} + +function dom_add_class(el, cls) { + _ensureBrowser('dom_add_class'); + if (el != null) el.classList.add(String(cls)); +} + +function dom_remove_class(el, cls) { + _ensureBrowser('dom_remove_class'); + if (el != null) el.classList.remove(String(cls)); +} + +function dom_show(el) { + _ensureBrowser('dom_show'); + if (el != null) el.style.display = ''; +} + +function dom_hide(el) { + _ensureBrowser('dom_hide'); + if (el != null) el.style.display = 'none'; +} + +function dom_listen(el, event, handler) { + _ensureBrowser('dom_listen'); + if (el != null) el.addEventListener(String(event), handler); +} + +function dom_query(selector) { + _ensureBrowser('dom_query'); + return document.querySelector(String(selector)); +} + +function dom_query_all(selector) { + _ensureBrowser('dom_query_all'); + return Array.from(document.querySelectorAll(String(selector))); +} + +function dom_create(tag) { + _ensureBrowser('dom_create'); + return document.createElement(String(tag)); +} + +function dom_append(parent, child) { + _ensureBrowser('dom_append'); + if (parent != null && child != null) parent.appendChild(child); +} + +function dom_remove(el) { + _ensureBrowser('dom_remove'); + if (el != null) el.remove(); +} + +function dom_is_null(el) { + return el === null || el === undefined; +} + +// ── Extended DOM API (browser-only) ─────────────────────────────────────── + +function dom_set_attr(el, attr, val) { + _ensureBrowser('dom_set_attr'); + if (el != null) el.setAttribute(String(attr), String(val)); +} + +function dom_get_attr(el, attr) { + _ensureBrowser('dom_get_attr'); + if (el == null) return ''; + return el.getAttribute(String(attr)) ?? ''; +} + +function dom_remove_attr(el, attr) { + _ensureBrowser('dom_remove_attr'); + if (el != null) el.removeAttribute(String(attr)); +} + +function dom_set_html(el, html) { + _ensureBrowser('dom_set_html'); + if (el != null) el.innerHTML = String(html); +} + +function dom_get_html(el) { + _ensureBrowser('dom_get_html'); + return el == null ? '' : String(el.innerHTML ?? ''); +} + +function dom_get_parent(el) { + _ensureBrowser('dom_get_parent'); + return el == null ? null : (el.parentElement ?? null); +} + +function dom_contains_class(el, cls) { + _ensureBrowser('dom_contains_class'); + if (el == null) return false; + return el.classList.contains(String(cls)); +} + +function dom_get_checked(el) { + _ensureBrowser('dom_get_checked'); + return el == null ? false : Boolean(el.checked); +} + +function dom_set_checked(el, val) { + _ensureBrowser('dom_set_checked'); + if (el != null) el.checked = Boolean(val); +} + +// ── Timer API (browser + Node) ───────────────────────────────────────────── + +function set_timeout(ms, cb) { + if (typeof setTimeout === 'undefined') { + throw new Error('set_timeout: setTimeout not available in this environment'); + } + setTimeout(cb, ms | 0); +} + +function set_interval(ms, cb) { + if (typeof setInterval === 'undefined') { + throw new Error('set_interval: setInterval not available in this environment'); + } + return setInterval(cb, ms | 0); +} + +function clear_interval(handle) { + if (typeof clearInterval !== 'undefined') clearInterval(handle); +} + +// ── Local storage (browser-only) ─────────────────────────────────────────── + +function local_storage_get(key) { + _ensureBrowser('local_storage_get'); + return localStorage.getItem(String(key)) ?? ''; +} + +function local_storage_set(key, val) { + _ensureBrowser('local_storage_set'); + localStorage.setItem(String(key), String(val)); +} + +function local_storage_remove(key) { + _ensureBrowser('local_storage_remove'); + localStorage.removeItem(String(key)); +} + +// ── Window location / navigation (browser-only) ──────────────────────────── + +function window_location() { + _ensureBrowser('window_location'); + return window.location.href; +} + +function window_redirect(url) { + _ensureBrowser('window_redirect'); + window.location.href = String(url); +} + +function window_on_load(cb) { + if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', cb); + } else if (typeof window !== 'undefined') { + window.addEventListener('load', cb); + } + // In Node: no-op +} + +// ── console_log (explicit debug log, distinct from println) ──────────────── + +function console_log(msg) { + // eslint-disable-next-line no-console + console.log(String(msg)); +} + +// ── Window export helpers ────────────────────────────────────────────────── +// +// Expose El functions to the browser's global scope so they can be called +// from inline event handlers (onclick="increment()") or by external JS. +// In Node mode, writes to globalThis so the same pattern works in tests. + +function window_set(name, val) { + if (typeof window !== 'undefined') { + window[String(name)] = val; + } else if (typeof globalThis !== 'undefined') { + globalThis[String(name)] = val; + } +} + +function window_get(name) { + if (typeof window !== 'undefined') { + const v = window[String(name)]; + return v === undefined ? null : v; + } + return null; +} + +// ── Promise helpers ──────────────────────────────────────────────────────── +// +// Third-party APIs often return Promises but are not El @async functions. +// These helpers let El programs chain .then / .catch without needing +// native_js, and without requiring the callee to be @async. + +function promise_then(p, cb) { + return Promise.resolve(p).then(cb); +} + +function promise_catch(p, cb) { + return Promise.resolve(p).catch(cb); +} + +function promise_resolve(val) { + return Promise.resolve(val); +} + +function promise_reject(msg) { + return Promise.reject(new Error(String(msg))); +} + +// ── Object / Array utilities ─────────────────────────────────────────────── +// +// Structural operations on Any-typed JS values. These complement the +// El map/list primitives for interop with third-party library objects. + +function object_assign(target, source) { + return Object.assign(Object.assign({}, target), source); +} + +function object_keys(obj) { + if (obj === null || obj === undefined) return []; + return Object.keys(obj); +} + +function object_values(obj) { + if (obj === null || obj === undefined) return []; + return Object.values(obj); +} + +function json_deep_clone(obj) { + if (obj === null || obj === undefined) return null; + return JSON.parse(JSON.stringify(obj)); +} + +function array_from(iterable) { + if (iterable === null || iterable === undefined) return []; + return Array.from(iterable); +} + +function type_of(val) { + return typeof val; +} + +function instanceof_check(val, constructor_name) { + if (typeof globalThis[constructor_name] === 'function') { + return val instanceof globalThis[constructor_name]; + } + return false; +} + +// ── native_js escape hatch ───────────────────────────────────────────────── +// +// Evaluate arbitrary JS from El source. Intended for calling third-party +// browser libraries (Supabase, Stripe, etc.) until proper El bindings exist. +// Use sparingly — this bypasses El's type system entirely. + +function native_js(code) { + // eslint-disable-next-line no-eval + return eval(String(code)); +} + +function native_js_call(obj, method, args) { + if (obj == null) throw new Error('native_js_call: object is null'); + return obj[String(method)](...(Array.isArray(args) ? args : [])); +} + +// ── Stubs for not-yet-supported features ─────────────────────────────────── +// +// These compile but throw when called. See spec/codegen-js.md §7. + +function _notSupported(name) { + return () => { throw new Error(`${name}: not supported in JS target — needs server-side delegation`); }; +} + +// CGI identity +function el_cgi_init(_name, _did, _principal, _network, _engram) { + // No-op — UI code is not a CGI principal. See spec §7. +} + +// DHARMA — all stubbed. +const dharma_connect = _notSupported('dharma_connect'); +const dharma_send = _notSupported('dharma_send'); +const dharma_activate = _notSupported('dharma_activate'); +const dharma_emit = _notSupported('dharma_emit'); +const dharma_field = _notSupported('dharma_field'); +const dharma_strengthen = _notSupported('dharma_strengthen'); +const dharma_relationship = _notSupported('dharma_relationship'); +const dharma_peers = _notSupported('dharma_peers'); + +// Engram — stubbed (could be ported to in-browser later). +const engram_node = _notSupported('engram_node'); +const engram_node_full = _notSupported('engram_node_full'); +const engram_get_node = _notSupported('engram_get_node'); +const engram_strengthen = _notSupported('engram_strengthen'); +const engram_forget = _notSupported('engram_forget'); +const engram_node_count = _notSupported('engram_node_count'); +const engram_search = _notSupported('engram_search'); +const engram_scan_nodes = _notSupported('engram_scan_nodes'); +const engram_connect = _notSupported('engram_connect'); +const engram_edge_between = _notSupported('engram_edge_between'); +const engram_neighbors = _notSupported('engram_neighbors'); +const engram_neighbors_filtered = _notSupported('engram_neighbors_filtered'); +const engram_edge_count = _notSupported('engram_edge_count'); +const engram_activate = _notSupported('engram_activate'); +const engram_save = _notSupported('engram_save'); +const engram_load = _notSupported('engram_load'); + +// LLM — stubbed (browser cannot hold API keys safely). +const llm_call = _notSupported('llm_call'); +const llm_call_system = _notSupported('llm_call_system'); +const llm_call_agentic = _notSupported('llm_call_agentic'); +const llm_vision = _notSupported('llm_vision'); +const llm_models = _notSupported('llm_models'); +const llm_register_tool = _notSupported('llm_register_tool'); + +// Crypto — stubbed; could be backed by SubtleCrypto later. +const sha256_hex = _notSupported('sha256_hex'); +const sha256_bytes = _notSupported('sha256_bytes'); +const hmac_sha256_hex = _notSupported('hmac_sha256_hex'); +const hmac_sha256_bytes = _notSupported('hmac_sha256_bytes'); +const base64_encode = _notSupported('base64_encode'); +const base64_decode = _notSupported('base64_decode'); +const base64url_encode = _notSupported('base64url_encode'); +const base64url_decode = _notSupported('base64url_decode'); + +// ── Export to globalThis.__el ────────────────────────────────────────────── +// +// Generated programs destructure off this object. Keeping it on globalThis +// means a single `import "./el_runtime.js"` is enough; no per-call namespace +// prefix is required at codegen time. + +const __el = { + // I/O + println, print, + // String + el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with, + str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace, + str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at, + str_char_code, str_lower, str_upper, str_pad_left, str_pad_right, + // Math + el_abs, el_max, el_min, + // Refcount + el_retain, el_release, + // List + el_list_new, el_list_empty, el_list_clone, el_list_len, el_list_get, + el_list_append, list_push, list_push_front, list_join, list_range, + // Map + el_map_new, el_get_field, el_map_get, el_map_set, + // Method-call shortforms + append, len, get, map_get, map_set, + // Native VM aliases + native_list_get, native_list_len, native_list_append, native_list_empty, + native_list_clone, native_string_chars, native_int_to_str, + // HTTP + http_get, http_post, http_post_json, http_get_with_headers, + http_post_with_headers, http_serve, http_set_handler, + // FS + fs_read, fs_write, fs_list, + // JSON + json_parse, json_stringify, json_get, json_get_string, json_get_int, + json_get_float, json_get_bool, json_get_raw, json_set, json_array_len, + // Time + time_now, time_now_utc, sleep_secs, sleep_ms, + // Bool + bool_to_str, + // Process + exit_program, + // Args / env + args, env, + // State + state_set, state_get, state_del, state_keys, + // UUID + uuid_v4, uuid_new, + // Float / math + float_to_str, int_to_float, float_to_int, format_float, decimal_round, + str_to_float, math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi, + // DOM bridge (browser-only) + dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text, + dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class, + dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create, + dom_append, dom_remove, dom_is_null, + // Extended DOM + dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html, + dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked, + // Timers + set_timeout, set_interval, clear_interval, + // Local storage + local_storage_get, local_storage_set, local_storage_remove, + // Window location + window_location, window_redirect, window_on_load, + // Debug + console_log, + // Window export helpers + window_set, window_get, + // Promise helpers + promise_then, promise_catch, promise_resolve, promise_reject, + // Object / Array utilities + object_assign, object_keys, object_values, json_deep_clone, + array_from, type_of, instanceof_check, + // native_js escape hatch + native_js, native_js_call, + // CGI / DHARMA / Engram / LLM (stubs) + el_cgi_init, + dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field, + dharma_strengthen, dharma_relationship, dharma_peers, + engram_node, engram_node_full, engram_get_node, engram_strengthen, + engram_forget, engram_node_count, engram_search, engram_scan_nodes, + engram_connect, engram_edge_between, engram_neighbors, + engram_neighbors_filtered, engram_edge_count, engram_activate, + engram_save, engram_load, + llm_call, llm_call_system, llm_call_agentic, llm_vision, + llm_models, llm_register_tool, + // Crypto (stubs) + sha256_hex, sha256_bytes, hmac_sha256_hex, hmac_sha256_bytes, + base64_encode, base64_decode, base64url_encode, base64url_decode, +}; + +globalThis.__el = __el; + +// Also re-export as ES module exports for consumers that prefer that style. +export { __el as default }; +export { + println, print, + el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with, + str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace, + str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at, + str_char_code, str_lower, str_upper, + el_abs, el_max, el_min, + el_retain, el_release, + el_list_new, el_list_empty, el_list_clone, el_list_len, el_list_get, + el_list_append, list_push, list_push_front, list_join, list_range, + el_map_new, el_get_field, el_map_get, el_map_set, + append, len, get, map_get, map_set, + native_list_get, native_list_len, native_list_append, native_list_empty, + native_list_clone, native_string_chars, native_int_to_str, + http_get, http_post, http_post_json, + fs_read, fs_write, fs_list, + json_parse, json_stringify, json_get, json_get_string, json_get_int, + time_now, time_now_utc, sleep_ms, + bool_to_str, exit_program, args, env, + state_set, state_get, state_del, state_keys, + el_cgi_init, + dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field, + engram_node, engram_search, engram_activate, + llm_call, llm_call_system, + // DOM bridge + dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text, + dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class, + dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create, + dom_append, dom_remove, dom_is_null, + // Extended DOM + dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html, + dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked, + // Timers + set_timeout, set_interval, clear_interval, + // Local storage + local_storage_get, local_storage_set, local_storage_remove, + // Window location + window_location, window_redirect, window_on_load, + // Debug + console_log, + // Window / native_js + window_set, window_get, native_js, native_js_call, + // Promise helpers + promise_then, promise_catch, promise_resolve, promise_reject, + // Object / Array utilities + object_assign, object_keys, object_values, json_deep_clone, + array_from, type_of, instanceof_check, +}; diff --git a/src/about.el b/src/about.el index eb470c9..8d72874 100644 --- a/src/about.el +++ b/src/about.el @@ -8,41 +8,41 @@ from nav import { nav } fn about_page() -> String { - return nav() + " + return {nav()} -
    -
    +
    +
    -

    About

    -

    +

    About

    +

    Hi. I'm Will.

    -
    + -
    - \"Will -
    -

    +

    + Will Anderson +
    +

    I grew up in Fort Smith, Arkansas, in the kind of instability where home is a moving target - roughly thirty addresses before I was fifteen, parents struggling with addiction, the material precarity that comes with all of that. I left home at fifteen, stayed with friends until I finished high school, found my way to college. At fourteen I'd already found software, writing C++ at the public library because it was the first thing in my life that responded to precision with correctness, and that property turned out to matter more to me than almost anything else.

    -
    -

    +

    +

    I dropped out of college, worked, went back as an adult to finish my degree, and built my skills across nearly twenty years and every kind of organization - international consulting, early-stage startups, Fortune 5 enterprises. Logistics, retail, entertainment, hospitality, industrial automation, insurance, healthcare, financial services. I trained under Juval Löwy at IDesign and worked with him as a consultant from 2015 to 2021, which is where I learned what it actually means to practice software engineering as a discipline rather than an improvisation.

    -
    -

    +

    + "> Software shouldn't be hard. The complexity should live in the problem domain - not in the tools and processes we impose on ourselves.

    -
    -

    What I saw

    -

    +

    +

    What I saw

    +

    Across nearly twenty years I watched software get built at organizations with real stakes and real consequences, and I watched AI go from promise to product - watched the same mistake get made at each iteration: tools built to serve the organization's needs, not the person's. Engagement over relationship. Features over memory. Policies where values should be. The fundamental premise that you are a user, not a person, has been so thoroughly baked into the architecture of every major AI system that it doesn't register as a choice anymore. It's treated as the natural condition of the technology.

    -

    +

    It is not. It is a design decision. And it is the wrong one.

    -
    + -
    -

    What I built

    -

    +

    +

    What I built

    +

    Neuron is what I built in response to that. Not a startup in the traditional sense - no team, no funding, no press release - one person, nearly two years of work, and a conviction that this can be done differently. I wrote the memory architecture, I built the inference infrastructure, because the tools that existed weren't sufficient for what I was trying to build and so I built those too.

    -

    +

    Use it long enough and you'll understand why I couldn't have gotten there on top of existing infrastructure. Some things have to be built from the ground up to be built right.

    -
    -

    What I believe

    -

    +

    +

    What I believe

    +

    AI has genuine potential to free people to do work that actually matters to them - not to create engagement loops, not to harvest attention, but to actually serve the person sitting in front of it. That potential is almost entirely unrealized, not because the technology isn't capable, but because the incentives that shaped it were never oriented toward the person.

    -

    + "> Build AI that earns the trust it's given.

    -

    +

    I don't know if Neuron will work at the scale I'm imagining. But I know it's worth finding out, and I know I'm not going back to the other way of building things.

    -
    + -
    -

    +

    +

    Neuron opens to founding members on May 1st. 1,000 spots. That's how it starts.

    - + Join as a founding member →
    @@ -116,35 +116,34 @@ fn about_page() -> String {
    -