From 58753a88d734b0e099a0b3c0f90370106b2c1a05 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 29 Jun 2026 12:40:37 -0500 Subject: [PATCH] feat(ui): native vessel, HTML vessel update, native hello examples, profile card, UI tools el-native vessel: El-level wrappers around __widget_* C builtins, exposing vstack, label, button, text_field, etc. as clean El functions for application code. el-html/main.elh: updated extern declarations for the HTML vessel's codegen API. native-hello: cross-platform desktop example (AppKit/GTK4/Win32/SDL2) with build scripts, Dockerfiles for Linux/Pi, and Win32 cross-compile support. native-hello-android: Gradle project with ElBridge integration and build script. native-hello-ios: Xcode project for the iOS UIKit target. profile-card: manifest.el for a styling/layout/i18n example app that exercises el-style, el-layout, el-i18n, el-config, and el-secrets vessels. ui/tools/native-codegen: Python codegen pass (el_ui_native_codegen.py) that lowers el-ui component DSL to el-native vessel calls, plus build script and test fixtures. --- ui/examples/native-hello-android/.gitignore | 6 + .../native-hello-android/app/build.gradle | 55 + .../app/proguard-rules.pro | 4 + .../app/src/main/AndroidManifest.xml | 23 + .../src/main/java/com/neuron/el/ElApp.java | 18 + .../src/main/java/com/neuron/el/ElBridge.java | 711 + .../main/java/com/neuron/el/MainActivity.java | 38 + .../app/src/main/jni/CMakeLists.txt | 57 + .../app/src/main/jni/el_android.c | 964 ++ .../app/src/main/jni/el_native_target.h | 573 + .../app/src/main/jni/el_native_vessel.c | 459 + .../app/src/main/jni/el_runtime.c | 11733 ++++++++++++++++ .../app/src/main/jni/el_runtime.h | 883 ++ .../app/src/main/jni/el_seed.c | 1942 +++ .../app/src/main/jni/native_hello.c | 123 + .../app/src/main/res/layout/activity_main.xml | 13 + .../app/src/main/res/values/strings.xml | 4 + ui/examples/native-hello-android/build.gradle | 4 + ui/examples/native-hello-android/build.sh | 83 + .../native-hello-android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + ui/examples/native-hello-android/gradlew | 122 + .../local.properties.template | 7 + .../native-hello-android/settings.gradle | 17 + ui/examples/native-hello-ios/.gitignore | 4 + .../NativeHello.xcodeproj/project.pbxproj | 377 + .../xcschemes/NativeHello.xcscheme | 78 + .../native-hello-ios/NativeHello/Info.plist | 38 + .../NativeHello/el_native_target.h | 573 + .../NativeHello/el_native_vessel.c | 459 + .../native-hello-ios/NativeHello/el_runtime.c | 11733 ++++++++++++++++ .../native-hello-ios/NativeHello/el_runtime.h | 883 ++ .../native-hello-ios/NativeHello/el_seed.c | 1942 +++ .../native-hello-ios/NativeHello/el_uikit.m | 1118 ++ .../native-hello-ios/NativeHello/main.m | 48 + .../NativeHello/native_hello.c | 123 + ui/examples/native-hello-ios/build.sh | 136 + ui/examples/native-hello/-o | 0 .../native-hello/Dockerfile.linux-gtk4 | 88 + ui/examples/native-hello/Dockerfile.pi | 99 + .../build-docker/el_native_vessel.c | 459 + .../build-docker/el_native_vessel.h | 69 + .../native-hello/build-docker/native_hello.c | 123 + .../build-docker/runtime/el_gtk4.c | 1027 ++ .../build-docker/runtime/el_native_target.h | 573 + .../build-docker/runtime/el_runtime.c | 11733 ++++++++++++++++ .../build-docker/runtime/el_runtime.h | 883 ++ .../build-docker/runtime/el_seed.c | 1942 +++ .../build-docker/runtime/el_seed.h | 324 + ui/examples/native-hello/build-gtk4.sh | 182 + ui/examples/native-hello/build-gtk4/.rt_T.txt | 397 + .../native-hello/build-gtk4/.rt_keep.txt | 340 + .../native-hello/build-gtk4/.seed_T.txt | 137 + .../native-hello/build-gtk4/.vessel_keep.txt | 55 + ui/examples/native-hello/build-gtk4/el_gtk4.o | Bin 0 -> 33464 bytes .../build-gtk4/el_native_vessel.c | 459 + .../build-gtk4/el_native_vessel.h | 69 + .../build-gtk4/el_native_vessel.o | Bin 0 -> 14692 bytes .../native-hello/build-gtk4/el_runtime.o | Bin 0 -> 244380 bytes ui/examples/native-hello/build-gtk4/el_seed.o | Bin 0 -> 50056 bytes .../native-hello/build-gtk4/native-hello-gtk4 | Bin 0 -> 303136 bytes .../native-hello/build-gtk4/native_hello.c | 123 + .../native-hello/build-gtk4/native_hello.o | Bin 0 -> 6192 bytes ui/examples/native-hello/build-sdl2.sh | 169 + ui/examples/native-hello/build-win32.sh | 135 + .../build-win32/el_native_vessel.c | 459 + .../build-win32/el_native_vessel.h | 69 + .../build-win32/el_native_vessel.o | Bin 0 -> 14736 bytes .../build-win32/el_runtime_win32.o | Bin 0 -> 43103 bytes .../native-hello/build-win32/el_win32.o | Bin 0 -> 31904 bytes .../native-hello/build-win32/native-hello.exe | Bin 0 -> 246847 bytes .../native-hello/build-win32/native_hello.c | 123 + .../native-hello/build-win32/native_hello.o | Bin 0 -> 5580 bytes ui/examples/native-hello/build.sh | 198 + ui/examples/native-hello/build/.rt_T.txt | 397 + ui/examples/native-hello/build/.rt_keep.txt | 340 + ui/examples/native-hello/build/.seed_T.txt | 137 + .../native-hello/build/.vessel_keep.txt | 55 + ui/examples/native-hello/build/el_appkit.o | Bin 0 -> 58520 bytes .../native-hello/build/el_native_vessel.c | 459 + .../native-hello/build/el_native_vessel.h | 69 + .../native-hello/build/el_native_vessel.o | Bin 0 -> 14692 bytes ui/examples/native-hello/build/el_runtime.o | Bin 0 -> 244380 bytes ui/examples/native-hello/build/el_sdl2.o | Bin 0 -> 31624 bytes ui/examples/native-hello/build/el_seed.o | Bin 0 -> 50056 bytes ui/examples/native-hello/build/native-hello | Bin 0 -> 324000 bytes .../build/native-hello-linux-arm64 | Bin 0 -> 300664 bytes .../build/native-hello-linux-x86_64 | Bin 0 -> 299048 bytes .../native-hello/build/native-hello-pi-arm64 | Bin 0 -> 300664 bytes .../native-hello/build/native-hello-sdl2 | Bin 0 -> 297632 bytes ui/examples/native-hello/build/native_hello.c | 123 + ui/examples/native-hello/build/native_hello.o | Bin 0 -> 6192 bytes .../native-hello/docker-build-linux.sh | 128 + ui/examples/native-hello/manifest.el | 19 + ui/examples/native-hello/src/App.el | 141 + ui/examples/native-hello/src/main.el | 43 + ui/examples/profile-card/manifest.el | 25 + ui/tools/dist/el-ui-native-codegen | 4 + ui/tools/native-codegen/build.sh | 62 + .../native-codegen/el_ui_native_codegen.py | 822 ++ ui/tools/native-codegen/test_app.el | 28 + ui/tools/native-codegen/test_native_hello.el | 52 + ui/vessels/el-html/src/main.elh | 42 + ui/vessels/el-native/src/main.el | 386 + ui/vessels/el-native/src/main.elh | 57 + 106 files changed, 58984 insertions(+) create mode 100644 ui/examples/native-hello-android/.gitignore create mode 100644 ui/examples/native-hello-android/app/build.gradle create mode 100644 ui/examples/native-hello-android/app/proguard-rules.pro create mode 100644 ui/examples/native-hello-android/app/src/main/AndroidManifest.xml create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java create mode 100644 ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_android.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_native_target.h create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_runtime.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_runtime.h create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_seed.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/native_hello.c create mode 100644 ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml create mode 100644 ui/examples/native-hello-android/app/src/main/res/values/strings.xml create mode 100644 ui/examples/native-hello-android/build.gradle create mode 100755 ui/examples/native-hello-android/build.sh create mode 100644 ui/examples/native-hello-android/gradle.properties create mode 100644 ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.properties create mode 100755 ui/examples/native-hello-android/gradlew create mode 100644 ui/examples/native-hello-android/local.properties.template create mode 100644 ui/examples/native-hello-android/settings.gradle create mode 100644 ui/examples/native-hello-ios/.gitignore create mode 100644 ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj create mode 100644 ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme create mode 100644 ui/examples/native-hello-ios/NativeHello/Info.plist create mode 100644 ui/examples/native-hello-ios/NativeHello/el_native_target.h create mode 100644 ui/examples/native-hello-ios/NativeHello/el_native_vessel.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_runtime.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_runtime.h create mode 100644 ui/examples/native-hello-ios/NativeHello/el_seed.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_uikit.m create mode 100644 ui/examples/native-hello-ios/NativeHello/main.m create mode 100644 ui/examples/native-hello-ios/NativeHello/native_hello.c create mode 100755 ui/examples/native-hello-ios/build.sh create mode 100644 ui/examples/native-hello/-o create mode 100644 ui/examples/native-hello/Dockerfile.linux-gtk4 create mode 100644 ui/examples/native-hello/Dockerfile.pi create mode 100644 ui/examples/native-hello/build-docker/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-docker/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-docker/native_hello.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_gtk4.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_native_target.h create mode 100644 ui/examples/native-hello/build-docker/runtime/el_runtime.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_runtime.h create mode 100644 ui/examples/native-hello/build-docker/runtime/el_seed.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_seed.h create mode 100755 ui/examples/native-hello/build-gtk4.sh create mode 100644 ui/examples/native-hello/build-gtk4/.rt_T.txt create mode 100644 ui/examples/native-hello/build-gtk4/.rt_keep.txt create mode 100644 ui/examples/native-hello/build-gtk4/.seed_T.txt create mode 100644 ui/examples/native-hello/build-gtk4/.vessel_keep.txt create mode 100644 ui/examples/native-hello/build-gtk4/el_gtk4.o create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.o create mode 100644 ui/examples/native-hello/build-gtk4/el_runtime.o create mode 100644 ui/examples/native-hello/build-gtk4/el_seed.o create mode 100755 ui/examples/native-hello/build-gtk4/native-hello-gtk4 create mode 100644 ui/examples/native-hello/build-gtk4/native_hello.c create mode 100644 ui/examples/native-hello/build-gtk4/native_hello.o create mode 100755 ui/examples/native-hello/build-sdl2.sh create mode 100755 ui/examples/native-hello/build-win32.sh create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.o create mode 100644 ui/examples/native-hello/build-win32/el_runtime_win32.o create mode 100644 ui/examples/native-hello/build-win32/el_win32.o create mode 100755 ui/examples/native-hello/build-win32/native-hello.exe create mode 100644 ui/examples/native-hello/build-win32/native_hello.c create mode 100644 ui/examples/native-hello/build-win32/native_hello.o create mode 100755 ui/examples/native-hello/build.sh create mode 100644 ui/examples/native-hello/build/.rt_T.txt create mode 100644 ui/examples/native-hello/build/.rt_keep.txt create mode 100644 ui/examples/native-hello/build/.seed_T.txt create mode 100644 ui/examples/native-hello/build/.vessel_keep.txt create mode 100644 ui/examples/native-hello/build/el_appkit.o create mode 100644 ui/examples/native-hello/build/el_native_vessel.c create mode 100644 ui/examples/native-hello/build/el_native_vessel.h create mode 100644 ui/examples/native-hello/build/el_native_vessel.o create mode 100644 ui/examples/native-hello/build/el_runtime.o create mode 100644 ui/examples/native-hello/build/el_sdl2.o create mode 100644 ui/examples/native-hello/build/el_seed.o create mode 100755 ui/examples/native-hello/build/native-hello create mode 100755 ui/examples/native-hello/build/native-hello-linux-arm64 create mode 100755 ui/examples/native-hello/build/native-hello-linux-x86_64 create mode 100755 ui/examples/native-hello/build/native-hello-pi-arm64 create mode 100755 ui/examples/native-hello/build/native-hello-sdl2 create mode 100644 ui/examples/native-hello/build/native_hello.c create mode 100644 ui/examples/native-hello/build/native_hello.o create mode 100755 ui/examples/native-hello/docker-build-linux.sh create mode 100644 ui/examples/native-hello/manifest.el create mode 100644 ui/examples/native-hello/src/App.el create mode 100644 ui/examples/native-hello/src/main.el create mode 100644 ui/examples/profile-card/manifest.el create mode 100755 ui/tools/dist/el-ui-native-codegen create mode 100755 ui/tools/native-codegen/build.sh create mode 100644 ui/tools/native-codegen/el_ui_native_codegen.py create mode 100644 ui/tools/native-codegen/test_app.el create mode 100644 ui/tools/native-codegen/test_native_hello.el create mode 100644 ui/vessels/el-html/src/main.elh create mode 100644 ui/vessels/el-native/src/main.el create mode 100644 ui/vessels/el-native/src/main.elh diff --git a/ui/examples/native-hello-android/.gitignore b/ui/examples/native-hello-android/.gitignore new file mode 100644 index 0000000..0ca7c87 --- /dev/null +++ b/ui/examples/native-hello-android/.gitignore @@ -0,0 +1,6 @@ +local.properties +.gradle/ +build/ +app/build/ +*.iml +.idea/ diff --git a/ui/examples/native-hello-android/app/build.gradle b/ui/examples/native-hello-android/app/build.gradle new file mode 100644 index 0000000..dd1dd28 --- /dev/null +++ b/ui/examples/native-hello-android/app/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.neuron.el' + compileSdk 34 + + defaultConfig { + applicationId "com.neuron.el" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + externalNativeBuild { + cmake { + cppFlags "" + arguments "-DANDROID_STL=c++_shared" + } + } + + ndk { + // Build for the two most relevant ABIs. Add x86/x86_64 for emulator. + abiFilters "arm64-v8a", "armeabi-v7a", "x86_64" + } + } + + externalNativeBuild { + cmake { + path "src/main/jni/CMakeLists.txt" + version "3.22.1" + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + jniDebuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // No third-party dependencies — el-native uses only android.* framework classes. + implementation 'androidx.appcompat:appcompat:1.6.1' +} diff --git a/ui/examples/native-hello-android/app/proguard-rules.pro b/ui/examples/native-hello-android/app/proguard-rules.pro new file mode 100644 index 0000000..c0c619f --- /dev/null +++ b/ui/examples/native-hello-android/app/proguard-rules.pro @@ -0,0 +1,4 @@ +# Add project specific ProGuard rules here. +# Keep ElBridge and MainActivity so JNI symbol names stay intact. +-keep class com.neuron.el.ElBridge { *; } +-keep class com.neuron.el.MainActivity { *; } diff --git a/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml b/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..780d219 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java new file mode 100644 index 0000000..25f72d6 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java @@ -0,0 +1,18 @@ +package com.neuron.el; + +import android.app.Application; + +/** + * ElApp — Application subclass for native-hello-android. + * + * Currently minimal: exists as an anchor for future app-level initialisation + * (crash reporting, global state, etc.). Listed in AndroidManifest.xml as + * android:name=".ElApp". + */ +public class ElApp extends Application { + + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java new file mode 100644 index 0000000..52cb402 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java @@ -0,0 +1,711 @@ +/* + * ElBridge.java — Android Java companion to el_android.c. + * + * All public methods are static. The C JNI layer calls these to create views, + * set properties, and manage the widget tree. Views are identified by integer + * slot indices matching the C-side handle values. + * + * Threading: every method that touches a View dispatches to the UI thread + * using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch + * until the UI thread completes the operation. This mirrors the AppKit + * dispatch_sync(main_queue, ^{}) pattern in el_appkit.m. + * + * Callbacks: Java sets listeners on views that call back into C via: + * nativeOnClick(int slot) + * nativeOnChange(int slot, String text) + * nativeOnSubmit(int slot, String text) + * These are declared native and implemented in el_android.c. + * + * Usage (in your Activity.onCreate): + * System.loadLibrary("elruntime"); + * ElBridge.init(this); + * + * The native library calls __native_init() which calls nativeRegisterActivity + * via the C side; alternatively call ElBridge.init(this) directly from Java. + * + * Compile requirements: + * Android minSdkVersion 21 (Lollipop) or higher. + * No third-party dependencies — uses only android.* framework classes. + * For image loading from arbitrary file paths, BitmapFactory is used. + * To replace with Glide/Picasso, edit createImageView only. + */ + +package com.neuron.el; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.util.concurrent.CountDownLatch; + +public class ElBridge { + + /* ── Native callbacks (implemented in el_android.c) ─────────────────── */ + + public static native void nativeOnClick(int slot); + public static native void nativeOnChange(int slot, String text); + public static native void nativeOnSubmit(int slot, String text); + public static native void nativeRegisterActivity(Activity activity); + + /* ── State ───────────────────────────────────────────────────────────── */ + + private static final int MAX_SLOTS = 4096; + + private static Activity sActivity; + private static Handler sUiHandler; + private static View[] sViews = new View[MAX_SLOTS]; + private static int sNextSlot = 1; /* slot 0 reserved / null */ + + /* ── Init ────────────────────────────────────────────────────────────── */ + + /** + * Must be called from the Activity before any widget operations. + * Typically called from Activity.onCreate after System.loadLibrary. + */ + public static void init(Activity activity) { + sActivity = activity; + sUiHandler = new Handler(Looper.getMainLooper()); + nativeRegisterActivity(activity); + } + + /* ── Slot management ─────────────────────────────────────────────────── */ + + private static int allocSlot(View v) { + /* Find a free slot starting from sNextSlot, wrap around. */ + for (int i = 0; i < MAX_SLOTS - 1; i++) { + int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1; + if (sViews[idx] == null) { + sViews[idx] = v; + sNextSlot = (idx % (MAX_SLOTS - 1)) + 1; + return idx; + } + } + android.util.Log.e("ElBridge", "allocSlot: slot table full"); + return -1; + } + + private static View getView(int slot) { + if (slot <= 0 || slot >= MAX_SLOTS) return null; + return sViews[slot]; + } + + /* ── UI-thread dispatch helper ───────────────────────────────────────── */ + + /* + * Dispatch r on the UI thread and block until it completes. + * Safe to call from the UI thread itself (runs inline without posting). + */ + private static void runSync(final Runnable r) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + } else { + final CountDownLatch latch = new CountDownLatch(1); + sUiHandler.post(new Runnable() { + @Override public void run() { + try { r.run(); } finally { latch.countDown(); } + } + }); + try { latch.await(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /* ── Integer slot returning runSync helper ───────────────────────────── */ + + private interface IntSupplier { int get(); } + + private static int runSyncInt(final IntSupplier s) { + final int[] result = { -1 }; + runSync(new Runnable() { + @Override public void run() { result[0] = s.get(); } + }); + return result[0]; + } + + /* ── Context accessor ────────────────────────────────────────────────── */ + + private static Context ctx() { return sActivity; } + + /* ── View creation ───────────────────────────────────────────────────── */ + + /** + * Create a LinearLayout. + * @param orientation 1=VERTICAL, 0=HORIZONTAL + * @param spacing gap between children in dp; applied as bottom/right margin + */ + public static int createLinearLayout(final int orientation, final int spacing) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + LinearLayout ll = new LinearLayout(ctx()); + ll.setOrientation(orientation == 1 + ? LinearLayout.VERTICAL + : LinearLayout.HORIZONTAL); + ll.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + /* Spacing is stored so addChild can apply margins. */ + ll.setTag(R_TAG_SPACING, spacing); + return allocSlot(ll); + } + }); + } + + /** Create a FrameLayout (ZStack equivalent). */ + public static int createFrameLayout() { + return runSyncInt(new IntSupplier() { + @Override public int get() { + FrameLayout fl = new FrameLayout(ctx()); + fl.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(fl); + } + }); + } + + /** Create a ScrollView. */ + public static int createScrollView() { + return runSyncInt(new IntSupplier() { + @Override public int get() { + ScrollView sv = new ScrollView(ctx()); + sv.setLayoutParams(new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + sv.setFillViewport(true); + return allocSlot(sv); + } + }); + } + + /** Create a TextView with initial text. */ + public static int createTextView(final String text) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + TextView tv = new TextView(ctx()); + tv.setText(text != null ? text : ""); + tv.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(tv); + } + }); + } + + /** Create a Button with a label. */ + public static int createButton(final String label) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + Button btn = new Button(ctx()); + btn.setText(label != null ? label : ""); + btn.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(btn); + } + }); + } + + /** + * Create an EditText. + * @param placeholder hint text + * @param singleLine true = single-line text field; false = multi-line text area + */ + public static int createEditText(final String placeholder, final boolean singleLine) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + EditText et = new EditText(ctx()); + et.setHint(placeholder != null ? placeholder : ""); + if (singleLine) { + et.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + et.setMaxLines(1); + et.setSingleLine(true); + } else { + et.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + et.setMinLines(3); + et.setSingleLine(false); + et.setGravity(Gravity.TOP | Gravity.START); + } + et.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(et); + } + }); + } + + /** + * Create an ImageView, loading from a file path via BitmapFactory. + * If path is null/empty the ImageView is created with no image. + */ + public static int createImageView(final String path) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + ImageView iv = new ImageView(ctx()); + iv.setScaleType(ImageView.ScaleType.FIT_CENTER); + iv.setAdjustViewBounds(true); + if (path != null && !path.isEmpty()) { + Bitmap bmp = BitmapFactory.decodeFile(path); + if (bmp != null) { + iv.setImageBitmap(bmp); + } else { + android.util.Log.w("ElBridge", + "createImageView: failed to decode " + path); + } + } + iv.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(iv); + } + }); + } + + /* ── Window operations ───────────────────────────────────────────────── */ + + /** Set the Activity's content view to the view at slot. */ + public static void setContentView(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null && sActivity != null) { + sActivity.setContentView(v); + } + } + }); + } + + /** Set the Activity title. */ + public static void setTitle(final String title) { + runSync(new Runnable() { + @Override public void run() { + if (sActivity != null) { + sActivity.setTitle(title != null ? title : ""); + } + } + }); + } + + /* ── Tree operations ─────────────────────────────────────────────────── */ + + /** + * Add child view to parent view. + * LinearLayout: child added as arranged child with spacing margin. + * ScrollView: child replaces current document view. + * FrameLayout / other ViewGroup: plain addView. + */ + public static void addChild(final int parentSlot, final int childSlot) { + runSync(new Runnable() { + @Override public void run() { + View parent = getView(parentSlot); + View child = getView(childSlot); + if (parent == null || child == null) return; + + /* Remove child from existing parent first. */ + if (child.getParent() instanceof ViewGroup) { + ((ViewGroup) child.getParent()).removeView(child); + } + + if (parent instanceof LinearLayout) { + LinearLayout ll = (LinearLayout) parent; + Object tag = ll.getTag(R_TAG_SPACING); + int spacing = (tag instanceof Integer) ? (Integer) tag : 0; + LinearLayout.LayoutParams lp; + Object existingLp = child.getLayoutParams(); + if (existingLp instanceof LinearLayout.LayoutParams) { + lp = (LinearLayout.LayoutParams) existingLp; + } else { + lp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + /* Apply spacing as margin on the leading/top edge (after first child). */ + if (ll.getChildCount() > 0 && spacing > 0) { + int px = dpToPx(spacing); + if (ll.getOrientation() == LinearLayout.VERTICAL) { + lp.topMargin = px; + } else { + lp.leftMargin = px; + } + } + child.setLayoutParams(lp); + ll.addView(child); + } else if (parent instanceof ScrollView) { + ScrollView sv = (ScrollView) parent; + sv.removeAllViews(); + sv.addView(child); + } else if (parent instanceof ViewGroup) { + ((ViewGroup) parent).addView(child); + } + } + }); + } + + /** Remove child from its parent. */ + public static void removeChild(final int parentSlot, final int childSlot) { + runSync(new Runnable() { + @Override public void run() { + View parent = getView(parentSlot); + View child = getView(childSlot); + if (parent instanceof ViewGroup && child != null) { + ((ViewGroup) parent).removeView(child); + } + } + }); + } + + /** Remove the view from its parent and release the slot. */ + public static void destroyView(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + if (v.getParent() instanceof ViewGroup) { + ((ViewGroup) v.getParent()).removeView(v); + } + sViews[slot] = null; + } + }); + } + + /* ── Property setters ────────────────────────────────────────────────── */ + + public static void setText(final int slot, final String text) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + String s = text != null ? text : ""; + if (v instanceof EditText) { + ((EditText) v).setText(s); + } else if (v instanceof Button) { + ((Button) v).setText(s); + } else if (v instanceof TextView) { + ((TextView) v).setText(s); + } + } + }); + } + + public static String getText(final int slot) { + final String[] result = { "" }; + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v instanceof TextView) { + CharSequence cs = ((TextView) v).getText(); + result[0] = cs != null ? cs.toString() : ""; + } + } + }); + return result[0]; + } + + /** Set foreground text color. Components in [0,1]. */ + public static void setTextColor(final int slot, final float r, final float g, + final float b, final float a) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v instanceof TextView) { + ((TextView) v).setTextColor(floatToArgb(r, g, b, a)); + } + } + }); + } + + /** Set background color using a GradientDrawable so corner radius is preserved. */ + public static void setBackgroundColor(final int slot, final float r, final float g, + final float b, final float a) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ensureGradientBackground(v); + GradientDrawable gd = (GradientDrawable) v.getBackground(); + gd.setColor(floatToArgb(r, g, b, a)); + } + }); + } + + /** + * Set font family and size. + * family: "system" or null → system default; otherwise tries to load by name. + * bold: if true uses Typeface.BOLD. + */ + public static void setFont(final int slot, final String family, + final int sizeSp, final boolean bold) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof TextView)) return; + TextView tv = (TextView) v; + Typeface tf; + if (family != null && !family.isEmpty() + && !family.equals("system")) { + Typeface base = Typeface.create(family, + bold ? Typeface.BOLD : Typeface.NORMAL); + tf = (base != null) ? base + : Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL); + } else { + tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL); + } + tv.setTypeface(tf); + tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp); + } + }); + } + + /** Set padding in dp. */ + public static void setPadding(final int slot, final int top, final int right, + final int bottom, final int left) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) { + v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom)); + } + } + }); + } + + /** Set explicit width in dp. Passes MATCH_PARENT for negative values. */ + public static void setWidth(final int slot, final int widthDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp == null) lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.width = widthDp < 0 + ? ViewGroup.LayoutParams.MATCH_PARENT + : dpToPx(widthDp); + v.setLayoutParams(lp); + } + }); + } + + /** Set explicit height in dp. */ + public static void setHeight(final int slot, final int heightDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp == null) lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.height = heightDp < 0 + ? ViewGroup.LayoutParams.MATCH_PARENT + : dpToPx(heightDp); + v.setLayoutParams(lp); + } + }); + } + + /** + * Set flex weight on a child of a LinearLayout. + * flex > 0 → weight = flex, width/height = 0dp (expand). + * flex == 0 → weight = 0, wrap_content (shrink to content). + */ + public static void setFlex(final int slot, final int flex) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp; + if (flex > 0) { + llp.weight = (float) flex; + /* Determine orientation from parent to set 0dp on the right axis. */ + if (v.getParent() instanceof LinearLayout) { + LinearLayout parent = (LinearLayout) v.getParent(); + if (parent.getOrientation() == LinearLayout.VERTICAL) { + llp.height = 0; + } else { + llp.width = 0; + } + } + } else { + llp.weight = 0f; + } + v.setLayoutParams(llp); + } + } + }); + } + + /** Set corner radius in dp using a GradientDrawable background. */ + public static void setCornerRadius(final int slot, final float radiusDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ensureGradientBackground(v); + GradientDrawable gd = (GradientDrawable) v.getBackground(); + gd.setCornerRadius(dpToPxF(radiusDp)); + } + }); + } + + public static void setEnabled(final int slot, final boolean enabled) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) v.setEnabled(enabled); + } + }); + } + + /** + * Show or hide a view. + * @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics) + */ + public static void setVisibility(final int slot, final boolean visible) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE); + } + }); + } + + /* ── Event listener registration ─────────────────────────────────────── */ + + /** Register an OnClickListener that calls back into C nativeOnClick. */ + public static void setOnClickListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + final int capturedSlot = slot; + v.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + nativeOnClick(capturedSlot); + } + }); + } + }); + } + + /** + * Register a TextWatcher on an EditText that calls back nativeOnChange + * for every text change. + */ + public static void setOnChangeListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof EditText)) return; + final int capturedSlot = slot; + ((EditText) v).addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, + int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, + int before, int count) {} + @Override public void afterTextChanged(Editable s) { + nativeOnChange(capturedSlot, s != null ? s.toString() : ""); + } + }); + } + }); + } + + /** + * Register an OnEditorActionListener on a single-line EditText that calls + * nativeOnSubmit when the user presses the action/enter key. + */ + public static void setOnSubmitListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof EditText)) return; + final int capturedSlot = slot; + ((EditText) v).setOnEditorActionListener( + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView tv, int actionId, + android.view.KeyEvent event) { + nativeOnSubmit(capturedSlot, + tv.getText() != null ? tv.getText().toString() : ""); + return true; + } + }); + } + }); + } + + /* ── Internal helpers ─────────────────────────────────────────────────── */ + + /* + * Tag key used to stash the spacing value on LinearLayouts so addChild + * can apply the correct margin between children. + * We use a stable integer resource-id-like value; because we do not have + * a resources file here we use View.generateViewId() lazily. + */ + private static int sSpacingTagKey = 0; + + private static int R_TAG_SPACING; + static { + R_TAG_SPACING = View.generateViewId(); + } + + /** Convert dp to pixels using the Activity's display metrics. */ + private static int dpToPx(float dp) { + if (sActivity == null) return (int) dp; + float density = sActivity.getResources().getDisplayMetrics().density; + return Math.round(dp * density); + } + + private static float dpToPxF(float dp) { + if (sActivity == null) return dp; + float density = sActivity.getResources().getDisplayMetrics().density; + return dp * density; + } + + /** Convert RGBA float components (0–1) to an Android ARGB int. */ + private static int floatToArgb(float r, float g, float b, float a) { + int ai = Math.round(a * 255f); + int ri = Math.round(r * 255f); + int gi = Math.round(g * 255f); + int bi = Math.round(b * 255f); + return Color.argb(ai, ri, gi, bi); + } + + /** + * Ensure the view has a GradientDrawable background so that both color + * and corner radius can be set independently. If the current background + * is already a GradientDrawable it is reused; otherwise a new transparent + * one is installed. + */ + private static void ensureGradientBackground(View v) { + if (!(v.getBackground() instanceof GradientDrawable)) { + GradientDrawable gd = new GradientDrawable(); + gd.setColor(Color.TRANSPARENT); + v.setBackground(gd); + } + } +} diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java new file mode 100644 index 0000000..47eec0e --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java @@ -0,0 +1,38 @@ +package com.neuron.el; + +import android.app.Activity; +import android.os.Bundle; + +/** + * MainActivity — entry point for native-hello-android. + * + * Loads the el native shared library, initialises ElBridge with the Activity + * reference (required before any widget operations), then hands control to the + * compiled el program via nativeMain(). + * + * The el boot sequence (native_init → window_from_manifest → app_build → + * window_show) runs inside nativeMain. __native_run_loop is a no-op on Android; + * the Activity lifecycle owns the UI thread after onCreate returns. + */ +public class MainActivity extends Activity { + + static { + System.loadLibrary("elnative"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Register this Activity with the bridge BEFORE nativeMain so that + // el_android_init can look up ElBridge methods and __native_init works. + ElBridge.init(this); + // Run the compiled el program. + nativeMain(); + } + + /** + * Implemented in el_android.c as Java_com_neuron_el_MainActivity_nativeMain. + * Calls the el program's main(), which runs the full boot sequence. + */ + private native void nativeMain(); +} diff --git a/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt b/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000..fbc3dea --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.22.1) +project("elnative") + +# +# CMakeLists.txt for native-hello-android +# +# Sources: +# el_android.c — Android JNI widget bridge +# el_seed.c — OS-boundary __-prefixed primitives +# el_runtime.c — High-level el builtins (str_len, json_*, etc.) +# el_native_vessel.c — el-native vessel (compiled from vessels/el-native/src/main.el) +# native_hello.c — App entry point (compiled from examples/native-hello/src/main.el) +# +# el_runtime.c and el_seed.c both define __-prefixed symbols. On Android the +# linker accepts duplicate weak symbols; the shared library loads one copy. +# The EL_TARGET_ANDROID guard in el_android.c ensures only the Android widget +# backend is compiled in. +# + +add_library( + elnative + SHARED + + # native_hello.c listed first so its main() takes precedence over the + # vessel's main() when --allow-multiple-definition is in effect. + native_hello.c + el_native_vessel.c + el_android.c + el_seed.c + el_runtime.c +) + +# EL_TARGET_ANDROID activates the Android JNI widget backend in el_android.c +# and the corresponding guards in el_seed.c / el_native_target.h. +target_compile_definitions(elnative PRIVATE + EL_TARGET_ANDROID +) + +target_include_directories(elnative PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# el_runtime.c and el_seed.c both define __-prefixed OS-boundary symbols. +# el_runtime.c's copies are weaker definitions; allow-multiple-definition lets +# the linker pick one copy silently (el_seed.c listed later = its copy wins +# in the Android linker's right-to-left resolution order). +target_link_options(elnative PRIVATE + "-Wl,--allow-multiple-definition" +) + +find_library(log-lib log) +find_library(android-lib android) + +target_link_libraries(elnative + ${log-lib} + ${android-lib} +) diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_android.c b/ui/examples/native-hello-android/app/src/main/jni/el_android.c new file mode 100644 index 0000000..5f629ba --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_android.c @@ -0,0 +1,964 @@ +/* + * el_android.c — Android JNI backend for the el native widget system. + * + * This file implements the Android widget layer that el_seed.c calls through + * to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart + * to el_appkit.m and presents the same C API surface. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_android_* C-callable functions declared here + * → ElBridge static methods in Java via JNI + * → android.view.View subclasses on the UI thread + * + * Widget handles: every widget (window root, view, control) is assigned an + * int64_t slot index into view_slots[]. The el program holds these as opaque + * Int values. Slot 0 is never valid (null handle = -1 convention). + * + * Threading: Android requires all UI operations to run on the main (UI) thread. + * Every JNI call that mutates a View is dispatched through + * Activity.runOnUiThread(Runnable) if the current thread is not the UI thread. + * el_android_run_loop is a no-op — Android lifecycle is driven by the Activity. + * + * Callback mechanism: when a widget fires an event Java calls + * nativeOnClick / nativeOnChange / nativeOnSubmit + * The C side looks up the registered El function name for that slot, then: + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This matches the __thread_create pattern in el_seed.c exactly. + * + * Compile / link (as part of libelruntime.so): + * Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID. + * Link flags: -landroid -llog -ldl + * + * Java companion: ElBridge.java in the same directory must be compiled into + * the Android application's APK (package com.neuron.el). + */ + +#ifdef EL_TARGET_ANDROID + +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Logging ─────────────────────────────────────────────────────────────── */ + +#define EL_TAG "ElAndroid" +#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__) +#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__) +#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__) + +/* ── JNI global state ────────────────────────────────────────────────────── */ + +static JavaVM *g_jvm = NULL; +static jobject g_activity = NULL; /* global ref to Activity */ +static jclass g_bridge_class = NULL; /* global ref to ElBridge class */ + +/* Cached method IDs on ElBridge — filled in el_android_init(). */ +static jmethodID g_mid_createLinearLayout = NULL; +static jmethodID g_mid_createFrameLayout = NULL; +static jmethodID g_mid_createScrollView = NULL; +static jmethodID g_mid_createTextView = NULL; +static jmethodID g_mid_createButton = NULL; +static jmethodID g_mid_createEditText = NULL; +static jmethodID g_mid_createImageView = NULL; +static jmethodID g_mid_setContentView = NULL; +static jmethodID g_mid_setTitle = NULL; +static jmethodID g_mid_addChild = NULL; +static jmethodID g_mid_removeChild = NULL; +static jmethodID g_mid_destroyView = NULL; +static jmethodID g_mid_setText = NULL; +static jmethodID g_mid_getText = NULL; +static jmethodID g_mid_setTextColor = NULL; +static jmethodID g_mid_setBackgroundColor = NULL; +static jmethodID g_mid_setFont = NULL; +static jmethodID g_mid_setPadding = NULL; +static jmethodID g_mid_setWidth = NULL; +static jmethodID g_mid_setHeight = NULL; +static jmethodID g_mid_setFlex = NULL; +static jmethodID g_mid_setCornerRadius = NULL; +static jmethodID g_mid_setEnabled = NULL; +static jmethodID g_mid_setVisibility = NULL; +static jmethodID g_mid_setOnClickListener = NULL; +static jmethodID g_mid_setOnChangeListener = NULL; +static jmethodID g_mid_setOnSubmitListener = NULL; +static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */ + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_ANDROID_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + jint slot; /* Java-side slot index (matches C index) */ + char *cb_click; /* El function name for click / submit events */ + char *cb_change; /* El function name for value-change events */ +} ElWidget; + +static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS]; + +static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) { + for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].slot = slot; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + return (int64_t)i; + } + } + EL_LOGE("el_widget_alloc: slot table full"); + return -1; +} + +static ElWidget *el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + w->kind = EL_WIDGET_FREE; + w->slot = -1; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── JNI environment helpers ─────────────────────────────────────────────── */ + +/* + * Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if + * needed (detaches in el_jni_detach_if_attached — call in pairs). + */ +static int g_was_attached = 0; /* thread-local would be cleaner but this is + safe for single-threaded el programs */ + +static JNIEnv *el_jni_env(void) { + if (!g_jvm) return NULL; + JNIEnv *env = NULL; + jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6); + if (rc == JNI_OK) { g_was_attached = 0; return env; } + if (rc == JNI_EDETACHED) { + if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) { + g_was_attached = 1; + return env; + } + } + EL_LOGE("el_jni_env: failed to obtain JNIEnv"); + return NULL; +} + +static void el_jni_detach_if_attached(void) { + if (g_was_attached && g_jvm) { + (*g_jvm)->DetachCurrentThread(g_jvm); + g_was_attached = 0; + } +} + +/* ── UI-thread dispatch ──────────────────────────────────────────────────── */ +/* + * Most ElBridge static methods already dispatch to the UI thread internally + * (they call Activity.runOnUiThread). The helper below is available for + * cases where the caller needs to be sure the call has completed before + * returning (ElBridge methods marked "sync" use a CountDownLatch internally). + * + * For the current implementation we call ElBridge methods directly; ElBridge + * itself marshals to the UI thread via Activity.runOnUiThread + latch. + * This keeps the C side simple and mirrors the AppKit dispatch_sync pattern. + */ + +/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */ + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)reserved; + g_jvm = vm; + EL_LOGI("JNI_OnLoad: el Android bridge loaded"); + return JNI_VERSION_1_6; +} + +/* ── el_android_init ─────────────────────────────────────────────────────── */ +/* + * Called from __native_init(). The Activity must have already called + * ElBridge.registerActivity(activity) from Java before this runs, which sets + * g_activity via the nativeRegisterActivity JNI method below. + * + * Caches all method IDs used later so individual widget calls avoid repeated + * FindClass / GetStaticMethodID lookups. + */ +void el_android_init(void) { + static int done = 0; + if (done) return; + done = 1; + + JNIEnv *env = el_jni_env(); + if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; } + + jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge"); + if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; } + g_bridge_class = (*env)->NewGlobalRef(env, cls); + (*env)->DeleteLocalRef(env, cls); + +#define CACHE_STATIC(var, name, sig) \ + var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \ + if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig) + + CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I"); + CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I"); + CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I"); + CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I"); + CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V"); + CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V"); + CACHE_STATIC(g_mid_addChild, "addChild", "(II)V"); + CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V"); + CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V"); + CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V"); + CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;"); + CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V"); + CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V"); + CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V"); + CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V"); + CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V"); + CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V"); + CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V"); + CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V"); + CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V"); + CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V"); + CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V"); + CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V"); + CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V"); +#undef CACHE_STATIC + + el_jni_detach_if_attached(); + EL_LOGI("el_android_init: complete"); +} + +/* ── JNI: Activity registration ─────────────────────────────────────────── */ + +/* + * Called from Java: ElBridge.registerActivity(activity) calls back here. + * Stores a global reference to the Activity so C code can dispatch to it. + */ +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls, + jobject activity) { + (void)cls; + if (g_activity) { + (*env)->DeleteGlobalRef(env, g_activity); + g_activity = NULL; + } + if (activity) { + g_activity = (*env)->NewGlobalRef(env, activity); + EL_LOGI("nativeRegisterActivity: activity registered"); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. + * Signature matches AppKit: fn(handle: Int, data: String) -> Void + * compiled to: void fn(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_android_invoke_cb(const char *fn_name, int64_t handle, + const char *data) { + if (!fn_name || !*fn_name) return; + void *sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; } + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */ + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) { + (void)env; (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_click) { + el_android_invoke_cb(w->cb_click, handle, ""); + } +} + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls, + jint slot, jstring text) { + (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_change) { + const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : ""; + el_android_invoke_cb(w->cb_change, handle, ctext); + if (text) (*env)->ReleaseStringUTFChars(env, text, ctext); + } +} + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls, + jint slot, jstring text) { + (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */ + const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : ""; + el_android_invoke_cb(w->cb_click, handle, ctext); + if (text) (*env)->ReleaseStringUTFChars(env, text, ctext); + } +} + +/* ── Helper: jstring from C string ──────────────────────────────────────── */ + +static jstring el_jstr(JNIEnv *env, const char *s) { + return (*env)->NewStringUTF(env, s ? s : ""); +} + +/* ── Window ──────────────────────────────────────────────────────────────── */ + +/* + * el_android_window_create — on Android a "window" is the root LinearLayout + * set as the Activity's content view. We create a vertical LinearLayout and + * store it. el_android_window_show calls setContentView on the Activity. + */ +int64_t el_android_window_create(const char *title, int width, int height, + int min_width, int min_height) { + (void)width; (void)height; (void)min_width; (void)min_height; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + + /* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)1 /* VERTICAL */, (jint)0); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; + } + + /* Set activity title */ + if (g_mid_setTitle && title) { + jstring jtitle = el_jstr(env, title); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); + } + + int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot); + el_jni_detach_if_attached(); + return handle; +} + +void el_android_window_show(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_window_set_title(int64_t handle, const char *title) { + (void)handle; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jtitle = el_jstr(env, title); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +int64_t el_android_vstack_create(int spacing) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)1 /* VERTICAL */, (jint)spacing); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_hstack_create(int spacing) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)0 /* HORIZONTAL */, (jint)spacing); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_zstack_create(void) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createFrameLayout); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_scroll_create(void) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createScrollView); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_android_label_create(const char *text) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jt = el_jstr(env, text); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createTextView, jt); + (*env)->DeleteLocalRef(env, jt); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_button_create(const char *label) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jl = el_jstr(env, label); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createButton, jl); + (*env)->DeleteLocalRef(env, jl); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_text_field_create(const char *placeholder) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, placeholder); + /* singleLine = true */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createEditText, jp, (jboolean)JNI_TRUE); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_text_area_create(const char *placeholder) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, placeholder); + /* singleLine = false → multiline EditText */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createEditText, jp, (jboolean)JNI_FALSE); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_image_create(const char *path) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, path); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createImageView, jp); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_android_widget_set_text(int64_t handle, const char *text) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jt = el_jstr(env, text); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText, + (jint)w->slot, jt); + (*env)->DeleteLocalRef(env, jt); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +const char *el_android_widget_get_text(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return ""; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return ""; + jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class, + g_mid_getText, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; } + const char *result = ""; + if (js) { + const char *cstr = (*env)->GetStringUTFChars(env, js, NULL); + result = cstr ? strdup(cstr) : ""; + if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr); + (*env)->DeleteLocalRef(env, js); + } + el_jni_detach_if_attached(); + return result; +} + +void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor, + (jint)w->slot, (jfloat)r, (jfloat)g, + (jfloat)b, (jfloat)a); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor, + (jint)w->slot, (jfloat)r, (jfloat)g, + (jfloat)b, (jfloat)a); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jfam = el_jstr(env, family); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont, + (jint)w->slot, jfam, (jint)size, + (jboolean)(bold ? JNI_TRUE : JNI_FALSE)); + (*env)->DeleteLocalRef(env, jfam); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding, + (jint)w->slot, (jint)top, (jint)right, + (jint)bottom, (jint)left); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_width(int64_t handle, int width) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth, + (jint)w->slot, (jint)width); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_height(int64_t handle, int height) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight, + (jint)w->slot, (jint)height); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_flex(int64_t handle, int flex) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex, + (jint)w->slot, (jint)flex); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_corner_radius(int64_t handle, int radius) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius, + (jint)w->slot, (jfloat)radius); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_disabled(int64_t handle, int disabled) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled, + (jint)w->slot, + (jboolean)(disabled ? JNI_FALSE : JNI_TRUE)); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_hidden(int64_t handle, int hidden) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + /* visible=true means NOT hidden */ + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility, + (jint)w->slot, + (jboolean)(hidden ? JNI_FALSE : JNI_TRUE)); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +void el_android_widget_add_child(int64_t parent, int64_t child) { + ElWidget *pw = el_widget_get(parent); + ElWidget *cw = el_widget_get(child); + if (!pw || !cw) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild, + (jint)pw->slot, (jint)cw->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_remove_child(int64_t parent, int64_t child) { + ElWidget *pw = el_widget_get(parent); + ElWidget *cw = el_widget_get(child); + if (!pw || !cw) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild, + (jint)pw->slot, (jint)cw->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_android_widget_on_click(int64_t handle, const char *fn_name) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_click) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_on_change(int64_t handle, const char *fn_name) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_change) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_on_submit(int64_t handle, const char *fn_name) { + /* Submit stored in cb_click, same as AppKit. */ + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_click) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_android_widget_destroy(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (env && g_bridge_class) { + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + } + el_widget_free(handle); + el_jni_detach_if_attached(); +} + +/* ── Manifest reader ─────────────────────────────────────────────────────── */ +/* + * __manifest_read: parse the app{} block from a manifest file. + * Returns the raw file contents as an el_val_t (const char* cast). + * The caller (el program) parses the returned string. + * Reads from the filesystem; for APK assets use the AssetManager path instead. + */ +static char *el_read_file(const char *path) { + if (!path || !*path) return NULL; + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len <= 0) { fclose(f); return NULL; } + char *buf = (char *)malloc((size_t)len + 1); + if (!buf) { fclose(f); return NULL; } + fread(buf, 1, (size_t)len, f); + buf[len] = '\0'; + fclose(f); + return buf; +} + +el_val_t el_android_manifest_read(const char *path) { + char *contents = el_read_file(path); + if (!contents) return (el_val_t)(uintptr_t)""; + return (el_val_t)(uintptr_t)contents; /* caller owns allocation */ +} + +/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */ +/* + * These are the functions declared in el_native_target.h under EL_TARGET_ANDROID. + * They forward to the el_android_* internal functions above. + * + * The el_val_t / int64_t ABI matches the AppKit functions exactly: + * - Integer params passed as int64_t, extracted with (int) + * - String params passed as int64_t, extracted with (const char*)(uintptr_t) + * - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted + * with el_to_float / bit-cast union + */ + +static inline float el_val_to_float(el_val_t v) { + union { double d; int64_t i; } u; + u.i = v; + return (float)u.d; +} + +void __native_init(void) { + el_android_init(); +} + +void __native_run_loop(void) { + /* No-op on Android — lifecycle is driven by the Activity. */ +} + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_android_window_create( + (const char *)(uintptr_t)title, + (int)width, (int)height, (int)min_width, (int)min_height); +} + +void __window_show(el_val_t handle) { + el_android_window_show((int64_t)handle); +} + +void __window_set_title(el_val_t handle, el_val_t title) { + el_android_window_set_title((int64_t)handle, + (const char *)(uintptr_t)title); +} + +el_val_t __vstack_create(el_val_t spacing) { + return (el_val_t)el_android_vstack_create((int)spacing); +} + +el_val_t __hstack_create(el_val_t spacing) { + return (el_val_t)el_android_hstack_create((int)spacing); +} + +el_val_t __zstack_create(void) { + return (el_val_t)el_android_zstack_create(); +} + +el_val_t __scroll_create(void) { + return (el_val_t)el_android_scroll_create(); +} + +el_val_t __label_create(el_val_t text) { + return (el_val_t)el_android_label_create((const char *)(uintptr_t)text); +} + +el_val_t __button_create(el_val_t label) { + return (el_val_t)el_android_button_create((const char *)(uintptr_t)label); +} + +el_val_t __text_field_create(el_val_t placeholder) { + return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder); +} + +el_val_t __text_area_create(el_val_t placeholder) { + return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder); +} + +el_val_t __image_create(el_val_t path_or_name) { + return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name); +} + +void __widget_set_text(el_val_t handle, el_val_t text) { + el_android_widget_set_text((int64_t)handle, + (const char *)(uintptr_t)text); +} + +el_val_t __widget_get_text(el_val_t handle) { + return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle); +} + +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_android_widget_set_color((int64_t)handle, + el_val_to_float(r), el_val_to_float(g), + el_val_to_float(b), el_val_to_float(a)); +} + +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_android_widget_set_bg_color((int64_t)handle, + el_val_to_float(r), el_val_to_float(g), + el_val_to_float(b), el_val_to_float(a)); +} + +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold) { + el_android_widget_set_font((int64_t)handle, + (const char *)(uintptr_t)family, + (int)size, (int)bold); +} + +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_android_widget_set_padding((int64_t)handle, + (int)top, (int)right, (int)bottom, (int)left); +} + +void __widget_set_width(el_val_t handle, el_val_t width) { + el_android_widget_set_width((int64_t)handle, (int)width); +} + +void __widget_set_height(el_val_t handle, el_val_t height) { + el_android_widget_set_height((int64_t)handle, (int)height); +} + +void __widget_set_flex(el_val_t handle, el_val_t flex) { + el_android_widget_set_flex((int64_t)handle, (int)flex); +} + +void __widget_set_corner_radius(el_val_t handle, el_val_t radius) { + el_android_widget_set_corner_radius((int64_t)handle, (int)radius); +} + +void __widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_android_widget_set_disabled((int64_t)handle, (int)disabled); +} + +void __widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_android_widget_set_hidden((int64_t)handle, (int)hidden); +} + +void __widget_add_child(el_val_t parent, el_val_t child) { + el_android_widget_add_child((int64_t)parent, (int64_t)child); +} + +void __widget_remove_child(el_val_t parent, el_val_t child) { + el_android_widget_remove_child((int64_t)parent, (int64_t)child); +} + +void __widget_destroy(el_val_t handle) { + el_android_widget_destroy((int64_t)handle); +} + +void __widget_on_click(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_click((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +void __widget_on_change(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_change((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +void __widget_on_submit(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_submit((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +el_val_t __manifest_read(el_val_t path) { + return el_android_manifest_read((const char *)(uintptr_t)path); +} + +/* ── MainActivity JNI entry point ─────────────────────────────────────────── */ +/* + * Java_com_neuron_el_MainActivity_nativeMain — invoked from MainActivity.onCreate + * after ElBridge.init(this). Calls the el program's compiled main() which runs + * the boot sequence: native_init → window_from_manifest → app_build → window_show. + * __native_run_loop is a no-op on Android; the Activity lifecycle drives the UI. + */ +JNIEXPORT void JNICALL +Java_com_neuron_el_MainActivity_nativeMain(JNIEnv *env, jobject obj) { + (void)env; (void)obj; + extern int main(int argc, char **argv); + char *argv[] = {"el-app", NULL}; + main(1, argv); +} + +#endif /* EL_TARGET_ANDROID */ diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h b/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c b/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c new file mode 100644 index 0000000..0682965 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int el_vessel_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _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(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +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) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + 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) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + 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("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * 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 { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* 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; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + 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'; + curl_easy_setopt(c, CURLOPT_URL, url); + 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"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + 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)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + 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); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + 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); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + 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"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + 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"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + 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; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + 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) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + 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); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + 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) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); 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); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +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_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + 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) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* 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) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +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) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + 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 default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + 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)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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 + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +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. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* 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) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + 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); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(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 + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * 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"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + 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). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + 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_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + 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/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * 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_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#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 + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +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); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +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, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * 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. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"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 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +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); +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 ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +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 + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +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 ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +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/ui/examples/native-hello-android/app/src/main/jni/el_seed.c b/ui/examples/native-hello-android/app/src/main/jni/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +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); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +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_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(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); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +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_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +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); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +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_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +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); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +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); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello-android/app/src/main/jni/native_hello.c b/ui/examples/native-hello-android/app/src/main/jni/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml b/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c2ac18f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui/examples/native-hello-android/app/src/main/res/values/strings.xml b/ui/examples/native-hello-android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b0cc308 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + el-native + diff --git a/ui/examples/native-hello-android/build.gradle b/ui/examples/native-hello-android/build.gradle new file mode 100644 index 0000000..3143e8a --- /dev/null +++ b/ui/examples/native-hello-android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file — configuration for all sub-projects/modules. +plugins { + id 'com.android.application' version '8.2.2' apply false +} diff --git a/ui/examples/native-hello-android/build.sh b/ui/examples/native-hello-android/build.sh new file mode 100755 index 0000000..4428bbd --- /dev/null +++ b/ui/examples/native-hello-android/build.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# build.sh — Build native-hello-android. +# +# Usage: +# ./build.sh # regenerate C sources + ./gradlew assembleDebug +# ./build.sh gen # regenerate C sources only +# ./build.sh gradle # run ./gradlew assembleDebug (skip elc step) +# ./build.sh clean # clean Gradle build outputs +# +# Requirements: +# - Android SDK with NDK and CMake installed +# - local.properties present (copy from local.properties.template and edit sdk.dir) +# - Java 11+ on PATH (for Gradle) +# - elc at ../../../lang/dist/platform/elc + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el" +EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el" +JNI_DIR="${SCRIPT_DIR}/app/src/main/jni" + +# Locate elc. +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +gen() { + echo "==> Generating native_hello.c..." + EL_MANIFEST="${EL_MANIFEST}" \ + "${ELC}" "${EL_APP_ENTRY}" \ + > "${JNI_DIR}/native_hello.c" + + echo "==> Generating el_native_vessel.c..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${JNI_DIR}/el_native_vessel.c" + # Rename the vessel's main() to avoid symbol collision with native_hello.c's main(). + # The Android JNI entry point (nativeMain) calls native_hello.c's main() explicitly. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${JNI_DIR}/el_native_vessel.c" 2>/dev/null || \ + sed -i 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${JNI_DIR}/el_native_vessel.c" + + echo "==> C source generation complete." +} + +gradle_build() { + echo "==> Running ./gradlew assembleDebug..." + cd "${SCRIPT_DIR}" + ./gradlew assembleDebug + + APK_PATH="${SCRIPT_DIR}/app/build/outputs/apk/debug/app-debug.apk" + if [ -f "${APK_PATH}" ]; then + echo "==> Build complete." + echo " APK: ${APK_PATH}" + else + echo "==> Build complete (APK path may differ — check app/build/outputs/apk/)." + fi +} + +clean() { + echo "==> Cleaning..." + cd "${SCRIPT_DIR}" + ./gradlew clean + echo "==> Clean complete." +} + +case "${1:-all}" in + gen) gen ;; + gradle) gradle_build ;; + clean) clean ;; + all|*) gen && gradle_build ;; +esac diff --git a/ui/examples/native-hello-android/gradle.properties b/ui/examples/native-hello-android/gradle.properties new file mode 100644 index 0000000..150f321 --- /dev/null +++ b/ui/examples/native-hello-android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=false diff --git a/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar b/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..033e24c4cdf41af1ab109bc7f253b2b887023340 GIT binary patch literal 63375 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≫L+crb^Bz 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

-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 literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build-gtk4/el_native_vessel.c b/ui/examples/native-hello/build-gtk4/el_native_vessel.c new file mode 100644 index 0000000..28bf38c --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build-gtk4/el_native_vessel.h b/ui/examples/native-hello/build-gtk4/el_native_vessel.h new file mode 100644 index 0000000..b62c288 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build-gtk4/el_native_vessel.o b/ui/examples/native-hello/build-gtk4/el_native_vessel.o new file mode 100644 index 0000000000000000000000000000000000000000..3a45efdeddeb987e8bcf4a637692f5e8b1a8e60c GIT binary patch literal 14692 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�GitqWD}eAMe5qv{=@ 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 Y>WscHBP#%pHf2;u?`vVHiApW@zp@^`x&QzG literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build-gtk4/el_seed.o b/ui/examples/native-hello/build-gtk4/el_seed.o new file mode 100644 index 0000000000000000000000000000000000000000..a9a2678b436c4fd22414c58475f49896e848d8fc GIT binary patch literal 50056 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 literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build-gtk4/native-hello-gtk4 b/ui/examples/native-hello/build-gtk4/native-hello-gtk4 new file mode 100755 index 0000000000000000000000000000000000000000..fc502d8ab1ddea9dd5570b506a0985aa48a0846a GIT binary patch literal 303136 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

e>ZP8HH$3z z6mP%+7ATD;Ov>KQMAl@`Y7*({2uX#@bkcmLxKetWYl)&&eHjGNB2`b$?F`(z@zve6^*-Ki53^{j84@ebv`??=Wq{T5pAi zYn<#;ufok*N#$Oh_Cw2~WyVymmT@BfG}$sjI#%HO~+I`orm?_cpxe*vq#QGeh@Qi2tK(<1!_K zH+q$tHKFm`lDjuOyS>N<8-_J4*6{R&Ge7J&8gqL8;F6_RKKtG6=No={u=In?ryeXj zpwECuWr(dpCE2+^#@gIFut9RUpW*<(he5UuhPL(F^e!s@K z%|i~4YPj*IP21)j7@f4f){>%itA(de8n$Iy^Fb|7U&@|xw*1P(A8sB!`R7vwr@b+1 z@!=JNs!k{|vP<6EKQG-o;>(VU_b%P~`Slu!&41`K^N%a{6lf6t@%Xqx6V6Y3*mHG9 zgHjp4G?_5!mlyV5eEjjXM_cDzFM8|9?F&yVU+7dqFn`257j>(zes;zNDwB|bQ{@hk2zR}(I5U-xou z?wqGL_lfWLPPO{witX*vWOjuo;|pKu5H|Ne#9iZY)v0^!B~A+n0H~`g6av@{OIp^w;&j?V9*l z?^Zj`?|io2U!IHgyOnJ9)})xppP&8ogR_@gtd4#9rP}qz-45TFbbDplb!WeSu4c3J zVYeR0d$856`b)AVBsOZauH6@XZ{*h8@5(wjRFxuwc#Z4PTFLF=E)- zM@F)*yoj{qxKwYU;aw}M<04&!^8vIpSvsJ#J$0F&DY&o+}2;< z{$?!?U+M7ZpM^JO)~ZpfNn(Xk9cxY;I$&klm~w^2URbcLU&(FWwe?>wF(Nr<<(}GY ze(dq(1LMlZesOtx;UN>Z4SjKCrSr$$FMHok_xioddQ6Uq>$>j7ovuSuwym0c&wx&I zkNC=UT`_vpi-%sC^>If1O*tjUe*D?a+TOE2RhwUNZr@9lTYQw`$xHWq+otEfL&xuV zr*wzYiEnnB;5(R5pwI3?zxnQ|cf3&heRuVW_x*X~)WqM;j_5J=!EJ?dCjEZf|LoHB zYsT)X^4ZMdLisn#ZYD1R}yNaK$RHOXB)(eiWpL^`srMMHdzFzb{EuSg(N#izslaFUzp55wc z*Tpl}cIGaccec|l*CXY6R($b;+;2_?2X|Rnas3OQb?umZ|);AI!OFQ8z^33J<^qg^3M-7W#T)f4US`}A!Y4*j{aY?uDk1trY z!@P zwa|Y`|KG}g>^UHmb-HWi<)i=nu~24mXy)@%UOAamIeC8P@mcRLj&1Yb=Kr74e|dQC zj=%rV<*9FfDI6Yl`_134TpF~{GqY^WgDKvb@k={@S#M#2mcGk)Hsg3UiGceQ+# zvwCup18K>TCsuq@;W4*zr1>T zm7~9{zM1`On-?dfrSCkE*{*-myi(P>_qesF@5gzK=08*GW`T-JA3m@+zDw?ypD(=S zSy*R%-}@5B>`Jb(_m%ta#J)MF`TlnoW{$e2;GJp>3M5r&d*IAN@2-A#!S(?YmbV{V zC8JYjc@F3>s!|P)&rlmomslm&+&`T zHQn0!)~8F4EG)63*kA2;Uwv{)%>8fWG|cZ!l}Q6R4Dj3cKij;vX7`x@`Vg%0(8 zz2BC$y{=CfmviIoNqOD>nJ{K=ubia2*G#JrccfOA*bVm +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello/build-gtk4/native_hello.o b/ui/examples/native-hello/build-gtk4/native_hello.o new file mode 100644 index 0000000000000000000000000000000000000000..c4de3f0a4fc76a9116ecbd98f34ea9aaace55907 GIT binary patch literal 6192 zcmb7|YitzP700hx45rYK6bH;BFcey#*v8A#Jhs@ZjqP}`ky#!IEW_G6_HHw~v)vu@ zkZEPK`9Kt@HHb<`Ma^1$#70y~CDb^dVu>nBC8Q7%wW!U9HOYrX0u@v~tSemlKXcEF z$Ac=hBgVh;Klj{oUo&^-^4Y&0{d<8BWdaxdnT>ml31dCs_NUGyQZZExHMz@+G#FfY>#q>w^W@kn779@=eKS&S zhOY{@Y~|6b2?WuH$Cl#fmY%PwK) zLltG>62>s;*z;ZXLNS@ottt)sR+VIV!=xy~EyR_4Ze{8D5-LAgB8~DVHDw}RUqzqQ#*+tseQ#OOH}5*yxja3W$Vu&@9nA5Q2TNp38Dgld zg0Eq6YGB{!bBx0pSFZf?XGZ1d)WEA)oAT+g$u?eGrychAFjmi+Ictfzb!^PJV=s5v zt6cUPm%YwqZ*Ta^zI*I30*0I+ppId=ECtP`?ZReTT zO?^R(&lSVAC>y*>ZDEbC({nSDTh4iwBmOws$4eLH%D4iaVXJSsWoYwFt9)V$^4m6^ zb=xj1r+jagMtsX;p4?K@dBatQ?NwAq+9TkIkK&GFoZLRyoZs?uEY0gM%BVl~So66m z)ICrJ#bICegF1Z~SY zWKVF6oCDiW{gcmJp_AUb!=ulPF!q#k`R)a2PnkVBh4tGu+#TG^BkM`b9 zZ7XtyT?+)#Et(vYI)rwdx*g9%t^w??<)Dt~# zWp^oJO;cQFqIx7Wo8ULtMq1! zq@w9?#I$N9WhkB9KME>|lw!s;C7Mnvdi0!@sTCd3q_#^mr%WxQoHP0}NS@U05B^RV zDV0pMCmv1pX`&^gX(l=Q$WVKzhvH~7Vd5)8={2V7t=V1|-PRiw*>hP_OFBcTla`Xy zC`pgePc3qq=8$@zv7>E&^P#XvMpKDiEo;_lx@gX1jLa^jH=5A3m|_}APezNHnnIr- z#+h2BEvukvxUs9HIgAX!&O?}t-b6}^?Zs@nu5oGOahQ3Hv(zPDD6huC()HiUuX6c$ ze8{~lKM!RE{#(f(Kc-vIZ(pJ)G7kot8IYy!`NuY++AuVPIYT!bzjlnk_j-+^wD4D114fv%Sf z)PZ=PLe-LiwIGfJAw@D!1-=AbDH&J*ejB<%G9bVO(9iJRfq|zW&Jdx;l7S+Ky*>1e zWZ)}s9yFb+z`z|4`+4ZLWZ+LAjyR!l$-o$hS1NQteIFJh9B zCXn8@DzF_~0Imn|dqd}#n#Y2K&>u=h?tpldLboLYH$bYF2kCvfCK)k7%A+&GAhm{3s)BpFjV`ASijGDoV7il?~}%DpU3IFTZlQgf2VZVr0w6OKeHaUW=DvRShE!Leo}ljZn|DZ zZkWCbDa9u!ck3@a`nMka505T*^oJf@=4tN;8sn~5J)pk1H3_$V2T^W4;3*GF?Tn}1 zqpy4P2Qr@%KkO+#?J1{y2mhSwdmjC3j~+xMTuy#^R@}NE%kfuUK~MdY9-Z-M$Qk`v z;?b|k_Bgu`Kk?`vd-Ow3e$`X{o+tliPx;?H^$&XD@f$d!zeAq-8$9KodGsTXe#g_^ z&phSydx3w>)hD&H9v^w)6CN$pta(n?)b4)M#8+-QlSoE0=WMSY?bdX)+lavYBp}15o#aOkrb-^^{i^C(=&2f|G#Lric^F9 z4@cBYq|*H+g9YRnkVb!7Yhc$+{mgtMah4JA(9?{WMPgKeb zKJ*hfMo<+sn(505m}uUhK%uL(1oVSX1WpsEr|O20rZPF5g61=2>M5(YG}Z)6YEIW+ zXlKztI%D)@qRAOOPe)@h98%P%u2V8=6~m^o7!{*ddy;pY_uEaD4~Ws+nYPo&JOipSKEQct~YFtM>cnLb#~chh~C%Yh{NGf zTSv1aJJdsmyY@E*9gj!uYYeuxbafu?XyOI0_3yMT$MDzNF30fiuw9Pf-)_4c!@tdT sIfj3$?Q#tN7TX2GOr|&6((hO>P~f)/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +# Verify SDL2 is available +if ! command -v sdl2-config &>/dev/null; then + echo "Error: sdl2-config not found. Install SDL2 via:" + echo " brew install sdl2 sdl2_ttf sdl2_image" + exit 1 +fi + +SDL2_CFLAGS=$(sdl2-config --cflags) +SDL2_LIBS=$(sdl2-config --libs) + +CLANG_FLAGS_COMMON=( + -std=c11 + -DEL_TARGET_SDL2 + -I "${EL_RUNTIME}" +) + +clean() { + rm -rf "${BUILD_DIR}" + echo "Cleaned." +} + +compile() { + mkdir -p "${BUILD_DIR}" + + # ── 1. Vessel forward-declarations header ───────────────────────────────── + echo "==> Generating el-native vessel header (declarations only)..." + "${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + > "${BUILD_DIR}/el_native_vessel.h" + + # ── 2. Compile el-native vessel to C ────────────────────────────────────── + echo "==> Compiling el-native vessel to C..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${BUILD_DIR}/el_native_vessel.c" + + # ── 3. Compile app entry to C ───────────────────────────────────────────── + echo "==> Compiling app entry to C..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" \ + > "${BUILD_DIR}/native_hello.c" + + # ── 4. Compile el_sdl2.c (SDL2 bridge — pure C) ─────────────────────────── + # sdl2-config --cflags gives -I/opt/homebrew/include/SDL2 which handles + # #include but el_sdl2.c uses #include — also add + # the parent directory so both forms resolve. + echo "==> Compiling el_sdl2.c (SDL2 bridge)..." + clang \ + -std=c11 \ + -DEL_TARGET_SDL2 \ + ${SDL2_CFLAGS} \ + -I/opt/homebrew/include \ + -I "${EL_RUNTIME}" \ + -c "${EL_RUNTIME}/el_sdl2.c" \ + -o "${BUILD_DIR}/el_sdl2.o" + + # ── 5a. Compile el_seed.c ───────────────────────────────────────────────── + echo "==> Compiling el_seed.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_seed.c" \ + -o "${BUILD_DIR}/el_seed.o" + + # ── 5b. Compile el_runtime.c and patch out duplicate __-prefixed symbols ── + echo "==> Compiling el_runtime.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_runtime.c" \ + -o "${BUILD_DIR}/el_runtime.o" + + echo "==> Patching el_runtime.o (hiding __-prefixed symbols duplicated in el_seed.o)..." + nm "${BUILD_DIR}/el_seed.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.seed_T.txt" + nm "${BUILD_DIR}/el_runtime.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.rt_T.txt" + comm -23 "${BUILD_DIR}/.rt_T.txt" "${BUILD_DIR}/.seed_T.txt" \ + > "${BUILD_DIR}/.rt_keep.txt" + nmedit -s "${BUILD_DIR}/.rt_keep.txt" "${BUILD_DIR}/el_runtime.o" + + # ── 6. Compile el-native vessel object ──────────────────────────────────── + echo "==> Compiling el-native vessel object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -c "${BUILD_DIR}/el_native_vessel.c" \ + -o "${BUILD_DIR}/el_native_vessel.o" + + echo "==> Patching el_native_vessel.o (hiding vessel main)..." + nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' \ + | grep -v '^_main$' \ + > "${BUILD_DIR}/.vessel_keep.txt" + nmedit -s "${BUILD_DIR}/.vessel_keep.txt" "${BUILD_DIR}/el_native_vessel.o" + + # ── 7. Compile app object ───────────────────────────────────────────────── + echo "==> Compiling app object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -include "${BUILD_DIR}/el_native_vessel.h" \ + -c "${BUILD_DIR}/native_hello.c" \ + -o "${BUILD_DIR}/native_hello.o" + + # ── 8. Link ─────────────────────────────────────────────────────────────── + echo "==> Linking native-hello-sdl2..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + ${SDL2_LIBS} \ + -lSDL2_ttf \ + -lSDL2_image \ + -lcurl \ + -ldl \ + -lpthread \ + "${BUILD_DIR}/native_hello.o" \ + "${BUILD_DIR}/el_native_vessel.o" \ + "${BUILD_DIR}/el_sdl2.o" \ + "${BUILD_DIR}/el_runtime.o" \ + "${BUILD_DIR}/el_seed.o" \ + -o "${BUILD_DIR}/native-hello-sdl2" + + echo "==> Build complete: ${BUILD_DIR}/native-hello-sdl2" +} + +run() { + compile + echo "==> Launching native-hello-sdl2..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${BUILD_DIR}/native-hello-sdl2" +} + +case "${1:-run}" in + clean) clean ;; + compile) compile ;; + run|*) run ;; +esac diff --git a/ui/examples/native-hello/build-win32.sh b/ui/examples/native-hello/build-win32.sh new file mode 100755 index 0000000..62582c8 --- /dev/null +++ b/ui/examples/native-hello/build-win32.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# build-win32.sh — Cross-compile native-hello for Windows using mingw64. +# +# Requirements: +# - x86_64-w64-mingw32-gcc (brew install mingw-w64) +# - elc in PATH or at ../../../lang/dist/platform/elc +# +# Output: build-win32/native-hello.exe +# +# Note: We use el_runtime_win32.c instead of el_runtime.c / el_seed.c because +# those files have deep POSIX dependencies (pthread, curl, dlfcn, sys/socket, +# etc.) that cannot be cross-compiled to Windows. el_runtime_win32.c provides +# all the runtime symbols that native-hello and the el-native vessel actually +# call, using only standard C11 + Win32 APIs. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +BUILD_DIR="${SCRIPT_DIR}/build-win32" + +# ── Locate elc ─────────────────────────────────────────────────────────────── +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +CC="x86_64-w64-mingw32-gcc" + +# -D_WIN32_IE for SetWindowSubclass (comctl32 >= IE 0x0600) +CFLAGS=( + -std=c11 + -DEL_TARGET_WIN32 + -D_WIN32_IE=0x0600 + -I"${EL_RUNTIME}" + -Wall + -Wno-unused-function +) + +mkdir -p "${BUILD_DIR}" + +# ── 1. Compile el-native vessel to C ───────────────────────────────────────── +echo "==> Compiling el-native vessel to C..." +"${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${BUILD_DIR}/el_native_vessel.c" + +# ── 2. Generate vessel header (declarations only) ──────────────────────────── +# elc --emit-header includes function bodies AND bare global variable definitions. +# We extract only forward declarations with two awk passes: +# Pass 1: stop before function bodies (same as macOS build.sh). +# Pass 2: convert bare global variable definitions (^el_val_t NAME;) to +# `extern el_val_t NAME;` so the app TU sees them as declarations, +# not definitions — avoiding duplicate symbol errors at link time. +echo "==> Generating el-native vessel header (declarations only)..." +"${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + | awk '/^el_val_t [A-Za-z_][A-Za-z0-9_]*;/{print "extern " $0; next} {print}' \ + > "${BUILD_DIR}/el_native_vessel.h" + +# ── 3. Compile app entry to C ───────────────────────────────────────────────── +echo "==> Compiling app entry to C..." +EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" \ + > "${BUILD_DIR}/native_hello.c" + +# ── 4. Compile el_win32.c ───────────────────────────────────────────────────── +echo "==> Compiling el_win32.c (Win32 widget bridge)..." +${CC} "${CFLAGS[@]}" \ + -c "${EL_RUNTIME}/el_win32.c" \ + -o "${BUILD_DIR}/el_win32.o" + +# ── 5. Compile el_runtime_win32.c ───────────────────────────────────────────── +# This replaces el_seed.c + el_runtime.c for Win32 targets. +echo "==> Compiling el_runtime_win32.c (Win32 runtime stub)..." +${CC} "${CFLAGS[@]}" \ + -c "${EL_RUNTIME}/el_runtime_win32.c" \ + -o "${BUILD_DIR}/el_runtime_win32.o" + +# ── 6. Compile el-native vessel object ─────────────────────────────────────── +echo "==> Compiling el-native vessel object..." +${CC} "${CFLAGS[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -c "${BUILD_DIR}/el_native_vessel.c" \ + -o "${BUILD_DIR}/el_native_vessel.o" + +echo "==> Patching el_native_vessel.o (localising vessel main + shared globals)..." +# The vessel's compiled C has its own main(). We localize it so the linker sees +# only the app's main(). We also localize any globals (TOKEN_*, etc.) that the +# vessel defines but the app also declares — matching the nmedit step in +# build.sh (macOS), using objcopy for mingw. +VESSEL_LOCAL_SYMS=() +# Collect all defined (T and D/B/C) symbols in the vessel object +while IFS= read -r sym; do + VESSEL_LOCAL_SYMS+=("--localize-symbol=${sym}") +done < <(x86_64-w64-mingw32-nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ [TDBCt] /{print $3}' \ + | grep -E '^(main|TOKEN_)' || true) +if [ ${#VESSEL_LOCAL_SYMS[@]} -gt 0 ]; then + x86_64-w64-mingw32-objcopy "${VESSEL_LOCAL_SYMS[@]}" \ + "${BUILD_DIR}/el_native_vessel.o" +fi + +# ── 7. Compile app object ───────────────────────────────────────────────────── +echo "==> Compiling app object..." +${CC} "${CFLAGS[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -include "${BUILD_DIR}/el_native_vessel.h" \ + -c "${BUILD_DIR}/native_hello.c" \ + -o "${BUILD_DIR}/native_hello.o" + +# ── 8. Link ─────────────────────────────────────────────────────────────────── +# -Wl,--export-all-symbols: make el callback functions (on_greet_click etc.) +# visible to GetProcAddress(GetModuleHandle(NULL), fn_name) — the Win32 +# equivalent of POSIX dlsym(RTLD_DEFAULT, fn_name). +echo "==> Linking native-hello.exe..." +${CC} "${CFLAGS[@]}" \ + -Wl,--export-all-symbols \ + "${BUILD_DIR}/native_hello.o" \ + "${BUILD_DIR}/el_native_vessel.o" \ + "${BUILD_DIR}/el_win32.o" \ + "${BUILD_DIR}/el_runtime_win32.o" \ + -lcomctl32 -luser32 -lgdi32 -lkernel32 \ + -o "${BUILD_DIR}/native-hello.exe" + +echo "" +echo "==> Built: ${BUILD_DIR}/native-hello.exe" +ls -lh "${BUILD_DIR}/native-hello.exe" diff --git a/ui/examples/native-hello/build-win32/el_native_vessel.c b/ui/examples/native-hello/build-win32/el_native_vessel.c new file mode 100644 index 0000000..28bf38c --- /dev/null +++ b/ui/examples/native-hello/build-win32/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build-win32/el_native_vessel.h b/ui/examples/native-hello/build-win32/el_native_vessel.h new file mode 100644 index 0000000..6a62882 --- /dev/null +++ b/ui/examples/native-hello/build-win32/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +extern el_val_t TOKEN_PRIMARY; +extern el_val_t TOKEN_ON_PRIMARY; +extern el_val_t TOKEN_BACKGROUND; +extern el_val_t TOKEN_ON_BG; +extern el_val_t TOKEN_SURFACE; +extern el_val_t TOKEN_ON_SURFACE; +extern el_val_t TOKEN_OUTLINE; +extern el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build-win32/el_native_vessel.o b/ui/examples/native-hello/build-win32/el_native_vessel.o new file mode 100644 index 0000000000000000000000000000000000000000..cd65d2c3a20eb83ccdcf1abc5113475d9214f456 GIT binary patch literal 14736 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 LH8(eLZTEiwfx{Mi literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build-win32/el_win32.o b/ui/examples/native-hello/build-win32/el_win32.o new file mode 100644 index 0000000000000000000000000000000000000000..87e72148f8e9eb3f046106712435903f34a47c36 GIT binary patch literal 31904 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 literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build-win32/native-hello.exe b/ui/examples/native-hello/build-win32/native-hello.exe new file mode 100755 index 0000000000000000000000000000000000000000..2a1d6fdab8490b0cf9b8bcc21bb0b4268446f0f3 GIT binary patch literal 246847 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

Fx!B6Q;Qj?Fuw)neJ$oF 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 +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello/build-win32/native_hello.o b/ui/examples/native-hello/build-win32/native_hello.o new file mode 100644 index 0000000000000000000000000000000000000000..55a081ba6004f845b075e55090a3d5720be11b2d GIT binary patch literal 5580 zcmaKvdyE@L9mmJ9orD}sj;eucMA1fcqFH)T$6}ILa33dii$^7iUNV&9~BblR1nAa zH$TsK*Nzy;yPx^J<~I+|&g9Rm5mYX&7a}7mAyR=e6G-OA@M<9jV?qS)RSjQ7P3ntv0fzBXU=DqW9+L^yMm~FGHP4WNDhP~Dq#8! z@fw*v)vkGZal^M4H~f4Nc`cjsPcDlM&!T!0DTd>BMtf>$QMPrU_cGo>4VdiVLfFx4 zZrn_kvgfT_!3$Up?U8OK)8Q%&e0abcS*l~?TK>)C7oz4E(9G?H$mG$`MtzP&ecpol zEgiCvrTYJA;ti2~E>oGwp9aD7y>I)0+59im&k}&M^o3Of`c>(GdbPCOlf8uPzqQA97BbRAmja-VZ z1@g|ELuV*bxS@{Z&O&{*bCOA@eJ)exYLe&By?3c$HA(f;Gb<}AW`kc!s;Z|eq--YN zi@Hp*i+m}U=EgYsfj|*y<2eqWnbR#co@4j0C_5L<7``($U7yUt*~?}mCw0>k@L$z)%R*fq3V|m$Acj63>szMI5hFa zF~cnz!K7o@m5SlnvraW74%#JWNMy=^Q#EG&sVbT%Id9K>SNLU%(8^8P<)S0@RUIcF zVUfVfQM%$$+YQ`u(J1(Bzy1f)_MU<*YO}S#Dd~QtnT(P{L2~{Swb%-i`Mh;teeN-j_Lw zC}W3?U|0%n*~x!e`p0?_nV$ISr&pz8FNER+1M5PzMg!l-a z?;&_`uoYi&^xBCc);~ms?$3PEA-yDcN zj)U0a6o`(63GobQkEE|@yKjJK#uDO(+U_L~_x!51dlj@-+P$e|e*w`(lo0;{eNxhz zxEkm6AkJkIXh>>zgSc;_nhtB4(By*H_e&t|#~Bd!{CREnEp7Lrw)+`~`}>mC-T-|{ zX7H+({S`!Kkc3zPaaOCb!*jdqLELUXh}(St#C8vZc*IQ*dyIqFqX6OzPHF8th{x(Y zh{x)J*8Uj8BXkACZM?4OmZpDd`Vju5{WANlpob+5f_f$G)ikQ(f*SVPiwocYB~?vDzz_a*-t?1`-+y`&~y_tEIod!Wp8S_rD;jiGKk{|>_{Wh zcO7U{(g#4?UqjOXXiRE%f<7Y&54q#T(Gx{$LGMu)e77Tx2}7*He$g(} z8!oi_(jMP!gF$E&+O3xG>$ah>;6f{az6sqnbcqxAuDNA;(>J2qhBWvOgznswZahX` z6S`@)OZdKxtphe~oucqv8-GyPm`bJJ;d?fXMm*+qMGD`skv8R{{S}1o*EC{mL$~T^ zQ5md(I3A;w87)PNzvv{ToL7mW<=GQB1<)8PiCeBr1#-11$;4C;_+?saECGlk_!;=M zC6A*v5Rn{5jaFIPW(GA{V=O^7VE)&!#In>Jk{m0FUgC^=(t{+mplW#zd$w)pg2qL8 zeMbp&oS$|{ei@ORs0B7oT&<$wyru*?&fmKv?UhenJf_M>#ko7yDcRN~+1n-AACXMn zF~i3qHq=UcovGHJ?6RQ~1Q)X5ISxyR&qr)X@}n*Zoh!JgwqEO!{JBfA9FaKX6DkPa zDo%o@za^z6(Emrpb0-e8+L*}x=nqx3a--*NCVE-ANJ;jXkoTXc2!3pD|sCXrE%vD z;c|uxomIu<$HmpkExUnbSBo{#`hJlBdC|?rq6-{$9KH<+X9lK~s$Z/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +CLANG_FLAGS_COMMON=( + -std=c11 + -DEL_TARGET_MACOS + -I "${EL_RUNTIME}" +) + +clean() { + rm -rf "${BUILD_DIR}" + echo "Cleaned." +} + +compile() { + mkdir -p "${BUILD_DIR}" + + # ── 1. Vessel forward-declarations header ───────────────────────────────── + # elc --emit-header emits full function bodies, not just prototypes. + # Extract only the forward-declaration lines: stop at the first function + # definition (a line that ends with ') {' opening a body block). + # macOS awk does not support \s — use POSIX [[:space:]] instead. + echo "==> Generating el-native vessel header (declarations only)..." + "${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + > "${BUILD_DIR}/el_native_vessel.h" + + # ── 2. Compile el-native vessel to C ────────────────────────────────────── + echo "==> Compiling el-native vessel to C..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${BUILD_DIR}/el_native_vessel.c" + + # ── 3. Compile app entry to C ───────────────────────────────────────────── + echo "==> Compiling app entry to C..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" \ + > "${BUILD_DIR}/native_hello.c" + + # ── 4. Compile el_appkit.m (ObjC, MRC — not ARC) ───────────────────────── + echo "==> Compiling el_appkit.m (AppKit bridge, -fno-objc-arc)..." + clang \ + -std=gnu11 \ + -ObjC \ + -fno-objc-arc \ + -DEL_TARGET_MACOS \ + -I "${EL_RUNTIME}" \ + -c "${EL_RUNTIME}/el_appkit.m" \ + -o "${BUILD_DIR}/el_appkit.o" + + # ── 5a. Compile el_seed.c ───────────────────────────────────────────────── + # el_seed.c provides the OS-boundary __-prefixed primitives + thin wrappers + # around el_runtime.c's higher-level functions. + echo "==> Compiling el_seed.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_seed.c" \ + -o "${BUILD_DIR}/el_seed.o" + + # ── 5b. Compile el_runtime.c and patch out duplicate __-prefixed symbols ── + # el_runtime.c provides the high-level builtins (str_len, json_get_string, + # int_to_str, println, env, exit_program, el_runtime_init_args, etc.) that + # el_seed.c does NOT provide (they are single-name, no __ prefix). + # + # el_runtime.c also defines the same __-prefixed OS wrappers as el_seed.c. + # We hide those duplicates using nmedit so the linker sees only one copy. + echo "==> Compiling el_runtime.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_runtime.c" \ + -o "${BUILD_DIR}/el_runtime.o" + + echo "==> Patching el_runtime.o (hiding __-prefixed symbols duplicated in el_seed.o)..." + # Build the set of symbols that el_seed.o defines and el_runtime.o also + # defines. We will hide el_runtime.o's copies, keeping el_seed.o canonical. + nm "${BUILD_DIR}/el_seed.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.seed_T.txt" + nm "${BUILD_DIR}/el_runtime.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.rt_T.txt" + # Symbols to keep public in el_runtime.o = all T symbols MINUS the + # duplicates (those will be supplied by el_seed.o). + comm -23 "${BUILD_DIR}/.rt_T.txt" "${BUILD_DIR}/.seed_T.txt" \ + > "${BUILD_DIR}/.rt_keep.txt" + nmedit -s "${BUILD_DIR}/.rt_keep.txt" "${BUILD_DIR}/el_runtime.o" + + # ── 6. Compile el-native vessel object ──────────────────────────────────── + # el_native_target.h adds float_div/mul/etc. + native widget declarations + # without conflicting with el_runtime.h. + echo "==> Compiling el-native vessel object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -c "${BUILD_DIR}/el_native_vessel.c" \ + -o "${BUILD_DIR}/el_native_vessel.o" + + echo "==> Patching el_native_vessel.o (hiding vessel main)..." + # The vessel's compiled C has its own main(). Hide it so it does not + # conflict with the app's main in native_hello.o. + nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' \ + | grep -v '^_main$' \ + > "${BUILD_DIR}/.vessel_keep.txt" + nmedit -s "${BUILD_DIR}/.vessel_keep.txt" "${BUILD_DIR}/el_native_vessel.o" + + # ── 7. Compile app object ───────────────────────────────────────────────── + # Inject el_native_target.h (float ops + native decls) and the vessel's + # forward-declarations header so the vessel functions are resolvable. + echo "==> Compiling app object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -include "${BUILD_DIR}/el_native_vessel.h" \ + -c "${BUILD_DIR}/native_hello.c" \ + -o "${BUILD_DIR}/native_hello.o" + + # ── 8. Link ─────────────────────────────────────────────────────────────── + # el_seed.o = OS-boundary primitives (__-prefixed) + # el_runtime.o (patched) = high-level builtins (str_len, json_*, etc.) + # el_native_vessel.o (patched) = el-level widget API + # el_appkit.o = AppKit C bridge (MRC) + echo "==> Linking native-hello..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -framework Cocoa \ + -lpthread \ + -lcurl \ + -ldl \ + "${BUILD_DIR}/native_hello.o" \ + "${BUILD_DIR}/el_native_vessel.o" \ + "${BUILD_DIR}/el_appkit.o" \ + "${BUILD_DIR}/el_runtime.o" \ + "${BUILD_DIR}/el_seed.o" \ + -o "${BUILD_DIR}/native-hello" + + echo "==> Build complete: ${BUILD_DIR}/native-hello" +} + +run() { + compile + echo "==> Launching native-hello..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${BUILD_DIR}/native-hello" +} + +case "${1:-run}" in + clean) clean ;; + compile) compile ;; + platforms) "${EL_LANG_ROOT}/el-compiler/runtime/detect-platforms" ;; + run|*) run ;; +esac diff --git a/ui/examples/native-hello/build/.rt_T.txt b/ui/examples/native-hello/build/.rt_T.txt new file mode 100644 index 0000000..7657b58 --- /dev/null +++ b/ui/examples/native-hello/build/.rt_T.txt @@ -0,0 +1,397 @@ +___args_json +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___http_conn_fd +___http_do +___http_do_map +___http_do_map_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_raw +___json_parse_map +___json_set +___json_stringify_val +___mutex_lock +___mutex_new +___mutex_unlock +___print +___print_raw +___println +___read_n +___readline +___sha256_hex +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___thread_create +___thread_join +___url_decode +___url_encode +___uuid_v4 +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_request_end +_el_request_start +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build/.rt_keep.txt b/ui/examples/native-hello/build/.rt_keep.txt new file mode 100644 index 0000000..6aa9058 --- /dev/null +++ b/ui/examples/native-hello/build/.rt_keep.txt @@ -0,0 +1,340 @@ +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___http_do_map +___http_do_map_to_file +___print_raw +___read_n +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build/.seed_T.txt b/ui/examples/native-hello/build/.seed_T.txt new file mode 100644 index 0000000..260f1fe --- /dev/null +++ b/ui/examples/native-hello/build/.seed_T.txt @@ -0,0 +1,137 @@ +___args_json +___button_create +___cos_f +___engram_activate +___engram_activate_json +___engram_add_layer +___engram_compile_layered_json +___engram_connect +___engram_edge_between +___engram_edge_count +___engram_forget +___engram_get_node +___engram_get_node_json +___engram_list_layers +___engram_list_layers_json +___engram_load +___engram_neighbors +___engram_neighbors_filtered +___engram_neighbors_json +___engram_node +___engram_node_count +___engram_node_full +___engram_node_layered +___engram_remove_layer +___engram_save +___engram_scan_nodes +___engram_scan_nodes_by_type_json +___engram_scan_nodes_json +___engram_search +___engram_search_json +___engram_stats_json +___engram_strengthen +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___hstack_create +___html_sanitize +___http_conn_fd +___http_do +___http_do_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___image_create +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_bool +___json_get_float +___json_get_int +___json_get_raw +___json_get_string +___json_parse +___json_parse_map +___json_set +___json_stringify +___json_stringify_val +___label_create +___ln_f +___log_f +___manifest_read +___mutex_lock +___mutex_new +___mutex_unlock +___native_init +___native_run_loop +___pi_f +___print +___println +___readline +___scroll_create +___sha256_hex +___sin_f +___sleep_ms +___sqrt_f +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___text_area_create +___text_field_create +___thread_create +___thread_join +___time_now_ns +___url_decode +___url_encode +___uuid_v4 +___vstack_create +___widget_add_child +___widget_destroy +___widget_get_text +___widget_on_change +___widget_on_click +___widget_on_submit +___widget_remove_child +___widget_set_bg_color +___widget_set_color +___widget_set_corner_radius +___widget_set_disabled +___widget_set_flex +___widget_set_font +___widget_set_height +___widget_set_hidden +___widget_set_padding +___widget_set_text +___widget_set_width +___window_create +___window_set_title +___window_show +___zstack_create +_el_request_end +_el_request_start +_el_seed_init_args +_el_seed_set_http_conn_fd diff --git a/ui/examples/native-hello/build/.vessel_keep.txt b/ui/examples/native-hello/build/.vessel_keep.txt new file mode 100644 index 0000000..a8a7964 --- /dev/null +++ b/ui/examples/native-hello/build/.vessel_keep.txt @@ -0,0 +1,55 @@ +_button +_color_hex_a +_color_hex_b +_color_hex_g +_color_hex_r +_hex_channel +_hex_nibble +_hstack +_image +_label +_manifest_height +_manifest_min_height +_manifest_min_width +_manifest_read +_manifest_title +_manifest_width +_native_init +_native_run_loop +_scroll +_style_button_primary +_style_label_body +_style_label_heading +_style_label_muted +_style_surface +_text_area +_text_field +_vstack +_vstack_tight +_widget_add_child +_widget_destroy +_widget_get_text +_widget_on_change +_widget_on_click +_widget_on_submit +_widget_remove_child +_widget_set_bg_color +_widget_set_bg_color_hex +_widget_set_color +_widget_set_color_hex +_widget_set_corner_radius +_widget_set_disabled +_widget_set_flex +_widget_set_font +_widget_set_height +_widget_set_hidden +_widget_set_padding +_widget_set_padding_all +_widget_set_padding_xy +_widget_set_text +_widget_set_width +_window_create +_window_from_manifest +_window_set_title +_window_show +_zstack diff --git a/ui/examples/native-hello/build/el_appkit.o b/ui/examples/native-hello/build/el_appkit.o new file mode 100644 index 0000000000000000000000000000000000000000..4f19980391d3f848f3664293e31155cb68b7be3b GIT binary patch literal 58520 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

ZgAeru`Cet$9D*erd?F_?Ay?JY7FS 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 +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build/el_native_vessel.h b/ui/examples/native-hello/build/el_native_vessel.h new file mode 100644 index 0000000..b62c288 --- /dev/null +++ b/ui/examples/native-hello/build/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build/el_native_vessel.o b/ui/examples/native-hello/build/el_native_vessel.o new file mode 100644 index 0000000000000000000000000000000000000000..3a45efdeddeb987e8bcf4a637692f5e8b1a8e60c GIT binary patch literal 14692 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�GitqWD}eAMe5qv{=@ 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 Y>WscHBP#%pHf2;u?`vVHiApW@zp@^`x&QzG literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build/el_sdl2.o b/ui/examples/native-hello/build/el_sdl2.o new file mode 100644 index 0000000000000000000000000000000000000000..c485e5eb552fe1b61e30a8902ea9b4540b43cc3b GIT binary patch literal 31624 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 literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build/el_seed.o b/ui/examples/native-hello/build/el_seed.o new file mode 100644 index 0000000000000000000000000000000000000000..7abc10279e8bcc2c092b2ed3b8a900a2223f216d GIT binary patch literal 50056 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�*zDg`>}`l{@U+=X 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

V#CnROdjw>|K=hmQ;o^fsrFR4K{ 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 hj_W$yeXY6q1-sSk$l>qM43U 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

iicB(>X1o|0=4QbOWVZ(*mq8A@n?{^KqKsN_ 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

V#CnROdjw>|K=hmQ;o^fsrFR4K{ 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%0Dql(E@{_9NZebPwINLT+b(!yD+_0(P zm^6osLLOxQY%I(l;h5BRq`wO&7Y>;~*Uxo%()C+)=3y4PKC7(j3stUp?*!zOIA*iq?~S-ihSh(Xp5qr}QgdJlW)6Y;kTZ4{!B0f_9Qc;-Ut_^kPnj>3Z?x zj#yt*NBTm(>AT>~iQzOM2gQS;e6`;1N=F0V>5kTGuSK}|_R>Ga2m68DL-}s-DpGFt zAvI>K*$190>;uoT5C1XR?01E{dR|q;p@Oxd=q%q>zFAMYV{uU^PhP3fyTnn~5@FjSj;OtVxd4twb_;MNc(??|$%iW0o!~R(JCj65}s!ivzZq8iAKF8VclC;_sTFbuJnmVnE@ITVT z$($dov}8o`zd~}RGh;lft>lJrZb{2een0#>&zce-^Rk4 zv0^M&9Gm@MFqYFa7U2Y}ne@|#-~OMp?Zsa|m9@c!H2x;%4s!0ijHkg#w9}cjPSxp* z{lUz6=rMlD5WZojD){E@7JiG)4S4+0Eu7^-jz@vV)$sVPXm+*)E{V_g{R8q!pZX*B zi+tHL{yXxvr3wCTru?jN?io3-XW24v#qZnV%Nm&*Y+UeD^wfHF?`;b?qcAFtV^*!_ zG`DEYrMiU)*a`f>&7)%*?$&Z!%-_QwTzEk2H|?I#5#xm)sP>Jw9@kwUFYpoLHk!ZD zXT>()_u^6bkn%IWES|Tvb!-1g`05w>AJVw4rVmx4gX8Jr6Qg#S4qn%fS;*w>8e+RJrX4V~Dc9MM{^^?p7oF|yqD|8m)uv&rjUU;wywA8i z(V?@V-YP5avhr2fP+(WIiOU~%<EOEK@y7)9OV(8=ySfU zWX)iV-ACVJEBjvM`=*`s4IK3?So*7o*#+?*f7Zq8#0nRK^3U*ksLjTXSJ-Tx)kp7y zmI3X>UjOA-{$)2uNuJrO(V5JJX}=f7xHTVO>xaw-=ywHdo((p9E1+K^%?CHTek=7Z z*fR+2K#&bnr_p16- zT=L!f(dQFEp9?iV{E2;b&~}MnL2J#Y)nnv(kF60t;c(JCvBLFd_4j97)IVi}K4h=8 z4zYbWM{CVbBf<}56dSpXK8~UfFH6Tv2B{Lw*M` zR)u#jc>fpsD8u31=@Ge4=HRA1O>hnrBJ|6#OG{1k*<#p9#$M=7H5pBP= zALEPu!sENejn8zYYJ4{4KXH6B7~fs{F}@wt`Af>(n@8Z^4)OF)9A69Ld+P|}^Ln-J zFFd~bSEEwW3G_4^l?1Ha_@wXrvq<2cseEoaF0enRtR@GqsZ+ zV0EBVrM(?yex(ijYhUAR%TwGJc#6H{r!*h;Y0uWJJ-m$Q+|Y*Bwyza#$SqE#W>1SU zGpA*u=4m#^vtGiNd8#6hwG-!ID)U%JF|SqTv8IB4>l&q#`28{cULC$qzFK@)zYW2c zwcHSVS=SB0mo?sq_@=d1iSHnKU-4nqD!cH{McaFAKRos=JZ(9DgumOSJB}u6n&+d{ z6WP};@B5|?uF#n+TF=)Zi=vm0B_->Zo@L!dU}z1ha;+2Wd1JlS&{eb^;mig;-^1zY z-J8JwMTh^THSpJ&GxP7%Cv%C`Kh~e-Mvn_~XMzW|H>dNQtO1iXW12N1>l4e@nh}58 z(ha0p7btCOMtsPAWd1hr(AZw>c*KJyjR7BAHp%mVThH+r$ZLGE27ch<*0k$_HSM~L z@Qc^9F@E5tHSPI-27al1;g|pO@tgB!;P<27?}L6%{rUK%_Xodlt)Mv$c@=-ZDEyp# z_596v{=%Qe?X~n^c$?$lKDaNq-`EG-YTMxOy?bBueCE%m=NIT>pZXH|;-Y6dK9jVR zw(`ey2JBMdsx>b2Va}hcz&G}ECe9hp)fNLtwXbsXR5SU5E*?=JMxpuL#|g%`wY<%_ ziW5xl$ZIdSi~bZxSngmn0>kVX?K6rO0Pk1Y!=;|VqwQnBz4zG<=J%w>Y+gm3wa*@q zSFBKbBj&>s?=!m++8*qS&X`!fY>(4t({1ctVtaa8O8u=1N|z5y%VE|v0WHS_v>YAK z($nwk5$Nb&VPvgutxEc@b>a@kchNKBVyNPa-(welJ%7F{{#4ugS4r<^f9PGUTTXUA z|35=*74Q8}tcvw+yzeLZ$@$nOX~mCSOoewKus6c9#-}y8{Hw%|oiTJyo(|1ax8iQY zFu2__B5nunE8g>}aVveX25!UX{1xEl*Wg|@=P@pmCGDS%9OsI03*uZA{(lX+?F6?N z_WwRlzM70X(9f#5L-WQg@x#S^{FqOctibooPU5F--_QKBogd`95@*(395IZ6%J=cM z?Oq>S6^tYX+ST3bws-Ed3 zdpD9rjV(ireW?y3Xy$9{T_K)zjX7gFK|AhUU<73st>9K41-T2H+sWD?!_PKxug3}^6 z@^L3swRL&Bv46w0J;C9SaW+-Ht;UOf7#rT?I%U4Q-3g2&Vw~hsP~L*L(fCq#~?$#fAF)js=`ljZ4It-c#bB@o9@nTy*iy| z-(wh^5BIwMaKk_Go|pJt27|4K?JR7n%NEXYch8!UOR+gdN1kls5i6fG^bso^1>A(>d#Ccpl1rB>gv?x1_mvVtuov zFCcxgOJ73z6qjB~`qcVn>-)MT&FcGY{!9~ne~5FBGuUevjH9-nk=O66ru=~*$_Qf*ng*6=L&K4<;Gq4-R2G7XF zwu6Fa^hvr`d|i-7OliYe!SnGvUl2T>0E`QxCA??Fcr$ovD$l*2yvC+I_~Mx^%{sGq zb}ApzVhA4?d*P#w51eID+W5d&iyx)^uj?vkIix)_E36ILlTO;NrTw0Lw1?J(dxG|) zt^GaF_!o7}YZz1E)X2`$^!@tu=KejWH}Cqz>CHN8^XM0lk5iFj_}TYLUG@QmN%C?-nWmksf9#+lU2f$j{N&vP=(JF}K}IvY&M@J6j~Iu>jG+3fX==fZoVJg?|1 z!=-LUb;&mv?}gSk#XMMZAoWRm*u|URA^3$dG^B6o&2W9+P}Mhet#9(Jj~nQFRnRx- zr0-Sq4P8d;8~GWw?`x|1mR<`_@~w|+==&C1OwvIAnJxKUcc6c_o!Ks`5h>pcy{X9C$`7WK;Q`w_D@20+=W6Z{1Q)l+LNj83!OUuUJ z5U1y$KMej2WfOIOJC z1T70_{wnx0_oZPWk_Vj#sp?zduekZoT_;yfSem3P{znU9Q7C$vtU~8L>h{l_p z^!E1G(NOxoLLT&QVh#)HR@&U(QD-Fj|31naE!uzF?CoeWPVF;sW=C;_Bu_jd&+CEi zaTRZJI?#SuGiUhhenh$cHqAS+eP-;1tJ2MdYtqeYWFuCQmQB5Z-yb8lKaKUIfZbQx z_n|qV?`=l@U(l{ot@wCe|I3L#Ae4VBJZp2@3 zPKRgTmuR6qu;Vo!>P-H(;=09ewV>Y(s~-1r0ndUHwoB2j|GORe-dP7^6{AbD&-RFiHvYw&;Fk}v*?^*^?- zm3HVMzR`H@pjle{1@>6^ONFmQvAw|mmpqTcPDt(~le^M&n;#cG2gUZHZ);1w{~xhI z{J!{=mi&6x-_0SvSu+;uVxK&r^Lg8v7uE%I7R`{0i)i0_5$l1A*td6QKYbgOFK3-O zbK#sUhi=!;M6cfI>f0FOHcu$+b*7KJh15x4ux9Ng?;P?}&)Rvug69gqe?y-4P0g-Q z#yQOAccEh!)AqHlkHXlPMrm|6Ja_!!K~erH^~+jAG?Z?h&-051!E=|_X?8e1t1Os4 zzYpE*hd%{=rJHG;KjT?Esv~_$P}kSt%=TEd*+rYM?mX&hj5_BonrICse5&!CFPN;w zJg$vA>pcLTMuU&Wa$1Bgg-2N%!!I8n;T-9b3OXuA@@Ty7eah_ts11Jg_yTZrMoY0? zX#G;gt%s1kJy$rrID@`fH)VAe`wurfZSRe%dOwuTAK6^_m}_HemeDbe8mI0oF7&)q z-RAOst1s8OQ!&C`%K!Cc){i`E96WO`H;7%Nb*FD^*WR8{*<5S1Q|+R9>ZYOr?F(-D z(zN-HY??OzGxtxM|JlxI^OL=#J>UnS2mzK?cc+;}^ zo9mFIe2#MAvKziGx_#_q`S)F|)4DbO4qy{^k{k?p_^RKO8asC5F7-1d>8Gp5rGalS zJ*@Op?rbgV?rNQCFjtKPb5;tNlM|R@8Ph6oRDZ7DRbAXsGhBxBGsX2Y#ripqeztNS zO|*~ijp?j>!>Z%l-AsKc+Emg1cSGv{A9ZPdZnavos8 z^oHnt(}YU|oZh+7Ug6R^w`H%-xBFuFvKiswbgvN}Dc@{Bzz@&g-P9MpeG^jI;+auC z;eR}ivBk6TFQ`*IC(7GhlQhrgM|tu20BBWA$9x_G%@-j@Mtf+y$kCqPRej%!vb#@? z@>U1g`W&!TzWbCYul{$R3e46R=F6WO3e)6*`g^Er@a_lR>@qyhZ^2x|*d6>;yXQFg zbAf+i4F5+p;Mc_YeEOKQAAL-AeN3m1S#clR=)=noycnNDp02~L?!E2ou0iJgZT9Yw z^8J+p^S5GuoC7F5(#oD-dK&ML)m?^VrN-oZ%+es%r*|!uV|{N!R_#5oo!osOcF6mX zx*zVmy!149_&X8As4VWLI~3ShyDP1C_O76g?mFpyL_6uitnyx?V)MN`Yi^Z~vFnv6 z`{J%B`yT2ILfb*;N=$+KP5hq1FFfAR^B-~SH_h4V&bIE|Cdg*t9fyj8;|C7@uJ>vjCt`I(jhPYn!2@kF@bu+ z@uF~InWo;aHJUnFihi;oqWf;>wmTgSa6fSfzlv88s~L^o2cHy|7hm@nU&pR;bJ`f5+x4Y@-W!wpm*DDV~zpcz=TRZte3C4#6alhyta{~8PVi?38JiT^j zV!zV%I^Q@Q^Sy?TjhD1PB>&l`mY&$aH2DBs^3k^gS>937u^eo{<7 zV$|Mm0_P&_npBT*cZt6EPLjn8FnX~~eaLP;Iw5 z{>yLvvNHP~?RmETUyxaN)3v`cE581TGJ6~J`u|C0IqR(WUm~+6tH`X`aAekG6`7Sz zSIg{L#`34jZ23Oe2KL3es`gJD$h)f?clS{ z7xj74Gwpfq&|Vg4vrnwOb&qxfe7;ua^X&XXd}dnunPG3J*T?b4qHpXC&1i6ITRpqI zp%3}}BIU6+w9UVhMbFwBnh>-huGT^Mr)6)5;WXB{v&h1o7)UN1+x7mNqO8tV?ZTGm z9#}7RH5SD)ukC_I*bKXu7=xSx+QA2GWsozPy z-%Q-E=b!G0R_nrnuI@tSV&UlPD&|^USFo*rcV9B`gC&pTd09&8xjT~;^~m#ms9t!_ z&c9Eba|+Kz+YaVD2=NN`^YIg&&(uW&mOo>BlHUl<%%j{D9s1t!>bk(Nx`HMDTzwbt z^?Y9q^fiON8tN2pY1q^6=5SwL#)Via?ew0hgONcGhxygciRsM6&^9st6>*`?@_Cz~ zxZ1m;ERJ1W=Etmlt*oO=4{*fKh}sWy@4gs<2V=9m-tk#K-d<=y-j)bBK5vii?K8%e zsgC?^-qo@Q-{D=zM&^A@CD-qkkUtS{E5_DSe#f1Ej}LMlRb1W95$Pb^idk2AQwT=af9G-=R#il~!WS)8wVI9036?U#ryg2%bh z_1w*|K0J>^TiLS^Z`ltYkKXubEKiIt%lIne(yjLHS9@1Mt-JThnWU<*ZgAt0j*&`d0}mr+*e6+*B^fXpU3V-h-L>mx_CPOkTXor z^C!L5SvK3tK_B*NoKDAgIDCyJ+hduq_DkkupQ4_JrEi36b+B}APUmZN*WB8{^E5nX zpXT{53ASX9a?@wW7s=v6*O%Hp7Qp^IaB~i}&P%8q`ha80z{4)TOpjsChcU(u*~SXp z!w+=pX`OFd;e1S;d$+pbyoFtC$Y3v!hfcSCA5w>M;F+FhT5LUR?`x=4FEen*_atv7 zd+<~?wYpw}-4dNrFW>&XpL@DNCy(c$;u+7rU1GDMn7)}z3}0ibuA{nR7}MGCdn`82 z+rzh=$#r!qf=KP%pc{7Gi}pyw3isX~@I%WHL$%Vch!Ug|~9Y;5WH-L}1(__7dk8I-V!= zEe_F2-{;U8SKsW=`(QJEe=E)2RS;KibniV=opSzC`(}Z3XrY^DG)IdDlHCtMlTJsM zrR3>5mM4OL{GG|ArFly84H12p@_6xlj(C1q630^g_tE}F2Ls=3qJ7(peD*cserD;L zVM|NLsy%tgRcxbYyLHQK=?yTc2cOrf-76Nezr0kW&h9zPMYoT6P-PkB7vj`R?Pyh&!$o{o)y2N61n^VpiME2{a(3p{tu)#8n-<+&G&7L z=D>JKYfF{KFcQDu6=IK-HYt$rS23QSD~9P{`SGj^#`7X$eKq5G=uaHa)<1tdTeN=t z2jD#BVCjC2(Gz-SQrnsv@O#)F%s2Y8Zsg-nHcPIs9qk3#4$7?!=R$Twk-eu`_Rjc$ z?CPGWcz&j%k?I<6fZfD837<8;GUPLSYhsNlUOyW4Lp{gqazEm+)}zRh;zyDvy+cLx zw|SL*P^M%0xFQwIoaMzmG&Jzp+!=D2Y+N8{rDGUG<>^SAV+K7Y)wcu?U>h5C(ergcsR z+4FqRv*)FaIp8Jxe)j!N?Um^?SlSS%Qt=w4QZFE2Te9631=%r(we=RTF4sZ(C2z6Duxas)0F zazy(+;UgH1k0FdhoGfQjrLUN-0Z)8s0;c44t@D%ApWUUPOnfmPh8SS&e#?HWz0cM8 z{`zj0{Dp#(qYh;4a;>Rsj>Pw39uvJ9Q;~ghqLK1O=%Tqye#$y|H zoLs(vU)2>}jpJx1pHb zhtk`tg{$}uKS#p#Xou?wovd+-ZgelI09~`Vl`r7%cctSqeJ&I-}@IHvVqfY0T!kQ&;k{nYxRBBN_^qN+0(O z_Ve9$=|dZlu8im9oSuCiUqwDd8-0kEUY4Hq>j#%t%a)iN!4sns^}J4fGi(c$CDhM((P-|ta7(PkKas!aU!bv!@E z)6UzWO1=fYTWe(;tEO!q9nZ0**Q5ngbFAXtvPoT6abC*Xc68nPv^AaPiH1Gza{DCl z{u{i*ws&F2WbcG?V%upWIU7>X@!jf;XAW@nd_U47*_d7UFv7ob4hYtN%mMz6tLOa? z8FAx_>N}o)C-mE&`TAJLrvv$wy5(o#vf>n{>FX6D?i^UMl=gK2&^6$YXPk5WpsyPn&njP_bILMJ=N*@H*$bDl(wq0ZMZ6v!H`Z9h?uicz4ehUxf z$NLKbuUl*Eaa8fYk2&1@Txj#P{fwcAMtrj;d5=>_k88oh>0lll+cVDY%73>QE#taI zIs?wj%XO3Sj5rS=k57w_j(#KY{Z8u1X06~?_NYQ0dG_;=w-+PXtj_>L{4%+8^U?;N zH$LWNg>hfppCa{$(>N^uf=RbExV=|_rGr_$G9V8MQ~E?SMX#5cTz{P zAs&1B<98YTYsyTIY~caKX;SgK^U(>5)1V7+oQ9Z-J2Rs1vkQjKqaU;F>PK=Q+u+ZF@=b<(vi|9*JX;}q`>)tRPoD*} zwRNE6fWE8glk)R{%0vV1-Ja#weB?E{b%^M)#?J#912QSPOAZ%HR!HkCo%r*1o<&2& zk$j&a-Iv$svul_>3t^v*ZtC+5>JwVY#=PF8t$*@f=lT~8F)xF?iYj_3h9UZd`mw_K z#c!tl6}pGplFF_Drrw<|cp7WNCA^Peb*tHy)BJdp|EmvtFGE>o=9Db49_H`1(v!69 z2*#mvB-WlfP2T`AU@@bA<;`i@Bs6w?^dU6EWY3K9nC8CQq!`#<#$&aozl}wzD+A_ zgj00)=>t0-(%Pm>N2Mj_F8007-kZg<=+_I6z6s88+d8vbjhR^9)h5uNHCGRAojnNkV?}oOzR{*|dQ#R5T8+hN1jLFaI z1$SPkCJ(6p@sW4{A4lSWms3yIm*_VlE>ICCNtf>pCh-Q%e=gq8=;kkdCst?28TUzo ztLMMP9w=@6>6z-z?3ds1V&@PSw6uTD*{+4U^SB+|N-=NrEbT6)5@TOnB%g2Sb?e+aaYc)nw$LVe;kDWIt_^ri-1Mbrr}3jzxNrxO zxoc~hv$VA98+>T-UBG&AHutKzAN(EK?P4B!adr#uOI_MMle_j;(&rBHbFHbJTlwDl zR`y7?rl;^brTZDmx+YAXe->kCx?}-o`1rQN>W(+r_!&#|=ID@TpbzmFv-8aTeA_Lx zGdFAU`vm_^+CTeg=D<{df9bW^t8HvcyT41B#(urgc4>EtGnH?otfKyb$7e5g^?O|X zE2%#sT^oTFZXVG) z!;I$79iKfJ99n=E%hjmtMLY|>$?*8Iu=S~SzXurCN$XDZOEX(WW#3HQ{|1)X71ia; zU*q_OyoYrrO>fX$#C|Y;gw#MwS zg5$pNr*`kvIH~s(zrHW&U?cScT-wXHq_YPJ2XyQxe!obc+V`12nlnF>M87M6Rk;Q+`lWw9R7U$$U+nlT?SY4DUe;^Izxk74rz+36)t8 zwcqRCDTMtg9@GI3*jHokp)fb%uca9pnhmn~o<8ioQ{-nj`}-~LTj-hZpC$1bJ+G$R z$$;Qw^!+>UTL4FA!Nhy;HC{5Vyv{57epJTYOuYxd-S>ko)1zC9(_3!ky!fq|+cOg~ zGaHVR{v`VVJ;DAdI^pMs*oNzTJl|wV;}1grI(YsFw(1e~RUctbRo~g&*vVa@P0`Lx z@Zf^?MC-R56p#IL@0A^y!rHRq`5byJKhpMWxUYu}Jt16mXAOI_WoB+%_9wv4Xx~O> zN2p`tJlT91``%=ZdqX>%J*54X%v0m+oe}b@1Y2tWYom3*oM@o`zc?$#UgH`+Cz0-* zqqWLX)*VYrI)mIh8@ZTG{aJqQJ|sDZj80144bNDHE6e1=>kLX^Ryl6aOZo}MAwOPt z$^I*Ccp+nIV`<4Gd$90C_r|lcXruk&T>A3lyOV;uCZW^ir9Phfsjpb#ji0i6TP5B} zr~RJAjVq6vFWFFgXk<8ZuS>Mo`R``+sXxt|Ejs)5?Qz+o=EZ#~|54Ej`r0@*9Fm;l zmA?#6xL*mMfp-l&)jpQsT0VJS2N%H*FSnCMS4&&<3yq)BFKut)H>TmJtn9$CjCVLY zFi!Nt4$R;;o#{M}Fe>sgC@P3MV^CkL35g6`bR?^{?LV(->IG*Yx{5-Rb{F;PmJCegE}I$q_n+ELHe@>ObTG zeH}g$4`@GU1RjX)`t!VyZvHub-`f<&|MUF5=W6+VGvyQf^7!m`p-U~lZ#!zfdmSL^qkOS@Wr-y-t&;rBf^oZol0&SZ?>_npJ|ydP)yV+U9Wvz_Ezdj4r=**GbyX~`~LV~ZNJZO#_!A1#`%5EMfh?* za`4eRz9Blq@1u_Seb347+e%&M_dVBG>Gv6|XDFB7cQ?Pa{JzoT@6+$wGB%qNoWSoJ zU`+D+P9-0Gu)d^&n|a3GdtAKVXE5U^A?iF5u= zh3~_;w!XmkIg$E0>o&yqAusWLNLTwl^8JqGo#W02S{F4Qk?lJ^?pt`hQ?%wCAttw> z4}|}mO>~1;$}XOV z(~WOZ)|+k__zA!9oD!^g0@+`vbBVzjT)p#QBl7*Q`IN-f9ep#|)XRQ-A$n()HYDdv zGDqO2Q%~cN-<(TJ-pe#qT5B9=9ItJd8!|KJW>qg9`(>ry3SZ>5h`fnDO%0rXV%;*! z#drjxfw+*B{VLV2GW`D~)+c3Z*Tk7AyT9{W%C#1tj>=gNvCfJP@$Z`%4iEF8XEX25 zHox4~g2?hq7P}%}uzFb^$uB=yF$Cn%=Em2~&7L1V-!(UT4$q>KkDY8_og?`U&oUnk z?7Ozi&3^pvqxF5rx!^4b;cT0mWgTk$Y-Ego6_&j;H~U8S+y}lF_=RcsqSd=PTaz(pkA5Tg1j@GMrOsfHtK4*`}z0H3fWAUX+T<;B&IS zV0~_Hv3mzI0v<{E)2x7&=h5TKTkbc^$(7W*YP7=7V z4&|H5w?K1~HE1bWU*X^F>YfXCu)e>8bw2m8Gu-{|@Nod{S!Y<=clhsX>sj}}!*cF@ zZ}2f@m9zH0A}s$9_5IpksdxF*?1SVrg?THcWjeRphYKFrZx^iO!8xH z%0SK(+qLx+@)g_qaBk&%Cik+m-GE0YW7?SqH- zz6pG_-(9{pGQGrH2K|=C^1@ip5zbCt!nVh|w#d-S$Gf>#^GrYR#8Zv;Vzq@1^5dAR zlXJG>r~Ek4^aSd_%Xp4vJ*K>-FpqaTC@#OypU>uf4F%`>O7?VyQgfKMdBXik+KV6R z7umue3D2Jd`^o8GPb(ekn(z|boZ~Ba+x@huzygQobdOnkA!+*1-1X~EMC(^*jgsPB z_QD4nmYK)=hz*;$tz+@I)YCdsW5~IE5D$}aPb5B_hgOSwfur zQVa46-FG2E++J`v6`*}Y9l_s0p6IUUjihZJ*Y|nATRIa#r{U&Z>8ttPz!QvW+Snah z@;|Auj%KW*JMxt@FVCYD_q@m4x%gb-h!yv|v44z(xU9{c=qKMZaDKkzS~8p|+dI3T z{?(TTA9#Yl-dWMWQKBhz(ADQ=&X|||17n5$cCMqk-D9-*tZN4WCib>Q*{^f9Ui6S0 z_s&Y$o$|H7K)#+(zUuUm|L@fMo!ejdiTXgt?{~0NPiqg+U+3h)@r3@?OtphX*2N#O&qlXv!S|gGXX$fDx6hA2w`H`s*tIj;2u_o<@!x%j6EHR z#ycK>#@N|fG#*TA&tf$1Url909!YkqWo}=zJOaEX@TWD?X^f#x__F4R*BkNvp{M(6 zsF!hdWp7m{R~N5`?xUU6Yn+!|fZnzU&K%&(;qFN){;msaP;|Cq@%J<~($eqeTv~0O zb?Ha>6`i!_R!B$L&!1?Zesio_Wlx=-mFRS+16NpoT63Jq`z77ot6_Yg zd-Ps5H)nerBtJ&{lD{H9=XVAVeGhE7uX8MQgqQ5}ER!eh$oukd^ABT=^?EmU`>v8)J+NgdPxEzQ7znSqyw>Fh{uf!y^ z-9_6K@=0d1FU!1Pe8y(0zVII9V-3J4lCSr_d>5FR(jDJxI0E}N+x(8TYVX@-w9Ww5 ze~QnPpGy72j<`Fam1h#;zOMAK3-yJrs;`K)23PZ);4=5=E~fmOfneMpXWYC8%k}`} z3u&yp`*nbK{jFUwHr}`6{a7)M$ebTv|7+a4#K>b_c=;^9pUhrhHJmk^vp^1;4WAn2 z3t983T=cg4#5zlf-dla--PXVj@fj59Wk#_Z)~UUIo$B*gt1cJ`4>z7Uj5&eB`go>4 z`%?qAHLi{JNUO)SV_b~)j1V5{?>`mZn%BheSX-K%3CGXg`M_@Yqx&v<;E!JuCUwMP zX!Zm=u7ztgjU^-RMGmaJc&PU9`5q@D@0N_z7;~{upPmsD-{b#&p6wKHr1@D4P zSDrcY>r&aFvR-Q~+S0tn*+%)XV+|H-^I1{$SKx&2yO=%Z_Tkq$L2R;9eU9UuscAdw zyT5(5xjv=i-VR34=Q2GzI|7at(-V%w>lp7+Vx2le#hohb0`uO$DB37)l=~R8#D`G4 zU$im%Mm^aHe7q%=M|;yPy))>kvTS=Bq*!F3KAtm@JZLX^yo4FIUn}bbG^tUq?&e!PqXTi6}^!y|-xUwB< z&fK1AZwa6CL5xoI2P*7da}j%}JCtTC7^CNdw^NFbn4PCgc0T9quZ`{Yx(N;AqC>F9 zFJ%_IF59rZX>AiSwi}pxXm>6A`z7so-}qW=elvEdxrzE2>PZ%d>)XpmL4G_iw6@tK zTLoO%rzKan@-wC3>B%wA0`X-?G~sQwhs z&>chR=ADOm-Hh*A3P+8x@DOtF(6;8x<54o-NZ&N~8eM@Wx@H_*KP9-T|1stU|4vT& z$uvY+-`ADnEBk7KMoE7UN}t_*Pue8;4}iymz(tXmACV zrFoyKaIy43q&Zj5dOhFhVkziRveqh+ujkD8hT~)xcOxUFm&l0DOG(}&2Rb*Vvr~nK zTAHitdHq#?`%_2t3^&1+t^s=yy5?n6@}oIIHvB!mte&%j_?BYm=&8jRS|fMYUiw$h zoSMpFBjRuIQfJt;1^O=A{yuQh+Tt}%|1?iNgFWWm)emZJy=y&o8azdhoSR!?-JV`r zNn5Sqw}7k4<)`R=wWEKcuSbP;8#|KdNgI07hTg(U`Hhj^AAzTtzYA=wo<9|}w}j;{ zQ{VfCDj%Re&)VncWlmw;yJhAR^Rf>!7R~jK-yg5dGKa?gl#hd3y>HLU-s$RX^7Y2Y z^^OVQ^_J`XT~$5*o+pF%U-Pn0J9r=U^|ZEtKI21p2cMAOcek(S@UuJW&_;8g*)H0` z*XBfqL`TUG^k#p@)~b>n@#7bJs%58qS2&D64W=_YWpn*^9pZBjTA%5xp}hykd?{iH z_P&iD0QWhAJ+1WZx_BRWhm#4DIqml*YtN;nTag`m&&^e-o!_X#CrKaa{H#mysp56y z)9ammrN6H9rLynNcRFHz?v+dCXW(;-@7u&tB1KjP{@^1lK@uiK_-$34vc@~{= zf(MOq^lSH1-Lvnz)!ACL@p7=2wqd*POFtcMX8Yv_-W|2CcJFkyy5lroxbe96KPPQ8 zFQ^T^L!W;?y@y9TUzSSBz!}-grH^p5x}?+qjLm|NkB!aQqwhZU`}McMqK^7dIsNGS zkIOyHT)A+suCu_6KgRVE-Z|j#L6>ZA9XO(6;U@Ge71?(v?fJds_od|0mpB?9QHp8B zGjI}mI9^#j#w%XPhHE{qwBUWfXw0~2m+L&-^13=N##ar-d_$B!D#Vv^qn&WoyId$Y z+-?YPQyuXin7=eTdvRIrJYKF2-}12i3_Bk{+F+fiwZW04CADN=FFr={ZU%>Y+TotA z!o95ucgibpr`-B$3vgE*lU;{9&qHY%*U@=qPgA80)|?pkW59iD&!q0MOiwc1maZl8 z%(^D!Xp7I!9U50({76r7a(GeFe=WV4TctOn6PjHV==5Re^o8h8*$=m8`Qct(RgOQ9 zoI}yG{P-HWdPr5DyZH0=5W3+!TG48`La-9?HIhUST+S)UrTQZPKFG&dHLo! zw~2bdi}9Km>nP6^I(kl&Kh2NX><;yL7gE327xUsxXuy3CjJj#~4GR-OBm>)A9lxeyt!7a9B zizI(N=XWzFp5*CoJ|wZj5SC(zMg!uHx%7lmuIHle%wh4Gena{K=Hn%$52T4jAL`%$ zv*6bJzWfuEZ%Fz%AKo8XI?m*ubmt+Zt*wW|GSIm=k%3YEdtu@Y`DTNdUlYD7&+O$P zrQc|*?m3}nGUvtZ6MDjfjV|tJZOo332aof>YrNCN^AbENb@9BiEWUCux_EAtF7jNV zi#%88BF`1N$a94*@?4>dJXh!<&lS4JbA>K~pXuWGDqUnej1Rfq(*?iuyF9cpl<9oZ zM)>(K<>!i@^<=g3J zzAMkAV>~YaPn8LW6V&f*uHV~+>6d)1koidXS`OP%uiM2jNi%=xsA)sE<+3R!GUi&s(1ys zSbBC@_Y3rAyaWH8q5;odhVDEx;Fs!YjR23fPmJ?527S9XgdzAtXfS;rG$3v?A`MhN z5)D*75)Hz*6u1}-6wC4NY#f0G5BoU2S(tJz6 zbYuv(IkXM<{uLw=C@C|>2{8Clh{=dHKvAx?t#TK+=Z z!ZxF-VbGB<|2q@Cv7nK}>Kc!I@s+EY<{ z7ESBKAoj_c_K?CR-s|Y$jRW36Bfxvp3E3-&>nsi7QQyWvywTu2gg$N3nJ~9U^AP;? zXWYKhl3(G{Hb!_WT>IKMV-<|?Zy2Ls+MGpv<9^Px4xa7o@!80P@WtO?PAS`C#=V;| z#UwTV>|nkL=e;}q*=6b&Pk?7<9KdOZdk=&9GrGO?gzP`6&of=0XVND)s?Yv#4gmk% zl&Q}|mZl?1(~$*qtk2Ca30&f0p8+o4)R<^>@I8mrAr|*Z>Kx?jAX7u?5Qh^@gpa1ot39~U+3=qoPY7}nU~3je#B2ND@`k~ zFLf&M)>Cy)=2GrWTvk#{@e_ZGPm)G{$?qahG*Ap~GIPJ`*8f6rxcHsjim?)J%Z6vc zc)trV+Y~Wd?i-Ab28yG>cSiXPzRuns{ZaTxKK6=pXW~Km?0uv5g0{E#v-j(C_P!sS zbbfIMzpAfu{1xZ_s`?DiJOA_bw3)u|^UnK%Cm7H!j_G=TAdP-tU;R8$&~vc%uxEJP zi_F-0TW~j7L>>-@H+GKC=fQvWHcdtp1Gjp}7I%);>-{=O9`RM;@+K$B>zlrTAI-I={O|+jQx|q&s z4ukK%fL@-SWts)&cw2S8E7P?%UHPv0O`V)^;vDyy;B3O>oY%5_lyv#*K?COk1mFBh zd>-#RYpzJj+HTG z3YjWa((u758rH;_z1?APrfgW8DH|5&V83{uIA2$(2f7&4m;32i@78!d7G{I1gv->bEt6N4cHF-KUmp4WRnS%W5a5zQ3cu z!(dUi(Ur+|=$qtr4@>R9eY-0wob2pAIBO5m+TN0C-{|a!D{t4kRg?xF(}Nhl*gklh zsPXx~uv}+{^sF;+vcuZ%EY6DGW1Nv)OR;x_4PM~-v{(^-Q9pIoaBn|Tm+!JMq z&6PdI=6HLo`vXatYyf3m*DA_n11R&lR#7HTW%qO;(va@`dgQI%jWb!bCHt>9um-zsm+2g>Lp|559G#{d~ ztHXBmmijP%ceK@HX&W)vq;8Ow%p;+OWlp>_}gQ7 z?o({2^7kaGH$%@#|1LN~(phyD{VDYw>yF;bv@y*Y0Ajg0FSpvS^?UuuS+14uzSD=k zMPp-~tu;`*2cE>sXkR4X-Mz&I8c$hJ0oQ9|3V;`xsr>bN68P*{#>E*qm?cSknc{k`p z^zuH<7X@&WetX&Q^h)9~>O(TaIx?9%WXFqTxF*}cV*L}_aBxXzqq^Xb&_>UUXSvfO zAJ-3M%j8n_x<2~iMpw_%<}&dYJu$bXfG{MR%5_hZx?)k~h{6X{cD$PN>gn#j)&jy!YxtRpAH6gsyLGLtrED(rbkX`$^{r0b)a;jQ)HyJ$Gi7S_@fvl~ z)Y*!BiLSX;-lvHDv2~*EhB$o$pFLMN`mVi~H4O95-rIun+ntp$gu-aW3R(t!PFG(aDqmW;%&+EYV zu8U8|zD{r%Qy=x02q)<)=^{9Y2iRA`N$@^OUBQ;GQH{e#=>Hyv!vZ(%lY!T8ApKi? zzFh@u-tRlGkl2LibF%8uH}8tzoeuO1o|^OR*Ue+{b8KGC2m0;ukXw^a)$#L-WLx+C z`tS>MADap>Y_l(wyt)E>FLgNEnM?!5vu`1*uFL;7bV+?4;>Kk( zI)%Ed)8p?7lw~WF>+gRE+{U)>9j!4{@v-umMxA5Sdj@|DY=5TFucc-l9P`HeP&rrk zNAM|Ahwq9nruf@ycQH@mlleQ;!q3_;zuQ_3{ezz;h57Qmt9=8;W4?iJE8n0mIrk+$ zZd>yTcP}Qy$=8?vD7cBQg&(PN4BLFHOwYjV@N=S`^E`*;&#-n4pLeF(BYeJN7?Wh6 zQ9OZ13*A{~-H{9P*SPy4Gph0n?oN=_5Mlj=srU|zzZsLmI zbsTu<9#f+`%eb%dkbWOSzI4u1_F3n|AMcCLIWDFT$;RY*%_3 z?GR4I-XC=gZ(^mw&Cj&o-9fxbdW^nFpY)Dn|J}V`tzkYO&&KQK zIMun_(NM7CHB$MkC~F%#XYBeduNAb80G~vDnP)225tQ|Me=RK2xG7_fFk4!nEEC43 zYK>8KB+JC#6iZMXH(7JF98PTI1w{ z_4L5U7`yE6*NF{tx7X?#URpo9v&)*l`ewHbw67$-J~p-yx?jk<+f9yS+l!P-w)`DK z%^zp+o)ev=F4A7_q-t8Zdw$b)zE8ou48?4Wwwf0-MqnxCBV6qnT!h!B9bU;DLzR1* z#ahqXENdrvvaUCqMVY~$#CsN;4<_HQ2wuW-qn~Gir}K~JJ35@D{R_=M=vu+g@uZ8^ zZ^NF|uAON27PX=5mTKCu{#)(&tacio8_&CZUCl+xXU_W;;}))hc>=%5Sb{jD%8pan zCj5d4;3s<1K9j0@`9r=h@*?tnfV_KK5b|1fP5o&7)CW)csV_g^80iOj#h8cj-8t~? zBkwHDod?mM%IRCQOZ)-y*3womwN4WqEqx+sWY*1lHSb7I5{{#FNBUyk4{#B&i35h5 z<@a-)<}k$${8$v%(7Ms?pLDVoMd!`WrH=a5e0K!%Y61B8Je#HK;JsFJ0(8t{uj4Th zn*yfId4lO;92P^-IEL7Sp2~d+!Tr}2v2DrVcNTeWs<*!O>~eCVJo(OzBt{u6t2%_qZyZEpOk|BS1Dfv?ZJI7s zSmqo4!%y{%>0qy=YTeV9M*h?2oU`j{Z+rmfp8;tSo>?vW_IA{)^-)%oS_Id_xYt?gb|=ki;Zb|-cW80;BO%-!_D zeEHEEoxP9^QatBiUj{E$yLrR_^}2dtoIY%~-L*^FAagb^NS;aC7?3Ubo%US`Xf^Xd=hHDC`n!{> zz3;^MbqD)lWcB^_>|c`7O{e(yzOw~@ig@`KRw=Ed&;8%q%{od8YmQJ=U& zM_7)3bVd05eeT5btanf@B=(^3DXn;e(r+MLAjY8d>q%#b6DY0wm}^}6wWJribQ9^E zODl%a=+d)D!&|eb#BVvj0bj<)`oE}(FCiT*HXYc36{(mv)$$`b#^29pbXJCbboQgb zIWggCw(zo;cU~8?HlQxw){O6{Z}RJZJ@c(Mi=`ravV(h(En83QLl$qKo@DU?CyTl( zTcnM?!D)Auk!_v5Lyns-l+M?Xn}@4t;bpYX>2>wEHCI?3{mNfAI?$(LG)I7!?5nMhm_Oue=-kpP>|6Dw$;U@o*g@jk8RPm|0*L-|)4Pi&t8U-DJp{LlZFaQ-s*4*gzYfO8z1 z*^AG*7aH=1AB+FIE#3Bm!B;V!`-T~h_m}ZEw)nNoTG7zOgz-OY?P7Zv!_l>~GJdN) zamj-BnY9*`|Ed`EyM+hx+@fzuRlI9FejlpoS9y=Y?j!%h3xf|l#-I2qndwDFS>GqR zsAT2+iy8Yy7?SJfD@p}{XZVuTI-Tzmek2%<7nF(TJtb>KG@M3;` z!t}&+g!jHU8mUZU`~$f8@?m`(>Lz8?K2BIx?c-RV%pcxw2y7d8#eRe8$bSJ}^KpC| z=GX1s#|i4_dxRce%~1=Tk4=oL?59%4c;euipX%XO`>9oR%}@1pg@f1S00;9~gJ=2P zo}SP*-j9$^9NGd8Q+|%~Jq<_Avo}P|yKmUm+{HYU<2`tbXqQQS=*YsVsF`<+-2eh8rQ)$u7pY4 zFAt3^j-b9I!)JrvSLv%7&nE-EoEDA)-q@ZBe(b^n;CON!ZQR7+cht}+;>L+= zs^0%$@BQPfuB!e2_u(*d1|&ozLgdFt#u>>=1PFx2&{U#UDq@JJKvtqu#;tC7t=rJp zZOpoH(&DC>GhsE1nP^!|S%6bFEG@*!etzx@Se}7?0Cwxl)oDJ@*Lv^$KKp&<3~2Xr zACK=J-#^adyx;rB+H0-7*4k^Y{o}28DEoO`EU$dmmDZp&XNpEM=c%=OsQPM0iziWE zZLR#_$*dj4KD_o((bfZkmy@5maI)>u)VuudRNjYY+q~6qWNgmiV-~$la_^)q%)52M z)<(|NVG}>7eeKx1wdlhud*kOe+WnSGU!wDHKAkpiap`T8e>v&e8~?grarXypL}Q~q z<{B0Qjc8wRp5c26t<5Kwx|=hFH&eHrz4Q5+*jBdke6QB%sjHZ~^6IIF3?J0`a?ecC zsV94VFE(9m^v=QenXh}IN8X&FtiD$v`~CF~Ve_%qy}TF6w|K|=$kwWR-MjpjHfVU0 zXpsG%)zG(E&%m4%(C~?XhKHMhI|Dc~*bDB)9+e+MI`f5X(X?5GvD6(E)Yn=!^8HE0 z_Fa7Xuwnq#H_U^%i{=|)KiM4I&Shmbw0YfCyny4LpM%hE zeE?7X@XLYCPx148qCFgDgXW{-)H68fv%y79TZUKQLz?&}c}eb5!d9+zxS4NuCpwI- zMLrg(cl0aq)AWu`!K3BYj^5FwCU5t^C8162xjk;bhPEDXRKGJfsNUz~tIrogb1%u>U;uV%*tb0hPP8|ic5B>6P?$`Y61oJMz+?=m3o@0U#QELnNNrHhx+DYch@H;*YD zzJEO6v(ZVpXsqJ-!98i!;*`jr^t_jLjMv!wUefBZ0h0fR-F(ep9;0lptU@p8(XGt!gyyhSC_!lmzcqlfca!y}u zjpB<=h86!y{2SS6#vE^_Q+cw}pw0ms2 z@fCJCyu&`~3i}h@F#edm@qN6`>HaX<*)Dw;CmO>!U@&j(I(B?targElo~`|uu?>bs zn~(h4F9tTZ@Y8(cPqb;gq|T7M4D%%SAuq!`$$iL6@=|hNjh8ATSNDh=#kN#udD34Nl0yl$_bVV-Cp&$n%D$QShU7V^{Rol{Ywr_#8fJliy`y0haQs?%+zxZH6WlrhFyzl_-ua1V(GNF=^D*nSPKK@*F2uY2 zzS~`XtV4!+<1y0qkZ8}v3mFIGPgz`pJiq5+Qxo=f6y+%+J1-wZWd?p8%g9?@8|o*m z514$?UsZX|ylzOIlQ$&K$s3aARpyFe-=}zv4%Q`mV~!yofe$WtDfu98NL5qwy`r!4+X z2WLZ4HUfFQsq72+Hk)bh1Fp?!v}tz$Hizr@z`^&`U4Sz-#^ZKxm3933|8_QhPOz4T zyuB6K^KGr8oj19*Y%P!cE8TlldYjX68;jX6HpF%{**j>wy9hYvJ6Jl;qjBdM+V(t= zKN2C||45qOQ<5vkaSTQS^r)wJ~+9+qrDwRr4nJRI!a-;2|3caOF0hY@|) z%D)%ZQg?^7zIS5uVXOaMSpGh2?X|X;^bkCdY*qW2KP{VwZ_=W1+3`2Ak-UHFiGj@z z@UwUw@ArQ&)`9f+9mL}rHP1etH74$qX8iTvsg|C0Ge6Tg>{i9>rbm6do&+wwyym@4 zza7}D`B8hy;NAH$cq;}T@^jihVOu+E>~ zC;WM44BS-r@3RZ{=pERTrDM#cS>rdKu*iJ4cUC;7IMv<%B^-K5w|R%=c#Aocv6wR! z=*|*1Zd<#IM;p_n2dM=6Tk7jdPbVsq&1mDnCx;8Aq2O(qep8pN}K53Lp`-`T#(lWK3^4X^zWlT z6McaW!k^>}dt<~mcAED7;$LDPXyga94|3$~y_1?c>qr0KHp!oGRKJAh&iilJfev%0 z{D5HZYTwCvM*56G?lH!XL;ie(t*fmdUUV_^u7K_f_`jf`&)TD%;`#A=Ezk9|FTNE& zK`i7G$Yv(~ev6)SZDYn-`uW!9hgDB~+}${=(9@VHbcl|NfjQ`RYT|R^}nK_^Pv7O zZsFFZ?lkLdQR3*cX}|)A0{AQQsN0QQu2PM}0HLvKMkZd&nl@BV0v154)D- z`;9%>Y=_P^aIRs~joW7zN*}-`VC$}?uh#PGUKRC?fiCTP?A|}DkeSxBcR%!v0$1k4 zW}61~4_j!olfRNW7rS~&1K&Qyvtrv=?g#O8jgzY$wTXQibl*ke2=?nvV!!Sw^zjJx zQI_jlKdKMK>z3M;E)Oxr6CEx?gL$#-8I5=)uyKy$3R_M-rXG zw#cp|@#)4Oj)P5V%TDFKy4cs#dXdIajnl~*uD30;x7&SFMz+9Y{+)9Q?}Epze(k`O zog+Q+W!^uha4luJgEAgp;XflR`>{EN)scyd~c+L?%^NY1$)796{@}_T9mFTx{BVjr4_fQhP+v6+a$%pHOwhe-B&V znhw_yDv84AXYT6C!wUGnmz@$G!k&wn_E4Fozx-mb{p>&7%2OEuo7{o`_c%a5zS2*y>P z_LCsZpZO3hFQdL)eWN4UW6+2lk}QYi9DmZ~0>7Mo4Cj(9!Jd!H)xVENkD1?#f1|Ol z<6Y5Xr)T&+d^7(w{NKWVE&p5iXAJE4D*t2o@8aK$g*{PpsBY(+!fW6e=a{#%R<=Di zG1_pg_mv#%c3|g$-45(Lu-k#12e#Je^1yBfb{^R6z|I5P;J#>XVO9lP=BvvkKZ?iB z08rfi!05#L9W&8=OZ-JaO}*ypjcCR+bV%SYgwHpcxa z9Xe_sI~h#(@pF}hq1)(@J)~`|iO8;2;!u%)@PC;9KZ76Y0rDfCyQe}Iyll%pX{}54 z#nyYUiyBW&P8b)s11uib9sfnA#+C!{0UKv~4Qs!kHR#%oi0{W%qkB;K?>*1*KYgC% zA9$YS-S=;wGyXNt6aN3tpPwNYi9PnVAVStEHn#QJZ>}>CP(f0PPOSk(um!37}vh?^H0*C!K z4_do2k7>^J`Wv3Vzb?4{lX=sVj31geb^J3n7kfGJalZe_FYwnBzJzn0yHtLL{ErLq z*Dt_dAD+@{DSQidGa;zWrp%?fmm89$pZx4V*bKGNGneWP+VHc|Kg^uXh@qIi6KJ8j$fcj8UalBn=o z@szoa<~U*9Q#HntruCT&aKs9LHUM@qYNqeUY|~kWHWUTMx}zBiQYHrXq4`dxAK3q_Je0&Js-TmFaK8w2;ZLCK<``EzdzwpzXMQ3tFgZSXTaVXu8TLeD=TP5Nc)lgJA4XY*CbO^*06 zxZ8Z&h_&Q)nkG~nLw}xw|nh)8$4EgYD>KJq(-4t-Lx zIoIl6SsG`uLyQMmkSyKo`bT!QvW~?PlulWbA7~v)etz>l^>agC2V;cvS<-6xQ#9mTw&vpAutvv(Y^frmC)Y~a9{rp}w0ho1L=QMcPY&CFdA-;HP4MB_Jh zk2M0L9i6d0@H`o;L%}(OO54^oWxMXZ(|JzGtLC=7mqv%`Z+cB9I=guMXfj$e z8%8-iBYcbdCC|hJY`^ETA6k}s@q_<}3^K>(JVb~KGM~bQcf$qQ|Mtk(7Hd3ehev`% zi~<08Bhtrpfxnq5b5~hs zR|jSI)_gM7GBzdmE~s7YNl3nR*Eo5*#zf?9yW%bNwAahpWicwvw|>oX1~@}}=1a^) zNmp90)-x$r&z+R~{xO%i^D?a)Tf8QU*OImZOY?Tgug)5KSirTteSAx}oKM@9PrPPp z;~m?*&UR$)*nU)YyY2H@%B#nVJ&_gi*qODGicmc(vaM_sq-x()uz%j4b-uEo!C>eQFd0C zGHRc=U1vE)qw&R^V{M$eU4Gm??8$!Yh0c%|9n3LI<_qT-tOL#l^kSFIHmsdu_D(*4 zXvRP2+}gO>>|KxFZxW}w^`Y8>$vzFG*W#bbub>~+I{(MuU?1I{nDV_Yl$YH5xFq(k zGpDsjaMkxxWFJ1JK7`kbO|}23dD+s1&MtwI((*Of7aQ~K+2Yfdjx1?BvS;bPY`M{# z+3-=17xm316h2b6355|cf8c{`g7HjkgQuP={Tr|I8D!#`P|xY}IB>|>7kj-luX+7<4tuAc1R+6vgL-$8$TU%dPk z@eP?apQ*bq_L<~g_8{Y|lc6ucy&EC#$~2l^-%Y!1$iWii;B?wVKh3|lJBYwf`YE1- zK5R-CEAZ+;FQaVJ-Xs46Zq(P@s#yF`Sza%h)rrCEQauSAnZNAdiBh;EI0r zGj|)XZy=yGsOSCZ9@-au+Pl}}=<{brWfKD3tdPq9ZiTO@wLaU2;b8x&0`~Fn&HPdL zAv=4g!&~h|HS`m>-zR>j^dRuF8Ux#tjqk$qbnK+9osN#^$!ON~hhsPkU7K24w>b^A z!>=*P?<x-Q(msH}I#uQReE;UF+(eu6~xCQ5|TLu1gOdapl#{YFAeCA%hPcvQ72a zBH1VGs`aUS*)SI9`OBF2b?K4pmf0}3Cq!xVe}DAzwO8VQ_1Ezqy=d(e?3B*A_Ahbm zs$8YL80gaYKa$wWFvT#FaZC0^F+9bjx@W*|*H0VZT~wY6?9qPEQ+_nC`Gx$tXGVP& z(MFN-c8|w&{uz7a$Luc0VPb!Ybm4{p3-215B-Jbj$X_vmd)O>pSLs|DkvI zr+4k6k}Mp}msl>?=tBPu=WV4A6} z9r$zSC)FR!wSRka~5B!dCjGzORzz?KmW_)xeWJ*EkX_!HS|5AcIPvG z&*3*qcI491`Lb0W_gU};{3Xw)zS5sBxU_^XsI>T;lYaVIM$>%c zgZC#LU25CIx4p37;YifT>{YW?-Gd5l$Pm!$mx6~O`Z4H79jljwp7;Xc<(Juo#3-AxAu7R1ryyHjNhIc zqZx&d09!h3G;()*kEih#XvCj;F8KWuFl?>(xz*WrW6R45*EpI)*YTdN0zCJ&NpykF z7LSkHvo-{;nP*HcEC;5>LchOlh_crHxvu@+tGswxca*koj`zzA0ngf==i5GdS!?@r z*EZu_+;$uH!1*{1^*>6S#>zTFMn^E}M)<#PbNx0~K>%Jqys zmfjki&j3evJDcqG!WX9}YTx#mRJTc1_oLKR?A4!RlF!jAIl}IL$m`$p7!TGUPa5Y; z9@+0blUnMeh4_Dtok_?Cty1F1~{8JI61uv4?Tdk73Q; zb}(kSHG!Ke$FkWW?u=Qz(5*cptmpN$$!4b6MaHvAyhnp~(bZQOJCDVC z>ksi&n8$bX{O`b#-y8cYwl=fL?==?hk+tQbIZwIRU%8m|-ep?{vNf^K(p*08pJNZM zuN%joCgHDW9i4B*;UDUoyY^jno++E`%l2u)K|WTFdFxW@kCh7PjOWip}$%O5B7ypzO`!I*S55pa}u}?lf z@X2RH{W{D4H~5kGR`RitnfHiC#5^U#@{5JH&O;>qto5Ul+_^lTE+6Pq_)-2_(=NRp znbFg;J@mlW8n(yZ)0Sgw z&G>o1bHI6{hbcc3n6lX^nDk=~9=F4*9k)zcVD!1N>I?r9&i|&qLI=#wApeP;I-8T| zDet{LUp}Y$TJs9h&vE@NxSCfe=4^UO`8M$dJv|?sRbTCxo+2CE*^}H)D4ms_;YXRC zna@+v&KW^Fo?gQjS``OB0BrMVf$e=-Pix47E3j=IPt`YjK>g4D0DCYdrt4PmcntWC zNainzP760sedSl{R4P zF>Ju{SV#Ypwx5p;xMzd20ZvD?_8!Vtr49Ig0ROqzfKOGx*I6nL|L?T{O&+Gx>lasp zse6T;T{$M6Z<|_JUjb9|hIY<9$&RG(_4WnZvSZ88_C>lU8}lB?!vE05{DYIHquH33 zga7~3#+($i<7xhTY|LwhurbXKmu<`q;`ei~F{3Cq$i{S~)py3Ra>`g=B#N}=TwPV5MQjz#p9*Y6i*2jNhFQGDy^ki2`3XJaYmWZmZN;1Y^AB$OD0>S= zv9EBw?K2;Bu=o+5_wa2gU$Z&tB=%+B+kWPq;IQ==1cR@7_i4Ii3#a zZS>Do=w*%{^QD5`MN#K+@mqQ#U#Egjoxw9Yl{XZf=$7`-h|Ug2r`b~0;|A?B&6d}_ zneQG-&pRd~D|Rn|-)FWhYMNSD0Dg1%z1`$2-h07a_H)&S%GuSeFH`)&<|ftkv9vbu9Tz$^H}V9qu}1`&QPn@oTiMz9HAf zo)~ziv(d?#InUI!HFf4Co$`Y$pJy8fX3}5tv0w9vhds1$8T(vMW)5WUVT|S`?ssB7 zWb63{7BzHMTR$((apx~t>(`uN(E9ln)|%1bV87_L8(XIqK6f4aMUmTXcp~14C!d5T zzvef-lAr8d*h)HO{oXmN^Apnc@YA_^@jrtehw%P|yxsgnvtsFCKZ~zx+}v~{xD2@_ zpGBt732$>|m&&$M-%_@{C*)acbE(zBgm@O=KLC7FZ(ENVjqRY1vEw@v$Fr>ciJ`hOv`-b~Fkx$k{df2mpKV@r&$(qL` z;?U3^+PHPjMlA zZUcQXHR>D(jfNY3$PfKmk#MD-?s4zCv+#zpIajt;XWy`uxm_q zWZ>s1cBr$?@+E0M?g#v-B68e<99y5@%joW2uRD@GW24qUeR>b+O@XheJ+^+G-~03?KQ$)j`ZkVqV0~*Kd$ptqH}02r z{2jU8pZUsh=1V>(d!FL`O6Vv0qPqeI!y(P!D9`$7FdU2P8?Rj1(L@6>)=XW-2Bh{f)J*s zxry@sLHUGc^fI;gi2Uye9`@1h3vjyE?#~L)hQ?vHuXU5{$>rVj9eRu|`ZXF4o&2Zf zogwaSEXQtmKi2M#TKNvMs|H^@y~Mp6eA*6aO5Kf)Ug^Gro#6j(!XLXLd9S0tGW6Gh z|FUt_bl&KKKKY)=&R*Fa!IwO0?wX&dd0n~gufn>d%N`|lo&7Q1tL|jz=0A0IJ=8OY zXE%7hxeA^;!?wU*xFu~J1JC=yx_=LzVgEo^#2C0pII6F_Y)$m>gusWakRczpm`44X zuD*C~vPpg8GkpuM%*I)IorfFF%Ln^P+M^lv&AVfo&G(jXB>QSHF42z+bTiJ($6ncf zG5SUKyy>oZ{Bn)oMprs65RBIq-y}@t{WH_^ew)`jIgaP^Q?&8e{Qg|6#;I^Uh`AYQ z#TibA{Mg64$>0pl)sQjIdbFg>pIco6h*Lv5UBlpeHqTbnaq&nxG z>U<)lac;h0Qt&>Rd+eqJ?{#sVt-z@f?#-MjW8PA)xgKM1yJ8j0UAHQxfFBKf?qOBj z!tLj0Zr?$kkN0Sw+Fvv-41%LNoK>l5=;vPEXKg=NZtav@^uEveyS98A@tT^0pS>&v ze=BihjsN}(tH0av_s&NV44q^3WyD*(3$Hy6FYN;}J`d_6c&|QUzAxR&%SP~Q{X<;L z_TO9EQ%c_!Eb)N863?0AnICF&i&w7PS6w;iX74_CWzSSUi?)Yv(-~Qvmu=fJ?6FV2 zExOfkni#`d@5aF-jedK?w;>s!9BX==4o~}zl;Jt6fx8FBv!DHv?K$eSxjGsvlX*9J zdbaX2DW9c$_LA*evn#fP_xb~rrS90!CvERKi88Tl=1vRla#h-7aG4R~V(sv3ux9|f z`6%`3s5d>R$FtR&E*ehS-aUO-LH1nyQ9S4pcqALUwQKqIqW1Y{9%At>-WBI+g70KqQTikM67_9p@hzHMPA@8bKKKy->YX3!nfI}k=54>$x%F)AV7t_g?b8&z zpq-xx-kibh{7C7uXlD-X%!=FTaPpuuKb~ogSkKlD_FnC1oY%W#RquM%8M#50bh>w)yhr3-)aO}t$V=#dVa z&Y^?qbNue|{#M1lnU`q2F*Wzle1$P2+22aO&8;L1-I`wTXgsu@YJ6!ATSMdI;?s!Dj4Hhw^{%8SvXGh^Pj%w)7O-ctbD5K0-oAgz0{VSHZoJ+nzPvpa{+7<%0Vtzir&=1@}&0z2GZzRxh}x zarJ^PU%Yz3-B+z%(DAO-3l#g?%KnUEc7*L6EM|}E$k))X*_w`gt^S913>W|zFz+?=*XX;|Ir=!+4?`eBR^07 zqdM{n_0OKgv+)`J8a<|OC~R@((-pJ)uI?y!*}THWYqhWSveKc41~wnyr~4;eTyT$% z|6=D3u5tZ+gUYjCr2mFDu*cTlNm!n9aE49Ro(CV*$$zHS>~H96`}xBM@aK}bLpD9nwz)EPx;RGi5&QMX{2}cr z{w6eiO>`kMiZRH3KL`!-%ZjpTetyBT*WulYee2Ah)8TH}Bw&2r!70mXFsBWCQNdOm zROO1)-O5>Uof8)yA98D`?WE~^xXmx^{P;B`owJw=KO)DkuuN{Oe2%H5956jx`9i`m z-p?AIeF=QoqwuQ5`HouOdINB2H$MLlf5JPhQuw>Q$q8=Y9G`y&e|S4w_#YR?q*X?9 zHls5rcOmdK2leTqP5#MwN~fHs%hMUQzt4@89l&1d`9~b?Jf082>xSzOo)ulc^(u$nVc1U4Hz-PX7dFBWLyYQ`g$(UGs@0tgS1)_Zz{N zyn6YfzWOI~%{8Srx;(y>)%m!qZ)1w&h%rTXbqjtQxc!iPkN+EmKV$E{UDCl|p7I=J zOM2&FX->Boc;W%ynA|Ko(2Xp}&#>698;^2|r;Ld@=To*grl~VMR|@p50tdz1%pR2W z+vfSQ4T7OF3?7%s!h?PutGOaPEQ)Xbo(rBOlO513oPC{fssp}vk}jJ<+#!K?yx|(R zQ(ZQ+zLw)|9Q-`(Ck@6k+baZ)cRoXVjI$aq*SYTs#$)W8rm%MYZT4P#yN6#it!|3; z__wn^?G$j34P6?PC+6ADx?ewi-LG+1WyznXb!@>S9?Xw?enI|R;jJ~$aQ~T&6|X+C z@RxP*o?g|b-)yg1)w0XaEF7e)hqV%3T`WGr$>v6X${k+M0ZUalhN6! zO>dquhwqw$KYKM-m;NihGrnIwqWnR>4x)F~F#Cs{@U|&YZpqZB;Vo0>kAAI3rt(_i zJo6pNH(8-?RoBjGIbZXM{S`K+s~k7F#bfp6OiK!_@QoDVfsw%6|m^E|;$M zV%r^zm4jui?_1Cr?OZ$M0mCDRJyj{Yjj~~wnmv^~i`Ur9cJDXATb|9v zVzcay5S^EnUF{+7M9B>Hvxv>CET?_RN;kW&SPwE=?w8QUm-i(My8DK4NprLj-<9vG zJqo=0m{?YG$Emg-MDwrR{*DBmbL{in?fopuY5dV0IsdSeyWIJ4hhB$h*p$GHVV!Am zb4BTK=^wU2?fDpu?y1q-`=`o7f1&$pqDS*P7rQyAaaMYMvUHrWO5-3lxNje6+S_{T z)f(?8&pZC2?I%^7M|@Y=jK>Y$UgK~x_}DC!7cT!ta~sux@4^WOX7khhj3)TKs)D96 z&x14gp)J9V`1^*0f3g;AI41tKo#FdGwDWU*(mk6)Ql>@x(0DBQiOSy!^Zh=``E>Z~ z2IEocf8W8dwI*Q5mWn?08!-)?y<0f2I9m7+%irvLGWpLY%hZuSpS$Lo1w+bmV86s8AG4M%lK;Pt`qLT9iL0g0 zTXzkPbLftgEnVC_fsIGDO$Yh6v!u*lO_wB}iQMySa_`m}Nk5eP=~Vl^kMWND$)Xt< zQViu}C&!X6{~d#a-VY`JD)@edF20C|>er*roaoR=AOa|$=fgPW)If}7w8KgZ8_zc}xs2FY+32Puzv zL&*&EEqm{P#>#L!JC3ujFXHU$#I5~}$I0j1R`xlkloSK(nS*@KN4{tAKTC3**5?D? z8QA~O*{zy59@4$ zpNU>7ry=u^4ZM^?i)bV>`RCftok9`};_E zE`5Js^E5|;?cst?Kj%zPf-AiGx^Sgz-qrKvba!D`?#oV2j8EW@;rkHc``;L+i_^ht z26Gc+y9fM5(=hTS-!-#D? zi}NNY2Zq-o+QxQDjzhVUUG%Y{LvO*BApiY0tmJ&trQDNooIBqnTULWia)vr3lbo-I zx3bZu!}!kds;_$s^s8^u@vA*LbMqK&sIN4pz8`)&pE;595T8dVoBGy76(8W;;(2q&C$ZW z%AH1!<7>>XhK1|g{#DJX6@!S=v1j$=yDmbS1Sg?OJQrQS{JnV2J}z*XYcxZn<-te) zUX|twAKc;@6@4O~WBM`Z$oX$fgHuGm+=Bc!1>c$;MSbCybAE6_Zv~#!Xj9D5<^kZL z{6TTBDt!{lQ@z8znmoa~m|uxJWj1)fBayLdG8;bb{Kh1Wv%7xXGD+jitY5bb`z)bJ zZ8$mP-WQ{(YM&f~9<7TFr60s-()8?#2IKkL^7`tr@U(BA52-I@+gfd|B-$R7PbNPe zzb5Z(2RfJN4A0Wzc4w1ptfQZ00~B+(RdyYDw7HLyWt;Dj_i5#!)2+eSsJ_s>dwloy z;8~jYX#6D2`}CT79TM-cacTP$+{@T}Y`UH+J$|0*=~*M3!Aa?|ebwl)GSIcg^UKks z@x|<==mCGTx6otX&c#*^Nl%524>d1I`Um()|L`34Pe_ks2mOidi`jp^LGR>GW3g}n+$Z@0O*xmn@ zb@4gYw>5XFq-_*%Y8@@N%P1IWf1fTxOjsqrFcFoUxNn%nzk(^{NbFoi?->tzqK**)nKv}jzAOq!od+uGx-es~@IU}X)T_*|FWnW^&dLHcL? z!28#~70Z#@iN~kF->K+_=Mdj8`TU|_I=G8OGwmcY&9n5f=dMceyyWrK z>O*K${t`XYXI}nfPjc=)8s*E+)4s8@Dc6v7XVIbE@#bI)@*y8kKB37zW%Ye=^@Yj| z1D^Qyw>L32ke-w7=FfHx%6LWkdWWOp7;5)zyua0@{lDbj8-aPDgUh_T8GcsWOgExouEdDuq#lO72&j&i3UAtLyBTEOx&tFj|^1OscK5t&YOU=!EpLibL z<9SG(1HfaRpW@-k4o}a+NxYADX;pdn9BpQ$U8n)(z&B&rmR-=CFOfmBYh@Xf{p_Rc z)2{rhDR1K&amwX1P5@Ir zujJ=u^$WI1ZJ2$;2Gp~TiEek^#%IQ z{2=Mm_u+@h1O1|Y^WRQhqW)67DECQVpYikjzGT5vee9LVS7kqJ0nQx` zmV61r6}iayxCVAE@8cRKe+>)o2Pb$L$2OcDM0U&cslM|^HRfu}zKJ%P2jLvVd9oT) z%HIqN=L?lM{}XWD>0r$SXYp^E`U+aDub?;Q`wIN7@jf=?q)Y0ji1=pHvFH#S^%HOv zlaqgVh589ziY9N@WSgE?OFxn3b=7PV@2?$1bFe1RJa}FX{+sGHol!vluyyP&;oL)N zo>;-Bl+Oiz$k^Ac4~08+yv5JS*~emIAnB5YazCSIzJJ9ZaGplJD6{xy%(LdH+z#p{ z{uKGXZ{_di;d49rD%aYzdV6Z`b|dp~;@g~IU6`xk9%$DOy}YYlJ@X&w(rD+uDBb#v zIgrNJWWJAQw^sH&XQEUd|J~r5jGRH* z1p2swwqy2d>-@qihN3Irh3D@F(N)PAGQA;UkH&N2-OCmh>V|3~;GN*sBkQ6eB|DOJ z$?x-}?Sw^z>~n2bdQ;i1*P9{RP0vYftsP(fN%`!o&Bz~bPu3!NW=?aBbe;4r_^35P z^Vu0U<8Khg{*YfoSvSh9KTzJrFx_i}toVLcU7x-9Ahjvyw%%t z#z}9_bth5|S=;61N%qg#Zpm-h4zWJdDUFM}-CRsQuwb9-VEghKD-1R`uXV6r0z8cc zzw8Xg0*%?EX)IX2X7E^G{)&8DY|*am<#o(@=0Bd#2_Kx@)w-d_Lu0?$UFY+NCc|0d zEPlimY_ZxHa;_fefwu|q`dqLdpki$CWAIw}l;S@;^0FY_sV&8M6I>WK{5(D8XL_B} z{1A9B3%T*@xV-^vqYql-GkExdJt!{MthMx7V(-K3`=XlnXdN+@UDnT$pZGfp;>Rha z*(S@J!C+n6FAJKkJ7r2?w}Yc`IAk-Pbcoxd9A}k zYZ-0SKPXwlPtclF&il8Nvp!*u+|uFY^`>6xXl|kSnf%t%CBMj1H?rG9o?m0UYc(*X zTjq!R^?PWO-$VLXPZzR$y`xL(F}*Vke*BHaVC{vwsgX_UOa5u+IO<0pMwUDAD|l1x z^K)5mad+TW$S?8}`(V@`B*%e#&{v1b>xH@>{1o;SNfy9Id>3Buy$D{EeL!vVAD_)5 z-S`Myq{V)fXsi*PNepL`Vm^<3V_@?S`Dy=#=7d`B>1CZQ+#4zW)LY%!g9rTj9sHE+ zJ?d-?xl<-@!w#Lyl1=zBE z{Wmof?-bY*VG*n|J+jW`%$L81IV1DXymd0OHLfO3TJ^t7KeJzi7Nbu8d~n zO8w$}Q`sZy2hwd!pf7Cfp>Gb57T)3S;rvNzSjwwFyFuG9xo-+&d z>!#d>*@g4qNi_DR+s4!XWB8@|s4*QE4C7I4}%GTIQR1**W>0%+GiF zm?rg-Jo%pFbqC{rk|+O@yeP=a0aHFf0iWLqX$Ab7>1;2~%yPNwVZog7%ygS%q8x_9CGyUb} zfo^|WqxOFuPky$LYtVQ2bapq|6n*T>!q+LUdw_%keva8JJsVwao+sb!T$Qsi(_*%V z{n;t=A%LfT(U0>E%X%84!WSvuIzRBBFj%Vm>x%L~Kg&wr;HyBNbXwn)-vaONYL>6Ex^xpT^Ixbj{@C+Yp6Mfd zzDMckOX73z%otNcTi~!Mw{bpupW<&H$_`td`&8e29oPRnGwvmIzD*t8g|EgztMfoj zpV9UOqs{91cb#8N>dsq8d#qSF73%sy?ZL^@h)E` zRnP6W`8__xA7;dRm{fPq~2*zL~Xr=Vx@JZ!`6oUYxX`umJf= zk3GS>C+L?C5qFVZJjlQJp3B7Re)WtGBTo#zGwysS_T(-bM!Nj|%dd!TJwQFhGO_uUAbwq3Y&e}MESXV@6W@U9Q7N% zlN)R=b*;Cy!fg%>EAa&)8+h9`)UH4v-?u^oIUpH z#0T{68pHkpK4ZW3W@?-E_Frz_*W4C8HoQ;empY%s+rSKM8Nc~%=PQsE*}ZA}3!eOt zk<_W>zQSJQ;#~SmFrr#wSIF1zfUVe^=v27|d#g|`-4#B6qZ{O>m*3;C6PZwTlZy4=KAEk@etG+9fhnJGE zl&=!>?`HQM71LMxLHzUfR?l8fNgH&g?(wflZUR1rJ{-0_@l)M+(FzRB7rsY5`K5}v z%P)N=d@_H6_FSz0;8w?L>A&Q|bejAJkySs3w>_YwiRLpM&zvu4Ylz}uy3a#@uWSR= zwqhFF{%ElKHRoLC&*JF3N<;OtJH=cy4<{jgc8BuS&hE5~h~^PpIW2 z@$+YQTHKjCwcTFPc5By^2U|JM5$&8dC3ooFWtq$j>`C1e`F6X9NAnchmLJd()WgT8 zjn&UaJJ(Ff{q0YCb%xj4qE4Q*vr!(GX38~JFIRMPd*PhpjELWxqxbx%VgeJ{SZ@6z z+Bv_hGgT%#=mmDz&ig3c%5c6UWorW6Bi^cSaPED*li!hiKRm};e($KJ&U)^bl%YX9{X@>Ow{q`94f<_&HKJR(SF&xnr*+KLoX-8wU(?jc9>bm~yd(5{vnl`5gZStIIllLO_J%PCa+M&naNmE7xr$Lm{+%&bv8ZQ}599x7 z5ohM<=Zdl1r&W&SmL81d_Fk9v1AN+$b-_WjSh>@x)FrKY-Plh^>N;ORaO5l0s}J!X zHCNe6n)wR;d0vo?uVDGu5c3t9Da+WGbH0M%g}$tSrx)?-zu|nob>PpAm(Sz1k7~d# z8tmtXdZ@c!in{9#Jv-I$T;rVDAEb+%dDq$YP!}J%EV}hV)g#^OVtn5V{^oq?Ai6lm z>7aBJAD_EQ(8#%lFx7#F>8f!|U-kJr1Nx&ZWy^eg8XM=9?aIe>q*Q_3zSU^<$+jyR<<%rSwpj^3zGq`w;)2 zd>0YvinZXo+zOvh_!WIZO^mRG?@&NHx?Z+gZv0DpU{5}EN zzm~Q&2EPrycKh*^cW7YERd*RpEv*%P6Q~OhG=HnF6P((E&Hg)K$|J4l+i~xHo}su} zj(Jis8|y=n@lUqH=5z2+{&J@GhR5H6PI;etX?ZTTNAajk20JZZoVnN&E8{uGxA_;} zJWi4N0a*TBI1U4@pJS{E%1dW`*?gVDOL&9NLH@6S*3;>aYbQrZ|6U4D)SuY!PVsES z@0s)Ky+-mIpVuOW(Wy9u?v2f1GwL(Z2Hn%B*zCFEnbR_kr_#71PUke0mZO~JNO^Zx z9DQZ{Hyc81B6UwqD7V=-HtzdbeP7wnr_l%V{}*ICLf$o}nNHu)KNg1-PyRmHF2@az z2Um^DJJ88+53SZLye{r@F=x#KO&4jmp8n=6B;TcuV~X(A{QPQV+JPg_hws= z;Wf@?=aq3U4i4foBI-@ zmRzA1?asbQ=GD(d@bY=l;SW518aVh^b!FM#P&WSNWjO|_7<9rb-=}H|UDUmN;+1%m zcQG}^O%)sJXuhJP@8#T$?ept7>PzLTFBNCy9#ZXjf)C~RslNm5RDZ7wu{x>o~n*+2Z7_wOwA4DD`qo;eDDZ7;+ zk9;3b#J-nW+*(<&N_yZmd@ouTN{_kG86#`%T# zVKu~Yrj*2^SBOWfMcDcew#WMU^2LR(-z(l}?I>QK@bd59{aR1EKS%FuRV%vuCF<&a z2g$$Y{7;2#FfX+>)-5jFp*Dya8BBPXYJ+##j8MnC9aG%s-_!=S#M)?HRr*ob26xF> z8!d|qHyRy$`%iS#Rs}FoFa0sGq7B}Q7X)q8%_`{K+PDBcr*6**XIC$7=({Lv zBes)=SWPaBP#fr}+R&WzguT>5=gb~sI~&7#;|w?AM&t2eHp zZ(dk$lB@S-zFVSt*hp)Sx#*uCqaL==>M4pAl+~+m=>MJSF+N$n z=?#4o!+JAZz3!;+W9pgxp3z`+hn>NFNMYHTuI!65h41>ZGhNx)#PP$jvs~FVnL?*8 zi@g?3!{%{bUv}HcipE%E{!sL>OyMiOEaQijUC6p=Say*syFF9bLRsCRV050{(Enx8 zIp5K_sG;va!+MQYZ%>pztMDn+TkPsBY3To)>MeHl8XNi^3+tWZ+B;!b;UgZma~y6r zHFf?+SoVC^27bZ&ecAKlvbm4PWAHa!d5ul|H@uN^`Im5yW6hv%Vre|_YdTunc!%N# z$ewKXIyZhy)mXv1WU=?D%>C!ix#dXxe?4`iPHP4EwYEO#^DAQ(macV5w}xqRE&4l$ z&Cj2~`W$1Dm6;v4nWKNL%)(f2QgeZD9`5kgz7F}p9@huJ^_L~t{`k8mU(=ZB`hT5^ zaeSFF>PwAr^5v2-&c-H<8N_|boAySH>G8LXCi`b>?#cdnbVJ_+|2(##?b43J!{8YV1w!AaQuv{1m^_&}Ke@D)QN{cye}mPMo6wH?Cu@zZx*!aXV2gK%G-VAVsTa2*-!SI)tMUTn)q)>Zx8 z+!klR?#Rzv8m!4{uB3VEd%&0Z!7^f^+6zC|a94SFYxjh(wdO4k`(f)VJ zxO$y}W6%2Lh`v9<-cI5iol67Q>d!Ajm*Nk?W2=7#_80v#v~2Ou(Avtgod=Ph2#unh zZ$!GeE&VCl$@8=)edKr|epnyDo1MTi94T*o1b_AJ@Q%~QMSWkO-xQ;X&isRkJG$VYnMh- z;Mes)lgCYYv2R@VUH!hH-~W|8ufw(f(d`Qg^&LIeUb+30iS>CkdK&w8(kYYuV1IJU z@#Q$?f8+DV<6E#V8NG(?h43!MAD#>UrQj#I_xtn}Q;_cRy%l#}YNXDAcaHRXH;@bI zqhzD_rL(rR`0yo=N2h`*)JMh(5! z-tQC0d)Wlf-F{vxoWnRP?b*41(%*#cNmn#ZXwGFc#&W$d=hr59H(v=pt)#q{YuPE` zC)|^I;tg=LcU9-Uj4xdk=en|k@3NaaNOmQIjIR=>u(O zTouj};i>1h%EUG;xL1!@vHGY7zV8$L%vH4pXz&7?ajlDo36_m9#Ggc$=&Q6B^1qPd ze#gVZp)A|D$+^E`#@)N8`oL5SJfu@R6&&`k_uD>?71*rcykNVFi5Wi95%BSFh^rOU zk42^9PpEy_cIzM4mM`P!G`>N17v=b#j`Kf*^%Uea-0!*mFY?`F*U?pYXF3c2)OX^a z>^w63HG>b0>zwSbqz_g}b|g2WHGUHFA6p)qy(}qaWPT1~vizb27e}}L2W^^Pkikv@ z|4Vnf_Zr@5V~c;UrG5G?u|GWL!aWzfizjYAzAe}LiqyV^f`>EO{P%4U`wu?m)5en4 zd}ZlLkIQ)a8CAiw3ZIGX>bgfBJ&v;|+_}vqu{@K_a?<+{A@{^&%=}EqhXVRvg z7MZ-s-sLZe*G%&8PyLq%H^EG;g#>p0Z~p~+=r*~aj>VK1i=>0BbuCxjWG|f3!@Y3y zRZ(LC{h_^ZyXX(WiAFnkDu;oIC}yr(x9iy9@f<@-G3i|AWQ-N>ZL=otR#KiMJiGm%mF+~UVTWLjQM5#@At}BGUFm|# zdcapb;c2+SyLFBhVzwvAr~15$sY+(od0e2=d@j=VlJ9-4zdD{I@YK)pCEx|$L^HoX z?PrDe6=?rV{EZCp+n?u~YJA6D8sB4oSg}p5o8$uDLgP=e9s{jfbD0Z1+UutAcDJ*s zWN`En-1-PiP02ECFj>bn#^@7ar7 z8gWja_E*wciCt_CP&)qvkABc_iIz!YuCXf=@X=ho+3lHFITn zexlulE*R*8`4h~mQ!q%^TF8e(7?pGHIa-Uqywu{_Zsoa+JD=(}qy1j$C3-o(p%Y!Q zxyvggH`VG)TU59aH;lX~{6u#*KFdqB zK98L;f70DqEE&|iUj0RTCX3Q{?RAk)D_t@_h<=p*N^UG&z7hFbk+lQJjrqRFSt3vL zo9R3KtaYPx>hqKx#Ag9d_4xtscMBKU32;elKJT^`O1W}8g1tN=0y(5i=-Y1No8aEJ z{pmfi-dD3jx>H*EwO0BF9ji?*SC+2e-|Ls^#r|&j+c16(|IE|O;ZG>f;h(+kJFMAc zIET#Hu;ww3Y3|;Je|MO2$vxg?udXngBsoA94-=zXu6CBnZegqRoO5`~w$bm-MxYb$ ztUYC~rj!sl3{h(^D~x8ok5`(3{QKB~rDI=|UPro*Em->a zIDHc7YmSR!cCEl^!*=dFPQ07I{_qv0my8h4UihrlVV=>JUGZ$h_xIYG-)R4>S@H~L zY;E(UrBU$WJickqx0~2c{YLUuVuvr~JAb2Y7ryKVVQ=1wQf{7PKCYwkcK33vLa_NIl8|7S?OU1&l(G)gRygzn9V8!iU;@Tfy(!EM? zS}fOF(T93))A!^gH~Nn<#}&4kT$}`+%S-LSxcDxmL-z(JH`m$w%F^5U_wuvhILXgR z$lW-Tcj-^qPHu^KF^>EbWBi(zB{Yt7XR_OyFNVg8Q)5`2@lyS_tTYl_EgsAmxD_1l zru=&1q4{fO8qQmJ_wi!wn~D2ZvQ6G(bq09Ijr1zZ-uKJ&q~GHl>&J(54ixqU=@RhmN*2Us9TnJ?LJY>F*{@_TiT`yswJi zrzmelhQ0I`Ju6%@%x`5Me81WG6W;f&USfOM>+)~n`yy+n^sHK9<$6}c|J@g7kcA7a z?~4~?Vjl~<7$>*q{oQ64FQE+MC+VDJwKFuYckDY)gK;d>gDl=IgmDK97vYZ5~^u)erg=kqyaq5m|Z%^7aOP1Lr%rI^W5av->`t zZ@wmZ;zQB-M#cluB6!e!an!H9q1rdBXY2Ee(KTe${8!3_eH)$a;t7H&Txj3o`Fyv! zcuuA-YPfOlzg|F}HX^r+=?B3Xi(D;2CK{LYEu9^Y(*yHkynE*_={v9pSn$>E>$o%O zJmALq;$qS>nZp)GC*62(XG3Rkx{sp+TRH=bl+MhdtvSLkhzIK(xyOIAIRo)}e8O0V z?!_1Co)y>8ocM0N&*Xj968s1kKbu*W{diu34^MuYucT2>+qNFvHP0qk`7k zhoF^oqxE)Zo#f8lQ&)7#W~_Dm`3bO%zcvS`;K6k8pHBIZ2YL^9@LkCiX|f~YMTWdj z(syf|oIL3BNq?(&G8LFpm$1h;=84G`?;*YSst$bVReFFc=kiV(m_MwO(JZ!u6DXs8 zmko4rj)WHSQvErOcE?dxc>1`8-bG9Bp3p(LHPF%IV;z)Lzlv7TxRSh9*FPHji{PcZ z8;$3*ZE_8s+#NQdIR90eW6V|F`$WT3Xc*;a7zGU`qhOBWT|eA;N2MvVEhA*``gV6ivF;3(!yd&wRTWM`W8co0KYyr-8u z#g3Eby5RXV`c(PQnpgaoaiB7OTm*mPEj*Q<-QBpP@B8pWd>0?3&zhsXo3!p{!1EdC z4}9APNtbW?P5n2oEN!>_?GWF0z2v9* z?P=P6nmPAH<#@v)=^1IlajMqV{}h*zotdQfgS`KV`GFrl4mKpdllESk?Id5eGw1vh z8<$DbxU6%t*d5csX^tN@9s?&e9<$zd{BX0`W>>}=Yu+)s{5&^zxyfr7C!a@ z$NNBkaP5S7@`2P3rUUT9`U(EsM?ZyqGa4TAEx^9we)@V?IUaY3^~GhSi>)tKmM*gQ zm8A>peR=8i{C9)L`TYAnxW5S(ne~;P*W4{8QCoZ=BiDAEPJFz*ASI z`-i~zVfpN3%07}Op7xMmd}`SJ?VtI~k(U0y+Wcgg_C@jSyzcKu{XO4r=y4tn!S`rG?Dwid7-{y2D^ufO%asLxX$Ia&nA z^Q%ZbqX#;EEPB4>==oNfp40srwBV5^+|BM};`b2Oh%w4|MLB~FOvM8RzTsehBMsZf z2#xRLRq{pOH%ru^G! zq_@{r+E(&Yw)JlM<8HMX`09E`FTJm>Z||v$Z0&bh5AFU^Tt~XQQ}0`N|59LUx2SGB zPo3fAkbKXNGyJ-m`hc=N{q0V#tS$VeWZVn)Hfl^1F64U|dDP2@(j^~m&DF+5((;Uj z$#Y%IC+k;uVLXI)++RCk;7-TGJJbC-@}wX}AUPpF)pxfO@81j$ZU-LySG&@x-|HB&kB|N8fvK6!fk~N8l~?{oaINF~@pY7Ga(=qX*f$HpGOddW zZxx)-m#3W37nXbfqQd1txux>?DR;d15SF`eQDIq7Zk;RVY4iWx;O}vKZHOZ@Xm4zi z4}Px<;MrV*G8&_kGT=5ZC}X%$#_rkh?SR)ygEEE}WjsFqzc^`0C-bgO^ltq<-ab3} zHk{8`2E56@iD`B+Nq&A|MIRzxCYQ+L709L840NxyCXQ48n_%6H%zXqoyP5tC#{CuK z$tSx+|M+AV+dJdGy)Q3az<>8M4SlcIKl*n*|H=5zoSg6Z#=dxSS_ckzI|9sJ^1?ai ztKp}O(WIyBDes}4B;)l58J|DcP@ebvwLI@R>(PdgkDTzF56vhV)+OKJXE~-v>{8 zygI}|eCZ`Wj9Z&sb}$F!6U#K5D(j7o@U`(js0Ym@-)<7~9!nq4j`bJ)Az2W9_6{$< zLjNImdy6-?yc=S?WScwOzFotG^pVh_`GoeUc2Cb1hVlMF`lm=9@D3NRaxkuP>w(ur zo$G2`d_;JY?qLA`X6$o+&vgy`l6%`*v_9&*(c9Rgl+XF{S4W*~zCWs!CnnspKK6-< z+P6e`qyK8+CbVVG>v;xGd%n*00LS+jI(Ukudz@{q?$iBVU3l=*^WXurc^=pvNMEj^ zevjkZZt7T@G4BfEf17OlYRALX<$m~q^KEs0X=FR&*(&;U6?Sw~)Q9~$6n%G5;a@*b z`UvT)i_6je0boe}4$h4FOdhUD?_c}I5!U!dxbp?08u~gz*y9}RpD!w02kZkg8~A2U ztk1o`P3D#G_}$+F&KL)09Q&a{IFlS4&Np1);Y_Fk2U&QN;7o9ECb91%gfrd2X*#QL zzK1il3Y^gm{TB+(R0n5zLtk?UXNH6Gld}p79UNQdoB==8X8~=)7;D}g)}7_*7SAf2 z9@d>TxUOVdaw^+U`%`4kol{)n;I%fdD)rEZ|MJE1+G>%uO)rswCS*bDs-ut-*~?n} zW7|g9`^r*{y)Q3i`Io)S=zo7h-@v*2Lq|XV-fl|Hv{#Jv)Pn9gdiJ`9+IMDG|7@Vj z9wqHbva`Ok!37(jy(Zej#vC+RW0eoXIgn@P$gZ-FB&vs2==6S$Ph(FnJYY?BZp+@K zyS{p4>F)0x@oO#06JFg}<{;eR)UEG(^F8rg@ke{t)feM9(~F2fjUsOhdOV8!mbFvb zk9YS&k+)T}jv#+TV5^@~jLNl5J<$>KXZ5rFH-!6@(%+FNq9@TA4gL1bbLbcTI?M7X zIAj{u@J=4@`bK%`E-CiCK?C2shL7l&_KO)`v%F`K8|R1i@{GLf^?7C+NULqSdGEt_ zwH{IWNN?+W%&jk8w-Wzseq!Y}?Y(K^UDeAoj#-S3@<~jeXX^)e%-HI_!x_*%D&C_# z^-tIrhjY7&k;`uTz0RV=pUz02OlfenObY_(6quYB~#8{EIt+8zphknYo^-r zWnWZR_%UU@Zy7Pa6+ZGo4L&&KE9seAS9qur{(qO@YyOqca$#NJ8%HTCy|Q*ZPo62m zQ9AoAkmvH5iJ^3Tx8E0m-V;AsNZ%T-y^b%&=d!r%IQbdqdW~WqSC&p=uf$H*FP1(! zPM=D;?=MRqAE!?w9lp*LZqoIP_MW0I-N23Uy|T1Xe*N3(3NN6nWUDwP>TKXHyi|Tu zUE#PepZo7p`FGV7{>EOBq`tfVPWI2>KT=otOPF6%QUA%h!XLwY?!imJ|8rg8cVWK! zUX+8sr><#g;s1pBE}oyXfBdP1pN093he>|^)WVO*r++t8epd+_d?I_2I+L=OpIYeh zu+YDBY zPg2J4&{?ox|GMG9zP147p1S1=3qSWTvz$4r1{3}KaR`%gMqAd=#y&Up#CNv1y6p4n z%)4jFzu6Xa-_}zCJxJBzeAPZhl8-TDk0dJ}wUaIz57>fkY6RnlvG_jR$U|2$M(k4r#Da%Am497sXIHYptB$Me;FHz-wGf1)E$3z;YYj=EOzsO z#mslv*Oct{^KnAbbJ}<0WAwnoS*6KRWlyT1pYy@-FV_~AO|x4ca>GuNYk z%(ff7@a!aqZ%$(mc6NaFtZ$gZtR2&|?0E9`q0~BW^d!DT2CVLx z^!ZGS7eAo--Hps`N#B9q>3pu<_npI9GG*%Uxxb}4Ggy1&eKXI+8ORU$=8J2*I0reP zPCI#|cS=6w(P(^$p6&_)|Jrj<4%kL!%~m9by*;^~ElI2)w)&HiAAr1!}9*9hX6L~V2V=G|tUYrrYRJ@yagNZElAV(hzVIG=ZSm|L-cQEmf zQ+V>Oedj4Wc{iBTfjPCI@7fUNNiQzE+QY<78J^g`6inXzy`(Mbcil5G!NEk=WS=e( z4Ek4R{fgKNeE5E|YvY_<8xxsb(;b_fPwd3L9lCMpiwp044fYLtC7C%D7<})jbN?9Z z7ik8st_;uFuR}N9_~ODfLA{fz)jO?9z1=S^tP1KKSFIkg2#muHsC-vTaT48Ze??APBv#Zqm`DumOLA_sBt2eJoy}2(bOb_b)s9L>+Rq8dpq%c0H zx3fyU=S^tG5I@hFQ8*aX`*gK>AFopH zvoi|63F@^_Px7WQVg@pBG+X;q>h<4n5q9=MzI)~EY;d=5%7^1yR-9F8n$P7T)wc_!!+9kjFgRy-&3|rcVAc7t_wq$FUuEB_b|g6U6($PIPqT4SG>OE1dUR{-X==PML-ow@E zomQpZzvl}l1oggEtseW8j_Th%nJ@62P-ov)l{%`yxMYyN;`h!d91iMzmU_}5>8tu* z_k*_UJ3HT&j}`k_-Z$!{Uh3}OM&08!4xi{GzRkovZkz|w+3xr*kBW1M;T((k*e&cA z(w*$+?zXz6GYj?bBR)f5csPkO7QtPS@U*@r=HCH-4wf|WRAIIX{w3m>Z_&-orO=^^JG(lwRZTN7EPZ>0-9#-VaLyQVW{wx1SVco;ZtuDGp^J0G{h13Pz? z5r1tCxvh?S8t?b%$W)Tf-HKoK>9s*R_aT1Kr;iTOxx=v4r(6Fy_}n{qn@=Afl;^I% zTYdUO`2lepqnOO!;&=3R;W{68rwn6M-!ORBgpJev5W{cRSg7>azjydE)<$J|b-#T~ z@18pD0Zizv3(~pgFQNCeAf0>s5_;={bnfZlp=@`rr-k|cdk9rxmGAzd-EBKLCU*`Juy;U2v8e8Z6# zrOl1Q^iPL%xTjw8Fu_p#N#C%L{L8lFWp|O=ypxf(S4l>hdprGN!j{&>`=SkXZv8IQ z59!7qU0K1NjK1h!Kd1A1>i#c#?;mG%Rqg+unZv-DK?EdJKxD>AIm4)^q(W*8PUbLV z6sS}PNJN473$@J58ia1B{or`*mzAMr4r7+Lw3u67ps_+T1FUYAd1nCa%uJf-&1Sln z_&i_hz4!a<_n9AH_kKR#f4+a5$Jy`wW9_xqUTf{O*Z%eH(%ek&<;*1Vv!JJWoX2}X z&wr?_i}y0W=4ihk44yGvf*erpS1zw$3r;NalLMJ|@v+R4ru_IsTARoEb2U-fOepgz z=jt+<=ldzW!_2*$&1)}4AFB-O$@5da`T4A+l6M_6YoCkXWiI-Y^xedp%#Td<{&p6A z7~(sL^D_VEmyr39p^ir8(VgC>sC@Z3z?)MC`J+AbPXb5q*T@Dy=6?tc@}x7rwR(x) zQFV1`jzKz#r$1f(9aS4jbKmjt$b1~%T$7VoArOHT_ z+6f;aT&KS#UT&jdH-x$-COCSEeVw+{ItPHO;O zo~`ZB(db!a@$q{VmPTAW{>gSz9^C~rET#9UqxUKZTU&;acN*g+`7|bSpM$+u(JFkV zg5T7_y+F;Hdu$`Uq)z*%>|UT+^66fn(fli~$I0HS0G9t&pNH>NJgj!P{Mm8T9^r@wO>SOOiYw3{aZmGM0CAzJVu0pO}rEfdQXELYvEPNVwssAZV z8y%!^Z~F73<%ik4tVMq6jNYsOPIhdNkGs)-KTtm2yNK)J4)mP^<(nAf;|}@vlTUth z`OlmET_S|VlUcvM$oFwiqu;=XF(7)=r!I9zIkajg?U#h<6MO&r=pdhHDH>)acgRIn z`V4xskzc$hKeqUDf^Z-l^L(HyLVi}>Q7*4wpIW3V9t19WiaT()KY?_ux$nR6Li|Yc zHyx-ii~RNV?ClBouUze9SU zlE&>x*gWE2?F&J7+>%<|?(BP?Z>7tp`%#pya~bcmg9rOIkVEdaO3Qv*+;g~ODvj*x z4t{GBGBAey!L_N@QPRc3DN`G)bq{C%FZB2I%ysoh=Gi-dZ5-Y$8QytRc$>uUxzaj| z_Bj4|GxdOv@ZcQM!$wEKzHLpO<~|PF#*vZIin7s(|4(^lP`W4@#E zsQ*20XUp9=1Nc_>b&eE|72V;A?8W=ghuC+6-CxT2LF22-)UWWB`n3i;tpA`_)}8rOKH+$raGX@vd$!{_ zy`Sat>VDG?%w&w1KtIrq;<960*^^w^g_QlD-gjCb^m9h^g@69h`%Yg2miBl%{^Knz z+ChKW{ZrT`eoqVgjZ6=#KaKyf`BYBhg!M5pDc+PlGsW+@#~x%{IbG?ZrTFHT&|Ke< ztT?N#*;`y$?OoB`Z3p0A>qmIl&ME`T`ZD%)x^n=ehx$i)=j)vXy)(|^ zB3)IKS8GOEdof;ebk@EU-5EU!8M@T%=QN(CY$y23hdKv+(hZLBJ!YgCFM#u}WTTKz z>)4`~?(m&uJS~5>-I-&yfyoGY)TWrH`SyCsc$z&r>L2M;qXoLH6WL1eH2Y~{o`$C4 z>G9As;%P@C;Z4~-JWc+u{1u+29uMEzL4C&4l&iMBi~4zp>#Jk?@N^@4JRDEg4`n^$ zDAqHMMpq8at!G`)uY*KmE_*!iliq`G^x<~#D|^ZC5!pRl@-M}{p28RiEx2FmsL1z? zk5q4=X#PyQZM>$Ahcb)Oi8oT#Xn!?#?A-+Z_?=}zXto~uG4PlYTV4dY)rGd4lbf@(4%bcyy7$dp*4l*Wx zCT($YET~@ZP_pAJS z{BCmiebM1J3H;=no&`+=zjGnF;D(wWoj>U!oS{!8KRV}QJO@t7f4cHhul!4eMfoi z!*5O(!9W)#VDK#%PZh%ew$}ee1Ea&4b@9H#E2X}4&+)yZ73ya~S5G1Jj`W~z6g#xMH)K5#!lygJ$SCF8r` zsxL3|b4k@jy!!IX$30~!tkkue@%Fr3wIJm zUMzmmOxypvKETPwM8-qE@9}QNx4&X9*tm#ph}QTRH|U%9-3@QjR~k2}Xt&0V6-P#T zTX!^C+qrKp+(+FwHfpb}w~f%ej=L41H~84tDZ8-)nuqD?j}!3GchHv`x!18cZIaTE zm3GFG;1Zdhvb1TLx(xYJbWrW zFFxPy?Ki?Ddr6<}WVcpB=+@iD46-Py| zWg`qzUn6hN04K3$+QD@DbvCSHui86YS=ki58!3m3{QO|$whzhG1?9Ram#7C>^*S!D z>rG|3X+fFiip$LHr_747+zCOMos{u>DmmAkpYxG(Pj}fmm(h0aF6~+O&$I>Is{L=F z%t!}$IUWn_Xs_`C8-z|LUXYKz9>kRSjRDSw- z|BY`)=cFC3$vr?}S?PzaKS;;1cc30UfR4FTeT))ZT}W-{!@&llK+_z9N9|P zl-jqRd#>m-6D%E_=iC3|QC?REIFs)2)tHjMKCo-(Q`t3lIhj)X!KL*8 z@Qqg(r?kiO3zAdjG+Mi19E|s^+Wu$8HR&bsHR~HM>rBU+;ZcnVn$ti3b$E^crP6Vn z>FD6w;2<~K{X9W%O^%{{-77R^YHuVmrZ#pUSB4L=@v1K$ockDTAGYDdd62Qf2|1JO zYz0QR58Ln=YxAsr&vFj zP@A-O@I=>-T1PaVfk!?9&u9)h3*1FR$?$RH(^~M^w?{H88r!*yVbVPV>&hpnH?nz( z)_Ky>mBX=j3Vx_xJS*6R^^{$CzW1bd&H~>p^~Y?+SG3vt9EGd&PJ?h{kDt!vGrpRA zfu80q&WX7r!F81IMgKkxzFXMq7tRy(?qCf(B%Mp3goGi$rX zWcpOPME8*<-Uo2~_C)Lx4Ddx)^uc!|`g0`n4e&j{d4G7*@EdRPbBX5E_T3Eb=(abh ze&c!YInUvvZ)?lTLvwfewjc}E7GNt~YeYX$ThdWm&hTvkCu<9~#14~d_!XIypRmD? z)A$JAMx2+MhTMTKp36^iqx~g%kNGX`+wjBI*ni_3>>g)J^drl^`+X$K(YlxO56e$X zy~t^N??t!CynAm={E9qEuIGV|_6fWF!H=k3!>{?2TqU@Scli8(c=?dGpaYagdLWeR zE-zn!EQI{8_n@_(p!{^^+W252IjZQE9AJ;xyv_C-kS?FFWH$e^q@2*6BbwXBqnm(p zY7rdHEGOWQo`6HT@WwZbO~Co>h4^3F-n+Tk`%o4Z)^M$D^hFE)*+}Oclh8S)1I@mj zV0(D`>YV8Q^n%X0g3pD-Ec?RBNWx-A^3T`4ZDP#quCnos9EF z$A5>8lr>l*Lwy5Y(ko{BL~zhu(Yt4XzA^j9^i494!7I`^?Q5g;u5W)Wk`dXlS0GFN zjY-L<7@V0vWJzkfp{cY+_S-2`&hmVAG`d2Q)V>8SPcML#NB>#$po?fI|YVK1S>Qp_jD zNWPfw$p3m^LF9WCY^_VkFH}PrKL$1)7V*2yNr<=chdiOY zANQs`M_as}n=Lvq_mvG_1MgeEW0%T~7HwI3F&UdlKEX{D-FxHXnB(lvkf+~!Z%9*{ zuxa+QPh~&8r2g-{sVD9Hy8XcUeIb7}D z_VA8R%9Kz>V`!82vFYwlgO%bg)2RK~8MJ#L?G_)X-Q8dGpZdr8i1}gFJm|ya=tv(P!QECqOugUm;j!4KK0FTp zzrx-|of^@@cz9}(hhv{5^eGo_^XnoH`qJKU*82dJyaf=`PUZc*W9(=D0rgm)5O4iN zT{=6jx~2fDfp%!`o%*Grj5#N9g5~AN_k->=^Lyk|!hL~*hYTA0s4dpFx2n&Tjyzo8 zWRQJ4z1|OM?L^O69neTJbeX&VI_4GV*h!wmzTIkIFgMd0sK(U>-^YB*U;k7Gy1}i( zGFI3;&TvzH`Y4f~?;j78pLt$`UmFl?=y96b?CP27!@xMvhrzeThiTIZgtcy}F-GlB z8`*ndx1t!5A6?tyekA=o;8|OOaX{Y|C-{=L$*JB=u5VSZ=$joK^-tHr zx?b~-r&GBpCG>Mys(0fud?6!~J{g;%O&^xK^$hgKNPJ`L&M$_F9`4#kyH- zLN8jI)+zrO`DBXTggBY9rBGnNS+Yhn~L)K z_oF_?oN2#*|G5Ys;j*Ks9CutXmy6z)@_4Lb92*SR5RW4c0p9Nor{FEfqB=u(?XM&6 zE%@*gc(xVxl*#_>7@q3gO+RQX*1F1W+W(l93HExm_4Af$+p(Kl+7@qK$y~JNlT#y~ z{wGh)HEQqZ=v(ge`>!(PsoYBDhMDrm@67z=<4=n(wI@qw>@NQte8l*k*ei1sygNc` zG2HP3KT+QJNqf4qU!{EI%IOMs5thxAS$Wy?%%Y>`XJ(a8k&n>gsk27$CFN6?x3S*O zKEp|Mtu;3tF~4SP>WI#C`4rNmBWFnWbit?QV}qyd&8L#oj6#{i{(&+>Uw@fH#7p}s zL?6u)A3?S?{}5e7EA45>(}&P_-_oH;x@DkS=A2RUWsh3hut#4vJ*>>@pa=Zn=YP_1 zozQs-ebxn?%b|bsFwVz7>w)?}x~yJ0Q*A0w?PK5bA@c&Xh}y}$QLnq5R_1lIQ~YIR zUPr$}8^#}-8xKYsuhUJhI~t02*$Xg3YZDtjBl!;AO7BO$Q|uMzy+Yl4n2>YsrgOfl zP}Ynytw#C!hAk&*2}s^yYv4xp~<$ zesm3e-D+}79OF%^^((OR;FU-3NPgqC@txRj+E&=tWp&2$fk{0Yi#iy$c7q%DcH2Iy zokvG)lYetJ(_76dE#LIO`!k!)Q$d9|4AO@bF30g!AEl1 z>}^QGnit8}utMu5^5gQYKD9M+aJ=W<)K2N)pB_Lb()JSi%=(8m4TftAxJGL={qunO zOX(fesobx|<8yF67|~nnHg+#J>D&EUAK~w)pBa~jfFJ&XU3tRBe~i(|ebfW8YEBj42KnR=JZ_owwl|pU1oHFb2oV! z%ZA@J8~!(a&$o1a4f!+VGo4PpF6E^SJc+Hzlj%o*Wb~d2CnV$Uv*H&k(5i+u~z(D&K*9bT3g#tnD6R-iHWRb zYhU3aev!{eww=!UeNy#TgUe*r^cBWWsr_vp7u)Lwk0x;8+h`%a6|YwF5C7U349~N` z3wSnagX3BG7iHT?uJ+)s_y)YYm;N2?^oe-)W5T7tyK#He=iWyAuW6olXLHX+wAU^1 zmSu<$$u&MYu=+d(vQg_ zI%*DL^kkku{j2>Pt#f8mtMHf2^2AWBqg6C^6W6k=bm#1qwbPX@e!m4j33O@f;(ODw z^iJNXoF2pDoeZr%*;%S(Y1`9$=TVYR=8wQB<*o|pjYnNvM=kB4@3^o&%CjfEQ?mHH zzVBrZDt&k>VcB`oJ$F;zxB2#aYILrsMR`fOLJmdVM+jPD3; z>uWDcX4G%yFHP&-yTv`fp-(o-7G(TVSgN>;P`4_Ihldki1Dts{c*ItDw z`-^lyTi>zQ>zUjB!FrCPo>9g1tnE9WLDw_p1@n)-drss2H~B;MB6*AK0Npdbk9yjX z$2>f!b=6NpW38!A&=@tDc=*!h0@$y&+MM8tJMFDe^zm+$MTV2IG-UOyPL?GrdH7d$ zV}4R(Hx|o??TtFi$)fEMEm_^=%IEp+I;*Lb`N|H>S2SjHaSvN3Fk^XB8Oa+kUdFcG zj(wV6P&y;8|4Hc?tS{>>LEf7K&w%$jX_sW;eVNEU6-?Nw-<;9f>haSzXSwRaZ_b2mb@OQ}&pEKA?)#0mOq?(LvQ3Ro3__dOXJg4;N{cM6P=pj zy*#xm=2`VY#Gl=ns?vRJZf!VRD^UEoo>$>-pI77~mm3IY8fAdHUCEGGwX`4x}Y;;JaTOh&LR7 zsrQWxU+5d>Et?e2J+l+f1^;pLvJ*>Z2-j0ISHP~(9t(fYcH60gkAvGu-+$wL#=v>3 z8%Ae6y}t>2XGiMUVY#&F*lp20`u*_hJBoF1Q6Bk#JS_B6lDEmLvEBP~q>ugG?8SKS zwkl=&$HT+_Z9nisA6SUXFN*RO*Y}e{sP9KbFp7O{aebRJxjti6TJ}a78A;{j(=uBl zwi_Z_-Hrb{G7r||#B1U+$=lcIYn?sQ8Q0awOsX!G`{}L7E3}kMn|~$E+FXgsa6bfg z-!l4h+1{F51AVS?-{D_*AG}3$t*8G>qCIGB$?@Fx4K#j8`!~Kx{*6{9oNE+prxlu8 zYp#eM^5^5c{UjS_yKf`m3^IuQVg3Z>Y`1Hkb`5RRc_!xKw_hS$v9)9)Z7jZst0x=d@-@hO8d$^o>vdxz$zIyMX`o(%0J}-@D;=Nrlnh z*L@~+&jHp-U}a@nF^*rhdU$K+RNlh{m(Hn8t$HWDo4!AZx7}xxx2EHyTqphfC+vVv zmyEdWR&eYhU+@l2tH-4o9J=5at)mK-eDPWXk=+Pi>?#Gnb9bDb`yBcFnMz+)_TlH% z7Ic#8&8od+snuEVyIt|XIF^6k7R|f!)RCNP2lM+Py^sfP9^D{)4#6Q3E6;ooh>N3}iGbEafPVfB$edm%aqzvC?JuFd7p zwv%?2^LD)TDYTWG=?*c`XD;p2{9U&3rnb8_*FrzpE_%1R23%{(!5cnTnm?P7BL26h zB+L1DK1@Nt>JNzrMM=99qyHjusDPN)9(4ms>O2 zHIjPd$LJJ2*cZ`4nV9Ehac8~5cR2Y3FWJUGSMV zFT~Ak$61AUNXx~4{p2%#hZf?81TX!6fX8F~;Sr7lMSQ&8*~44lYvCW!cv$W`z=0=r zWY(P54^IrswUFM2C$1izt0CU=#Jq?nb~FIPcwtRhD|VOb$6>iIlQ%yBx*r|Wo%?i; zG@D&AqH)N^7SiQc_|{bV9eFdGxt#taebPktKf?Ws{}n$LpQ_1bYV{SakDHMp?J=66 zddZV2N%h9?#=)=Pn@X+Qb!w1q_Er<~D$+$Cm6I-6;l_$SG)Nt$vl%fBuy>=g=xb6r z$veLK-iv@QUzg#qrX)8r{GL^sJ4^LLmn44&jK7WT{pzNY)qdT7 z3%G~|{n6X6OPRlmdC7J+hS9zaHr8S%tNro;d=UIJ2hiAhCHuNv;=#FnPG8z#-qPl9KG zBfEpSd}|53`mFFn7Q`#t9nV=F+V;4^{UZMR@ZrY}%e{^GbAA2bZuO6d>i2t{eEne> zz9N&M2;ZjG)ChbF)F0&mFMW?}YAppn$z|KguK#I=@??=kFDv$qoaI?(w6K>W*+b+# zD4l_MD0KEbltP}aq}`rIO@x)U|Hj~MljIm5@i%fg_!wVO7~_#$$>zI7+X3Xs{KvFW z@^Oj7LA;fL4{Yxo;}JX?t#c_p>GOP+v?~274f9S!;_lp){JO^S2-&oo>bl9CwQHG z#)*DXUh#qbNAj>*@}M%3&obesFt{E?Sh_y*DSXlVC-q)nyZkTpo5{o>^xI4DwY5?B z2Yn@+yOegQ^rb%&^bPv={}z28bhr!GErdnihyLjx^bKsoc)n=1+meF4TfYQ*w=kyY z4G6RC`WwTMDbYwcd<7h2*JaV`S&i8iUy{4Y(NR3{Q6DD%?Svz_9oVkZykS0b(!p#l z&5xB&`)Jv(wfM?4FGNQ%7CtOoOjk0WP#jFxEu(TG<@Z=Vh*k zosi7?$y05Qhlkai_WPd~^_2TNt?+H;Cl~I!(%M#>ru(i)YmdjnAWipOk%mokVFX|G zQuH8hNnm%@-j5y~c$1Gk02d_K4iMC&?FZNzb=FsWpvBJx_vz)-^5xrrps@ z+4m@H`>}a1k$v1|ce7VRcM=O{-M`!d9h^+WZNE6LS^E6Xhf@Y){osNQL)NSDj1)5oYtv7?OXr}MZse-|N z9ocqs(Gh&hMzFYQ+BY{EzdDuf-io2$dd+vP{o(i>-1(Z@GVFDdk5@i~qeW}%SJ|%e z`|Pfc_Hy11T^-D5945V!H2Ff;v)?=G{kAS%@U=f(^F7_$au0nfUT+c{mq+JqmpeZ&7!^F7qAW-ow@Kn#F5`F_$qx|ML%(6K^*?K zzWzYw4(PERn%iq$gYAc1&zhvyV}v!@Q`mQ1#9cxn}5* z@b~?{sP|(da<>jrZ;X3j=c%po*KBufRaiQkwnzNHxFK6)q-*P^fbVT_@;qWZ?ef^& zb6~mQ3POvlRt)Kh#o1m`$DSYz{3H|`>60i85(NVe& zUer6){n0Vj8Lt0g{lND#Ym4e8xrL-Hjw7W2Bjs&2GUG+Mi#~c%L7fchHkcqFM}=O;ds370_QI| zyzc?;u&mY{kTyF{8rf^9ecVNC{OZepma^+z`P)@K&77OMKZ|T>obc;`5#HR7G%E6G z2xrA5=z|YpK>Cn2Abm*l?TYHzTCmkfGy1T165SNpYNAJf^pTC>*1kQQ6;6NY+=lj( zbSz^&h)f;s^%p#(^#{rOQdhQtyy8RA-{xQ??z{=|mf4@)ccPbhzO_yr?hOvtyZg{* z*dX*FZ9w{vHXwaSGx{(;J}7;-vvylZA4kJm%uaKBIS36)q07f5qZ;2M9T|_)f4#X_ z4g()UAcyqFj&R?a*Vo91AAd|P>4Ru|CEnUcf8;fP;eSIdK11EL#XM&Bz3dA?cdX%F z5Y25$*czYHz^G(jU3$eu0O4`AksW$6fb?>gpe6@jfD7#_Z?HDY<`ZI`GBK{01s7oY-?ZC{2{7 z>k#rpd$I~`Ixs-rb6*Gb1^qohzGzR@!Sl5h!z+>__HrJyecvt4GXPG_#dQskZx1y4 z+9Brs=i+=t_>2CWKR#$1))eO%0JrOl>l+}S+Hm>n$h)*Cujj?RW?wk|tuN@q_`QY! z?4v!{K01>z$KVFG%t7+ce4Y8H73Z%n+!2Q#e6#Gedzfq8n?APZ8@A4{qzC^W_S@p< zKkfeUGd6o)9AV{=?UqU1%ss^ux&I`3Q*lUEK6K8&;A*XorYpA~RbR{f`XRs{7tMLjBG^#JPwss}z$ zJ&I4!XTD8bC}nT(XuV*?P;oubfq1?lqw=g_LV#T zj?MePOY=VU?QG!1?IO^<3`yME-L5 zmxZ5bVKfk}94y629`x<^kf?okg3lIonsD0Kwq$eSZp&bd)7-F0`-$sHa{o=4toAp4 z@~ypDClBG7%No*Z*3=t~O1*5P>E{;NAM&m0l zqfOKu-z!bJ@igO}?*9?q9OEzZGcGR7~+-HB|IQDl9jrO2EDjL<9Z?63Zsn(7K z&~rgzUcbZEWr!F2%s9^F+@I1Os$F((&m8if&%Bs29sgXK+r6wbx2L`|x3{`9_j8?1 z{1j_#pJHvz$%B=f8RXwnl6z|j`TQB#nY0m@S^8IZW5)ae491=}={rA%Jn+Bm*`?l4 zrggV?M1v!8nG}4_eq8;dD;_S;r;wH+O>(X^DDLFx{e|epx7CBJ>7Jav@VHwc<>EIM zU{OzN{)6}(t4nedm-;a_>>17D#%-cG)xBWIzXWGjp(&;A?V!?~4rv-axl z`E4tlwQrEG_AeuC4adTxN7=flg-<0+ANxHedS~U{wG(^3f$b!nz24ay(qnp0M{5?; zyQAvFskwVfn|4irzu{-&MdexQ^6dSp{LR=rYcEN_cn|6F)k*Gk*P-sj(>+4kt1(wT zvB@bL&oln5F8Y9d3^t~-KDV#d`}V7Wv8X)0qfcYFzdKR%(pgx+e6wIO-bvRtc$gYf zkavyQ>-~L0gwb2AE6YDSZ}zkwT?2hr-R<bB0yEV@3 zp4XABV{RfZ?es8Y@8lhA>zu7ICdi|^7S-qQf2+SQkg(ZF-c}K9WgjbEJSY2E@T0LN zxceY2UKqyN8us8~%j=CM|DI0RzbnmlW<7na+v|f(lvqES9IlT9X~lbe!g5+)qa60r zg~Cm?U}AlZG{s}X1?wZ^!M+e4(fXPz!`p1|n!ypTog;h&&*k;-6F9R6rm*H!gfnT# zZ;V%5PB@b`AkL&2&WU|B1w4fFdh9lve}LaT=|z#utUDz)7g-JEh4nt(H)P(Vc3?*g zNPG764A>6R25bju1Ga-SYX^IU;8nMFmuyGeU!AE0?Vs~~IT-EPOEVzOqz#BOX#?U+ zn&BMm&k1=T!7tl56YKaTZpZVf)t?D?=v%a7OIvhLsq%;iG|tvgR(pox{oK|+q*1>3 zZq4m}KX=JCm$J=`UaBhK>6nzOC(=)6oaAfwm))`)>^P zr?eG(_kNy>To^4Iz$^3OD8{HsJr97Z+NQZwUbc<)Lmgkb`fk5>M{#BF+^mw-C1G4S zaWhI*FZDK!@{J&FV#(@lj{a89MWd~roa*uL_otKP%BXW`I$7?fsN7o7l5$4N=Fz#E znQJ{5(9`O8G|KytFz?)tNd!xKpgn8e1gu(q{ao!n681C;2^IjU|{d95O4@G$w zkk|SKoZ~TBdq0O2be`xCr^)^x&Fj2Gn&h7}uk#}ML1Vwym^4>ctn)07c!K`acoNf& zx!G}y>kEtVemugvCT!!0V{-RE=XVZ}S9dzZaNjz{Xm?(5-uH|#+Uef#7;kK#cE=lW z9Q%$E{E~~xxqXl^pQ@dbkHp$Tz4@bCfz_H%GY_vyuionR)#cO7vyU(Ot||IHwdnhi zA<6QsMc|4{VZ5q&SEZ%htHzrpw}{+<|pFY@W(;S=v%U4^>sww$NTTHqYPe^{~i~8PxRkcMBn55_ao8wSpPlZXe&S3e_s%NkMQ3& zMc?KA`xnu7EGr|%S@{yuC7<4Eg3XhR7Dso@M){evPxzLw`gMC4Gs(y5L%kQ^~tc>AxLS>qg~{ zrEb{|(t&mUEq35q`qojoBT3KrcOC;>h`u}`>FX=h;rx{w3%&N`TP=yvn4%((=3xf2HB|AaK-wi8j;+>sri%wfErRkIQCY zPcwGWc4wO?tTDXV*&DK@Y;U!nAE(_J8thx;+g=&jEIKP8?Sxu^!tFLbt$E#R@O|nwke-*1fK4kiPEDp zHfA3SXN}8KBR@*#RGoW;7Mib>RM>k6@m({d#rA=0hhW}gYaF7N>;|3B25|17;lsXJ znvv+s6xgZi1)1A8p03sE_1ZeR>V?LnB_z z^DV!c*2msRUw9u`hPjQkh5k~1f4P!0H}7e7>k(R?>YyC&qS<^#>*QUNQg`f;4kOK< zJDJ5?u-?l(%Liyt(P>VQ7-vHp&9U$%z$F43%{0-lNHM!s#DLw&}{tmv^3 z9;yS!34}!t;e5Gpc5^fQ;14UG&eQn1Gza@P)kmCfYqXxJx1$&K@P<Jr38;?A zz!aSDCwX4s<^vi(Y<>^!I%C)Scga|!H&iCFyMnjPlH-Zp;jI0TnKE!1ZoI|%4C~*- z)s#oPMqS1)8apj7@#ytl!SAnZC;Cr3`gmwBvM#PN|7kiPTJP2T=nB!yjZqD5J-9)B z<8AoAOX_;-I4_g>@7mmdK(|VG_$Q(t{Vn{mB`KTtShzgGCt+iT`xVf;{+?Iq*=9c< zq|NJtd32UGY5sQ;c$scYWxAVOJ2Vbfk{<4<)H$T>A z=i|l_hrQdv7{S_8W>pAV`dKv1i++-OH~va?lRnm9PX=SK=7_p8LFIf~^oF!-{csJ> z_sczAM+h%yX|j*M$orfOUmqX%tpqd1QR%{wzSG?S!cpe~g+KRSwZ{8C>!W=qal7bK z;o;}<_xQC$wV5>OYCA_W2|bCv+Uj+czR{bp?$S4NtgW8M^v&F=!+ooctl)v8_(ba! z`e|Mj+g3&V;l^M5AMk#eaN(>^Jl5IROdRzv7xMKeZ&;7osqy_sAEWPWePc<_7WTRH z2fHwi2C(sORk=EgVT&JS4=kY$)zcr$d)(UxS})<;3}-x+^gR6~aDhh@{^|g@^xICx z^?}-{y$XgO{hsAc2g%HTADg;E_PFZNeX{z$pSQ`iwynLu-Zni;>kW=JyrNBm|@jhOw*5s^z`S<<%KCe$*n&J3JDn66t%UJw_1s;~Y zQrz!`FY)E7lm80oA=+YgCi?YUPY+jD3Vk^PxyxuSH4K~5>0rvX=A9i`T#w%T^f)Qa zx8pdsf5@j9zOCFF>dq#bj)w-t@CL=1b(8*ZJ}ksJSX&tkXJ1b-{dfDjQyonV@6Q*` z#3kA@7Qd&vQ)i&|`1~Pl`t4)-GF8+czVAEyI0R0wtB;un^hd9)6N5eu<_d>QFYH&1 zXWj?T{S^D|^<(ES_P{Vd6n%AXNytvjtpQ@3=7+lv+LIhu<7;^+13@zA|Z zXVa&j(O6T>d&$5O48?1|*jc10-ukc_-cF~?hHD69PWWBwa7t5WJGy57jfMNY4+MJ) z!Zn9@f9P8DKenmnueKKx+41)=bbm|x1xY8ayza}pW?y^%E1a>tb0ao@=B6F@GACe+ zyoP#h4WF^36uNwrZ<{lh`8liRz^Q#Jy{s^gX!f=w;XE{RKArf^9BVW56OG%U?F(uf z}K7!=4G@w zJ!$in`bnENuKmN=l_gE*Y+U=?*_CBY=h!?2_}>Elt-#kg>Dz!`GnPBo=}&wJg1;fv zGbvXIe2p=k@UHd&>kM_v+G93vYSa6v6M;Vw_`g3pW$>RpI|Y2>nSt=9j~Zxi^V)Gy z9j8?0hN%wvAgW`0&#k4ZV?1??r;Z8KF@ZXsadrIe;B~+=E2u+nA6h%EtIWN`83wI= zsSfR>eIng7uWJ>0`6}+0zLx*%vB%c_#Oi2Y`_qHhF(JjgfjZ7|b=*;z`!#h4zUIb~ z+X~LCcg+FzTezqdJ2o9?ODE3XNN-G<#ki^e<5On)=b!xQyzHz8 zugSg(IK(&a`24*5-mY`%C0orJuaPf5IId^fAEwL4wWvWh#NZlkU8A28>bVZ1|{?gCol#tGRUck|1q zqIX2H#|vI2mxVUUFIsuI&8sdq$9k4AJ2AHn>`v)I=`LTNd^SFu=}4{i{!hiBH(GUf zd86*5BA@O$(Ec{zYkG=wmGNUxe`VBu`7QnePFh3lFHgU4p~IoV^>n%mIw{OJGEe$g zI`G;Fkx#_8v5P%`<^vEdM?iZ=%S_d*85Y0a^@Y;+H$0u0Sa-X{_lEKBEX4mMjDL3_ zeqR`WX(9f_Fn&cLzB7!!x)A?-82^z%dTkJ0k&%D)lI zG4l3=vvn!mG|ZV*aYx zZ=cdwGQ2U2ZyYhFF@_6$3-#E&lYQv14I1RDk#%Im-YifU8Q>nMR*Q$v@#RNhcsXC| z!tiT;vJ1nL_^G2jD+x2Nva(kYojC3Kz%p8PUprSmi8v!`?sR$R@gBj(qf-bg?4W{i)!=*ams?}_4+r)yeN zztX#>s{Y|tzv9Dh)vYxQc-v?KeIh#12BTdqbshx{$|t%4%V-T8qd)MiPsqdEBFcAW zK-WMv4OjXxOP`7tWIG6-PQ}r;7IzVG)M0V-vBhCS;M0%d=xd9^j<7ybS^C`KmJ$d4 zR+hfExaGvrHj9G?EN&%n^s&Xk3l?`ps#SY~LLEVyyV2nnibr|l3S;l@_bkla{ zgU(|QF}C)~*Q&ip<`XHa+jp~rrFabbD87m{7-Tu0hQCX|iMP^fcq@&%W52TUXOv%g z%E$x!Ebz^)CQbMy{I%)Ap53rL@*Bw?qIXP;@9DeD($wn9#z$@BJ=Q3mzR!$BMnfGq zxykOsG+jq}<5=|CGnZ-J^F$n1(@)$v{lqaI__AHV-Gm*ayJrn2)h(U@m*>y{2dW)T z^s8`c^!vv2jlO&ko1%QgFSws}M07u`U_G;bXJ%_Yaq{08I&^bNb#LLFJocK=@ zzpCW0RW;A7>Z#fPiKlDIDq7SYWY7A}@PT(S%Tw_?GTmDDLElSWR7ZvQ*yu3)iR?43 z4JreDq;K?&jKLAUY8UO{Y;NScDFcq`LKj&ccvAh*=;+=#p{bSr+mP9LdXBwuo23&p z26Z0QWPPDIa$fk5k3EnZ8sBwhZZ`QcRgd3^%=UH@Kj(&3n=3eDq4w7t&wD#1ym_PX z3K-FSzpN{RLtMXb1RweGxF0COOL7ejZgt~OLT}P1U4j2ZIzVFwx^8&pnU%a7Uev#& z86D8QqD?&RYWxG%6MijWxGzUvlBT{?U-#)x(pE9<_UX@(DYwSF2~ORDQ8Jvqa($%w z)YjpgGxB|;cBO9lYzhio?;)a#>X1n=;N-Ca^{29$8LxO`$ok$tkq@m1# zL$}u8QBWAR_ng~8${~c3~Ml3y=;r;Sc6GWkG6|%>>t*xDq7SJ8|$O>vo!nbbbhE5J^}vA>pnd(cL_9?FGx5I1D^ak z9iP}bL*MuDtv>WWqn+kz(DK{#rDRL@L@8bCvGR4e{!N^v@%?+g-i<*whi~}!bKGg> z0{$KWA4j=|QkRYi&W3T8Ylyw^@=$7PAzpeulS;J~mk(ivce5DWpnS%a=YN;lp!Vd! z)7M|lxwA^nrj-v7@30Q)eo05`!&*Z^u5C|L2v7UGv=;JL>nGCj z@%XVfpK|wrF~+KmqQh-&yxv0H+6!ZTC(`w1pJcRvPycI4!dY{-0xs^1Ex4(yaA9v5 zJcCb>It9NJT56uGIkLUOM?7)??+mhNX zkD!12b^?86{iObie9qG$PIE zDf8ng?^p-7AP>PkeSyyPdZ{_yTZoRLZs}Xk2VM?@&wZi|Z4=)e#o1r|C-qUG4;mQL z3O<(TZD!F%<8`(K7>v7umF6zLV65J;E?=8lO}hk}dy>Dn`|r>IdJi_7fEtdmB^eE6Q(6i~pJgQH-#QW?WlMcwGN>cmdwz1xpXxoKnCA{f_QyJTkal}HPVG%i)p(=!+S(p{tF^tE^e^87(;=#NKfJIXzT{Uo z(B9_6c<1>`@-i5I{ff3G`Khp9SUSYFS3Hj_KdinI4#sPbOEw4tL-C=V5xyl{KIGo%x#s!|Ha*W=itwv9jGC%KSw4I@I;HFVk3j^StAMP5+vn^m2@?|HT>V zM|j!nVDfgT&m6wsxv$gDi=cD4_F9t1_Gw{nm-LtBeDc;l@{Xa7N>_*CjE?lX^Haq9 zDBW?X8!x{(797++!|HO{za@Ia@A8IY0=hC9KN%0u&)}PA-+xkX2|uqRrSq$_FT9~P zw^wzbE6rXmD)$0qUgf<@mCLH1kP%xy#@@(VIdp;k-PrL#XeJ%oP!icj%HKx1r}Hl0 z{fC1mpNZssE8kupXme;BmPMTOZ1Fv$-rhPqL1vFaIDn0 zpf)!L>$K|wd)4MIh6{9h!zsBtt;C+$f7qv;ceGmAl9p6wn$v59kggfa^ z**vo284tV5#}@8|PuihDThdw|9?rS#QaiUD_VxBy??-#k!R>U>Lw4?PqYLZR)v4SB z@Kc@a7hFAXopX5a(5)91jm2S|#r$;%{kqci#m8Np_s~{9?=8;%PVz5u`Sq@$?u9bn z34EyW`{SaS`UhGzIG)tHdzyI9OEZD-b_ZiAFqBueukiwTHwy;xTvrk481Yf+BAbuy zsJi2nT)SYo{St|^ubz^7hBRc*=&1eYKK)t2geO&pjf3h(33TxG$H%Et@)f}(UAn;KSKCC>Z&9c4yO+Ge z7ap?zKmB*48;s^QmmAndfu0+}c`)-t?|m&a3buWw(@&$~0+ z-=cDvOaxzTE6$_56|`v%{WTmPf%lgt$4$xn2=3EiPe_q|-#&=UF99#<_fw6hBEM6L zdN$E_{v68sK+i?@Z7*Wpw;27%+g9i->^qyg%07KMuO2f!j1#}vo*VY{u1~x3G{Cw0oThneXai^6 z-y}SK0bb&v%z5~?oG(e|v!(0W#ivew*)MTJX2ctg$c(OAz15AY%6B?>q$72vQt+&8 zuU;};`+sznL~|7{Kfc`==xAqgBoE|!BQUbiE2*~&?_L~elPw+j1dH$6uy=Z(IX=8- zeuAEe+U@ie`#hTc*o5xz^A_QJCitrUD(oALO|L==KZb{WdlLEFoN3=lPFLv6hR!wk zarVDe7M-T^4aGhs=^KsVzcyOCGcx$C6&KOJaBhZrS{uAR#rK+bvTeFVx)8f2r2l&y z{Z(&Y8W!G14eh=I>5Oc=4TU)g>%P~s?t3kI@v2nso>knF_(A0FdSrJQ|JU-rg#WAf zU&#L|{ul87LH_6SKVNtS=kVVCwOxu=f9IFjocUoN7O&DK8xtxu_k4ItZhQ%}yHWZ! z^@io5<6L0Q1-6qf%lGV*+$fjt0C3yWr8(XIax-!HJ!yLj=fIxyjCPfyPCvJrwR_#X ziw$NJmr=WSo^123r9Zi4-dxi1wROEJ=gU-4W_D0Ubk!Lt__%k?(589kx-_-hmsvWd zY2Mj_H8lxqDQ{k!q4;UZ_*r|_%{wJg2fmimYYneid)LjYQQ88|YpBgLp~p=8JKoM$ z8~OHicaVP~`HjZQ(5>!nz;?p$_KvFe*XE>uHaguAx0QCOO&#ok8bP|zd@^+{3}l9I zzB--tanqlny z0FHEUo;Ixa=cIglK2n?eF|fA!yQO^J(bmij5e?V8ux{ROg$wnr@Mo4pH@;)Kf$uLp zzOpY!dqi~5ITmEi=Mz3ZA)j#7IbGqD@i0A3>uYmgqimf!YgwQ%INegf>3hJLJW#v7 zQJeepK<&D(0OOuRXxEp0yRbvlu21-OjY+oabFN)#Tlf?HpCk>v&?;VQ_WelsHdjV@ zKCF7KuWMcJ#*_AX-kMX~p0*j3d6V@o@+mvieAndtkl^yIH0d?n;ifmP@Q0cnuz5Ie zwqdsk-Z%cnKl;J)-$;J!3^(74b(r+xrEXlBBKX)2=(*%vFRgJT`6fF2X|_&Qyu&-_ z;TUupG~14hWImSpO6FY}v!XZAmkS^8H{8KR{=Fg?Ut;*{$R$u4^{@D>%h@ z%n$GFaBMKMBb==un=GROa4 zd}$=S%73CQ>4J|<_6~c7ZO*;j+pEA+U#$=3sJi#c=9cO3EcMnI_TDIK?ipuO$Tqcp z<*ZlnxeDN)q5iQv^pDP6zC{0oWwtq92=k~+Tn@ap9}~&Fk1L0d7(=6Tx~#QsbaOkw zh|^eq)jDi^2UD1aZ%AqF&h|=_V_jBh^yP)VK5HLma_lTJX^LljY3;9lzYW{R`Khg8 z`@nmJqw&9Zx;UBex<+@^gtSmNzH`jc+WG){uTLK!Th<4p8IIU~d4KnnE30+oZ6Qvs zU*fj9Z@d4BGJfBEbXLIiHT4Vx!^4UDVGTI3*58MoosOQt{)hhP8J#mP)a&WAv>!UL z7sPbB(GR*Bo!A30Ae~4PzUbZmF`YWGIc)BQ4dUyUt{aF>cJ4y{i3wKD&JvqX0z2W2 z=mkG-*zRo=#@Xndz(w{>pw@UkG|H{&?&MeWCk z>8`Aw?((s`tC#uKQJ?(+17aZlyhXJ2Q{Hu-=W`%GXT3i2u2VcP4yT`#_iN&zYUT{p>@%un&OmCWzctaFEs76Im78eT88*-@Z&n7I{aBFALc_F@4m*&lyOUHQ7rk5B44Gy-d;B5D{CgGiG6Sh`7XCQ2F z#sG&g)x(jE{w#1T4V-?XadiQxccC4K=`~{uPMsgCb*^Oij2R9tUBZ??Ib?36Qi?Wx@$=KAgOzUeF;VOo?l~WuBQ$4$u=xi z8%&RL77*VRyyxdHCTHIE*sl8UHOjvwIn^FljbU!wi{HrAygP*rajkd{8I^3VcQDnS z^QcdA;VS8bA2II+uKbyMuw%C!&Ugk*RED#3(VdLwvL6y}GETUL_|T6o|2}(O2~hz7r%e&M(ER* zA3`}P>>cp_PH)?*@1;0(8iQffRc<}9M zU2XWY51qdEIHq{ZUh9`y>*bD)4d*sZ$vsJ#jow#6+H$_*y3r{+`1W-#_F?K?NVr{k z#nt^d-@a~*HKJWozXWSYf!_7&W_c&$mEfg4wEF~~wwsTJdR}zj>Pxjb)X4{m^p)|Z z=BV%{_FJp;mTb|Jfze!l*XI8EH1gXo>M1E>j~)EVra#v1qVvst`UIJ}6<8kEC)op# zmyZtmX}*)CkL_Rl0bkrqUg<9R*`)I{&#*NC=xDYI`bTz%#u2acY`qTM0Y95eNjGS& zp0Hu~_VjoF9AuY-eYaFJbA8kwUY9t$1S48!4|GVqWPZq|{psjV@sOt5-9F5Sz9svD z{ZrO=PrY{Fi@)a$Q0*_6r#3_T{%Ec?dVJ3Smq`EBwXSe>jZZs`G*9av0Q)fq`z&Dl zJmQtCgOM5?@%RM3#VeCM>=6Z9&g$ZueoNAnZ_^h>H)I(8zLPplKIwxL{P9(IL*?bW z`LYXVfW_Dw_f?AT+g+Mu-e6M3{66|GPno_qxz#s(;@k2X<-|+3x^@=&C)hg>$WT;n zWbS#&ncf_R-b}l>XZMu;&be+US^`W0R_psgH{TIidkxPv$zfC&n zyTB{JI3b3i_WL$|hdg=8s!qlC(FFlr#1pc6)W>=H)AOyu&}82#XD1yF9N9=Ao-5qg zCEsr$EyDFSa8TU|9rXX8-mCkmH(W<6KkVjtr!cl8_fM7V%m((o_Ky0y;}{Ej85hO3 zHs(}1-TNGT*ZrUHd~K@teWEdWXz$Bam8axt!J%+pOH?kU@eG~4gK~dxPNdx{T&cH{e~;Uj z!0kSV+cDr}b`|aMdpBfD%7?NW96G>PcIh#c7f!N!gqLg`wO@I?ZK->yRo7FhleGK) z3hnv7)zu{$ePty%@n^iRES!^|m*s2M`hoO1 zyl(wJiF$OG)X|QnHtzFQ)CR$IH1c_EpBFT{oBV38x53F5+2Ew9-Oi`9q3XA_x%VOC zza?F|?F~kE)*pZ!$x=A~2-Za$j?0S=s(^E{gCl&R@i4%tANkh1IsZj_*x#vlds{V^1;^Ieyv>x|G@><+POTkw0B@KLfmr_f}aun@`Dcq2CvWI{fy6pKNULg4S4CkZH|nR=D*J z>E7nHiJNr>)bWntRI}-VR(NjJWk*N8=?1++^~~FPJ8PTlteVDS@!gleV>^9i>r7Rw zi#YkxI3CLv{yyE`1-$M~H@1kEqIdbUE~UE`m-NgH`ohb+?TLbJzHL9IUB7%a>caMVUiHaB5Pk|52Q_ zvrZ>E>E33;iFJy96jlT4f`X+xn++CgCI2X_^$wQ&6oNGn4b;E={jA@hm-_WK@e4F8 zmJ8qiDvutG>>6a_%>(3bF6vvar^UB**n;}6cKj-sk*)~lH-WCjFEp=XSt|Ef1#>;` zuR}-0cOx->m5)Gm$PZ+^k1mrJu_C{<4!{#PrrZd;_2V~=f%_SyLa*QZ+vg@^n2SD zPrv8s#nbPm+{RZIPyZM4KQv~^^zT$Jnf~o*OQt_qzhwHi<}aE4z_KONTdrL)y%XAP zN{{c!r-#|u$9#HdOSX)@O_#M~%k@8`B|Ad@>6Ywh{g<|6$LhbNB|A?4sg~?S{f}+PRUtmhTPD zaWFT3dEF_e=MI!J)^H#5GWhuFSH#m3kQ?sU)V_m$Y>Qsy=bqfe-9trVqQ=p!z)Nji zdhE0O4`v@L?<3^-Fmkc;5c0N=X9anGdI)*HPM*if`@=)Xdk1-5pluHo<@J8v%)Ou7 zwSIleD=TI`_DZ%S|4ORlgLmC$dHa8_I`n6s@rl7{5avCXx8V*(uTOEm^TF~=zxm*K zK6D6hPB;WOpZ?U~b%pIao%dJg#x`e)2)^4B>uY-J#>K{&nR2m*TudI=8>I zh++RETtXk~!0RFdx{@A^Yzt1zS zy#T&{WL*6lcNPwGUa^^cnp=4Ix&v`9bGYZJ=LPEdqsCI|3G28Q`(Q8gyXUFv1?u`E zFl47_T)=MX-SVL7>)&4Rb?pYWe;?e#);vr8+r7a01NX)y_UQDZi@(Pmd7;et{(iyY z`|oe{;lDqWEZ1K@um0}A^L(KwkFWoqzNfPN*B{F3&6Y1X=UmjrFz*Qu7vVDiPxZG( z`J9yvcx#Y)njR^trzo%J_ru~m1K^^5^=qXS-xu5h4xUyg{@~DPm9=~W(@J>#bnC%! zAOG0F^IW!l@H}C^eByQFeZDyFA<*rL$AwFQZU+vb%-AQ2$`tisi2uL-JlR&?FW-Dp z@evRExF>_SVwnkH?*2s*CU-gFYj^jArX!z>{B)XcZ})3zgf(By#I`bf)hnmwUVRyR zi=Xrcd(WAREbhTJ-XUKdYoh)66Rh0xlzHXPQMv0>4xJ@io_#;_{%Gaw>_0XlYqq3) zAfr1KivoRS*h0T zZkpDMmqSy{ISscn`>9`hL-noVZA%5wpZo8R9yD>WLcba$lv%LolgpI z-cj{(RqmGJvNg%FCs@7XdgN0wnmQgfIhn@Vb|7c!AL9@4CTRom2WbXtTL1l{eK1Nd z{tY93BYK8A`q6m{dzxb%ywLVKI3Mzk%*5kze(!?(`#O`OJzWj7bvruC{HGzboE zw+BDU$8PiYbtmRVf}8l|2L1)_6TsFOpti`aNX*lNF;RYeqX~AT#*>D_oc^+J=o;z_ z#g#>I37z5cTHoOJj&-1i;dKUSRu?|;SUwFVIt)Cd!*qX~)&t1*a@El%+Zw^&X;W%g zt`WB1;#>?NL7JZg;W}=R=RI@pow%yr}-ToY&{vGuEdqeqTCi zF8_dSvHHZDl-U;IfIgCcPCicU1JIhH<&~a-4qt-?iWmH-?*e-!@Fi;Rjn)bD&b;~B zZT*M(Zh;Q2KEas_9D7?s>C};hpM3tgYI(e0^Q+(iT2z?)_|5W|+Iq1)wcy=eP{HdOfnMd%0JATyo0j*ojrxx^s zXrZ4k7wfWZ&>0@9b>)4!(cyX0WD^8Dm~nj|8qDTSHTh#zR}FR5&~NrG^|l)GDHr3Q zyY#e9m3QZ%j26;YtmWj>(Oyx(Ram&TGe-_>Y;W%_nBCU~-V($Ob_Qjyn=`AwJ6J~p zpWfa2r`|pKr%(3sKmF~hBk*Sz+3M=w#TOLW(?xkg+xw_@44zl@YF%&6A?90Ll+V-V zLl&Q$AKVhe70(aUjxQI%>Mzf~9YUTL4AFiPTleTPVEoY980^7$9eDih#UviD`EQ5E zEB@Qz@e#i?LGntLpL*A!(OPw% z`8x8>e;s+RD$X1DJz^b!-!rT8Ep81;I>7HYvb~ZM3j2rSGVJHEaq8X@ySMD^+H*_3 zJY$UZ9ld`D_lp|;M|(6c*STJFP)Bu1?#qNLxeM&yb+4lK=3vLi`UriHuf_&v3{2d+ zj4c}9JwLgr)$WU5xng?erXgc0w63T%wBB=a72BrV)`*R*`-uOS)Y%>v%B@HLhRkY_nnU{gMCw8zQ&OO11hu7G`hFg2w&4A#yTvQ={qevKjyH zydi=OJm7mFF>W2aO%eKTDD!&W5IG7yGConcf8dRfKOrl!fAJH>Z-iXNU4w!d5nqPZk-sS??Y@F!{i0$)aPDT_nD#(Pe)8eg{I zb0E%q*PN+m|B{`lALhy_zj&hte6)wruhVD_c|ZD~3mlDiNat;W+p=YadVD!Qzf82h zKDw*d?%1WtGp(^L4*-eBWexC}(JoS1& zVys+NQg~ZMb!kkaew~NUARCPF_O=S|q|L*()@pR(9J=&X{KksD%Xwo3Se?TdpLth> zanRxtZ>$j4NIrXG#f9yCl<e1Rm9mFD=ysUjg=ri zeq%*&p>zDk%29UL|K_;8q6_V}H&$xtU-S-Vnp^VC39ic7q);^V?M zR*36j{Y3S55$F=n5>OHE@jnz_&gj zPiM8uM}Jv=imvKU=&bjTgi9y%&(f#%uF1PnhNs0{Idz~WXCM;}`pykK!x$Y;g7hJmi2&E4L9yq@uK z|BVaSSAH>fN)EO2z@zZpj`}EivUJjKEd22xTpEOj2H}bze0UH(A_yNDgvSTrV}tN< zLHL9qJS7O99E48`!e<2GHwWRj1mSao@Y{lLV-S8v5S|x=FAl=*4Z@3q@cV-B2ZHcr zLHNobd`%Gka1g#H2!AsOw*=vD2jTAq;qM3Ge+|M<1>s#m_%}iLcR~2MAiO&WzYv7G zg78a0_?001S`hwQ5WYDG|5FgYH3)w;2;UxrzYv7)48ng8!lkncJUlcAR|Mh1gYXeS z_{bnUJ_sKhgpUitCj{XsLHOh#d|D7bBM84a2)`u=pBseV7K9sv@H>L=ydZpW5MCUF z-xq{G5QHxa!dC|2Yl85HgYZX#@W+Gj%|ZB|g7B?D__IOy_8|O)Abe*KzAFgd6NJAR zgj<5}w}bF^gYfr*@V^G(e+$At4#GbT!cPR@r-JaVApDyk{JS9hToB$JgkK24T|xMz zApA-Yek}<9EeQWT2$!B4@O%)i2*QU4;Uj|ZkwJKT5I!~t9~XpA2*Oi>@X0~=v><#& z5PowIeoGKOHweEi2sZ}dcLd>iLHObz{N5nEI0(Nl3O6jj{PL?#tV~s4>Qg>&>Y1l4O)bCtq$`(RxAMcwD|a{DQR#x$uYCUpmtR-;{^eI( zl}gRJ`sz6=ud7^k)s@#>bJgY7R({}`tFEYAemN2EUw&fcl~+~HecxsEl`F5Tyl%zv z%B5FdU3vM^8cyP;7jLD(P&f*{(kL6)EhvIJSeW(k7Ouw}ahK~X&K&pGc&li|D1?|Xf( z=lWj1>zTBlbMDvqcc1e)=RW`Ld+s|pMJ;z0x#y^bzOXxJ-{>z6BJm>kN2T{Fzt16u zj=VyrufVMq2i@+lI2Oo(BTsUu&USjj9$$ey-@h;3F*DPgGxD7(v@sNR7Y!68%^cZ_ z+>%J1zgW)5WEN&Qmd#tfbkV|9IjYF%^W?ij;VEvfS{MxagLCZpPLJ2^vWNZlyrA0| zcH1|2d@lcnDfXoyJEG>yTe)~)4pLaKVilB;@A0`^r>U?f>~$-sC|sxt-JXKNuqyKS z9L7P8=m@>3G&N04S0}1;HABr*C#hL#wsNXmm8V?Ft@718HD4`InQEb0q>fR?&bNo(ZKz_saXSu5^wTKcR{h&8T{?|(Bc2E zJ=*E7&Y=^uub=*Zl;*{ksEa3{G&<3sI~4HyLhcE3rm55kIBmE%G+_=(>;e`-9qyoT z>V#asYazET~pR5Xl?tB#r=BZOAs0kA#eQAeEr#NBG1hsV43NQt{k&*?4nhr)Aaos>RZotIbOIp6MaxpB>eH_p+6&nXi_b`#m*aR~&19$%g( z;PeV0tquO*IyrRv3WCle9F0;b#b|WlyQc5Xru{BySPN@F%PMttQd!! zc^FZ3r{lmW{6XYEg^B|KG~$pa7o$fwh#n|_?#&m5zAtd+g`MK;4B;a5U?5VsaN&Xf z`R-uQ?NZr6f1cY_9CUisDmO+lbPZ}@z!UPjaNxs8JoucUFcL1n@Fe9yZXKq7aBMK6 zl^gLefO_&USo-osMR{(tra(vqFtq!_INik)Bu%ur^VIpN6Wo{$M2>YqHB5+<87;FI z7oL<{UxB|-dIlM+18L8QHAoY5*{2WfaQ-E_d?5wp${a*gCYf`{Z~(pMVmJ?b+(8v~ z7KBuhJM45h!o(m*L@ zj^M%tq>2MLiCZ7$q5~`l%IFvgim_&2ZAYdgN-I&xk!a=io!9i;t8JAh{ae15Yv(C@d}1ikhp zk2E4J1b#T1$&HzF!38-mjb*5dX(w8x2y{{LCF;DadFL!#X;0I0&Q&=p&svak*2;NV z=Z{k7EnL2M<-BF~1uK^4EL@!fmy0L(*G-rs{ch?6sy%Y507gW&bdb8}80q~N;xfj( z#Ov{`bGuS8wuQozFD%C8Qd}e#J9-w(bVK%HpL0E?e&(8!p%g78xG^9TnhT4e{*Zk| z!0lVLD$AaNRJE7?_>(}8-G`qg`?fDp{P=1G_2m?Tg!{^>`zFi+i`^&W1hhb5NApFNLQIi0iB9cT1)35V~ zP9p4R%a{ch*ck>{nix7?ows!Pq7~=sdFFX%&s(|N94=WnFH>QPj1GMA!0ZGQN9mI7 z$fnDSsa?=rApMOr>itITHwuPYV6NlIS-P8=Gr&2xq_H+xl9Q7?b=s6^c1$xShJGF1 zjz+ZKC?w)T$BCSH$&pKBe)!fkf$Z2sjsRT|~XJMUW=Cv1u{J+ZX ziZhg%hr84~JvD>&m;3E3xmxC5kFhn|jd9e64%NF+Es`D#V~g9q0_(US0*CF(FuLS> z5HrX0mcfnom2MZVkG!xNh`Y~ezS9M`uo!DBb(YUr94^GjcM+m5@&|K0E{roeRdc3g zZg{)wa+-Nqs%}6qWG)|t3aiC_pBpKlugSxJDOag|p&rInj^A%z=JZKE^m#&RsjT*V zPOp8HOlyO7y|UBiSy~kEx{FXcBrRE6j2?Rfrb`mgljpW0ivyhi{p>lIU(3+DO3yE& zoAvtX|5eMjr(p8o)xMZthaGJDm;@iMunG_51wEJ@hr$~%$b0fJx4;OWE5p8W7v&;< zSmqXCRAV@w-?+*kg#p5?r2dPX=>APqZQ?Y#6^K>r^7z$yjO2dI;){bA>M*Qg741Z~ zq;j2@aODOu`gt*-Mf*fs@&!;x=_U1msW2097GcU9)RQS~U?@co8TP9+6V#eDYE7h$v>zJvlP(Q!c+u6DP0p`8QytkKG1L2QcYF z^2MI8%G5K-Rc>!SPN`!raQgf{Tv%R}g{FtB&39uSkuTdAGKrHt50zi+)7_4wWm=fR zB+VaGxhSF@Ar$6Lo&q2ApI7J!qne9@h~~yj1+z%$u`u#uPKK4K3SiyhNBc$Nkg+~8 zBM23G0&0Q>gBu3jJkwq30jJ-bM)8aT)5geT3hw5F*l5B$ z3Mdm$&aI3VWPYWOA{y0T=2=X3-)svGCa?QuUHTONxe z9bjds*97}e<|NWEWa|V|hb8lVx6rZRtd&_jikU+qO;gu=rL@|QMk`ibc?W}Y-_ z_PqHEG8ZmV##+2&>FH-=EnB`K`^=TAa?bkQ*{jbv*O{A#eYS!^&jsteMLvJv!eA&| zynaK;#*6kPGTHGzi$7`V1c$>+6C4h_(^la(M~u1!S6&AzV)XLgd7C1LGarnygpaq zCzVPa0>L**6rPt>2SD(Am%=-+6n^)h#z63nPlfk%DZEQY9SXs{c7^w$D%_LSBIyas zxg@@%C-s&LnG87zB56qcaggI7vmkQr!y%I(r$Quec(=7OXU#x)i&ZATO8rGwba-)+E&`eQl+DK z!1$$+^OZ7OBp>l6-O_<+^YpYd`-(I43|W@Tlg+eR&u!i7#X%N^&LRYG<`udp%cKxH z``9D#PtFsUp>ykXZg*fZR@3X@E_2CHC=z3`p1Dn45%d&zFbO|FPr!fOI{yEDy-2%~ zb}H9G8btoYmbQw&I9woBTqAMH4yQC6qrv9DL&TmSt==5#0~+EwM%|*qR^wl7QPxD9 zAn_0t7d}K;DsgdCOj4G3H14v+%G!m*)?T2jfpyAKh8Ib8ZB&+|iP$$oSk@+4 zRQy;x)i%yzD<5aE#2;y~uEj&bO?b&@YpTVTiXRr%q+6`nr&v_VBFnHQ{9t<4VvDtH zsYP{WSuAzSEUI>e#a5GTiHkqeqS{tjEWP+KY4+I`>!vjpRqeM}(}GAVWQki7##5kS zOI&s_enh?A5?8SSKU~^uv9xbS?UY$8nPspqN1B&g)Z8r=OX(K892NE+*qa(~&W9{2 z^-0U{B|Gq=vllGZ+MN~^_p!y+{INxK?6z2|zqP2$A1v0Mc&nv!q*WyzY_--LVO2|x zv|8IHSk>O+t=68YsPQ>gYwO8YRep-qIxEAf#>}%?+vi)=tOZtE&H}4iooBVxaVccs;-9LHC9z|v(=J%i&bUa zZnZYtWwmAAZB>=qt(Lm&i2o4Mkp~Q6H^R<(*lJz#h*f3ouv(WqYgMi`Jf+)?G~TjW zo8Gpngm{*FDmbUC^3CmuB*m(Dlwq<`(SoSK#w$nbE_5|9;&`zd3k@m5)Wlv1vr_h%D zGvO0x%l?_L*)K~dolKrWdn#?S$B|HqcX#72p>#U!6KSW@omk9(st6$rJYCHMcYj~ zpLPN5LfRhM7tqGG5&jZNy|jyH`)K=V2WVeNJ4ic3J50Nn_IlbIXqV95NE=&J_)93g znD!>xm(czL?LX2krF|*w%V=+=T}JzI+FNK}LA#vxR@zt6zKV7Q?W<{DL;G6Vm9(#; zeLd|z(XOI>1MM4W-$c7w+p5%9F*BxtmZ`%LU_)k96 zXnykke|TOgvIPB~p7;Ou_}3q6>i1^achJ6<_5-vZq1{aTS=ukreuMU#wBM!uA??p- ze@Xj0+CR~bJ5KUpmnzYJqckU|1867GK8$uGU*pI}(w;y&h4y6H(`gSqUxJ#Y-R(T9 zQWr7YA821jdkgI=XPfwC|#QFYRr#AEf;-?MG=pLHlXi&l%hDzunGm z)*4DHjVQ{z|3ax~_jm?~b|3R!xSRJl{5l=`e-S?6zi_AXukw>IB>cYj1tifu(ho%9 z$LyE}Vj75PAf|zs24WhBX&|P7mJWVl6H#zN*mRzo&H zY9LQT-iHjgD$5ke?;x8X+aMo74zelBB8U%i4dhXqvUWrIAYzEw4oDZ|TgXAf)v)Q1Y)BY#3#1X!3V8?eEhKS-8a4qk z9darp3$hwg2-yOug49DEhO|IlhP(;sh4evwfQ*V)!^T2VATuHJAvus-NC2`4awVi1 zQV(f{v_am7`~ca1B;rEmK(Zjehxj1nkn16}kVZ%gqyzFkeC^akrG66CjG9Qu+ zITvyP7fRsWiA+?Z)A6 z76jeSuzRK3=W++#!NG1h?vikE(7h<=FIrU`%y;Iw2m7Aw@wxmPj7z55>)aSQ426UK zjf`vD76skzAt@|&hZhEeex7Zq&l5HQv;0oi{AJmWm1i5y_Itex*Smf6KC{>z9GddV zvn6+nJYHta6k~zc?PS_?@CTSZ!CWS+OgJR$jdsfpxXz65kgj;11^dgGGa#LT==6NFcNw zw`mwUV#wl`j;aD&Gu4otz}C~_C&1(3PI zb>a*v@~?LfLYzNBAzvWq@rCmTI10ngymjK|$ai}5SwjOZ1;x%Fa_4Y_!nlNjP=Y@6 z_##LC;xG?`gR+C7`u3r_M6~71%X5oDoMAssA||X{Txif5O6$*q4Rf8Nz#R^IiqJlQ zp@`-3pwZ{~L(1XuhBiXeE~yb1)0M+r;?6@_<}~gSxaTu!e!jOjR0u|zh??(*vc(EQ zY57JAa!{;2(N`cZqJ5Ylw_C!5-IB}v4M9&Bu1Ll0Td(ba2Vud%;*hhzcuA)H`7Y@! z3W@|A$g;;b)9G>r9ZnydJt3E;Ky={=1&V!gIeJ2&fbM&wF-YE^0jD<-+3PP5mx5_& z%27lenR*Aii%`X|*J;%htus3FIeq?+*X@Q9Oku^;Ap+D1+N$oI0>(ce{ai#9fpDQ{ z0gCpy9JxVU2zkyBPBO&ZH2xt@Ay0wN2}KQY+~Aa=4mjzqcptmmXk@6Gi2ntCY2Xnm zDh|6#puE8;7&mDm(cy}Hbk!9ZbV{3&a_Dm7z_hNQI}ddibjyVzHm)dyLw~1DQ?pQM zNNQGWoJe26A<3AKs5@j*4~6oazIUa#4Hd(XS+d$xUjz%U5XrBq}xZa50-AV zY{ZVRcfBLm?++tIXFx^%EDrT~HXu&*RO}CPmp1#fxksC&S7TVU&0nkz$zHj1*}RqK zsBx>#TDfT6f`#hnI8`pb*5wX$+*vtUOP4QHr^TsCxW%bz@fkLM-hwk0uUv7~@=P^y z;mVaOR;mM5sD+V~Rz*@eCr-7$FdDJ;{48dF(@2=@bz*iuC}vH)m{kvtM4F~W-(c~% zH>g`Jx_@|~kC zjtfG5pZZ4J-j~36Vb6NZ9n_B&m&<_}093lw8mF2*kqAv6N|2%e#%AN^cA5D1T5$?o z*bNBUhd?O7M-nvG9p2zZrN3^?^A}@adJFz_J>tK2rN=Z@Q+L3t1*eEp4JeAT=6k$h%ynGq37alZ3;b(7LgMNL9JT3FR=p?T z`(P@Y$xg2=zZf0eXEvQSV)r3%Pq#$-7ISE~r=T#`9}KB?Y%DaI4S-E=BH?)fo<%*n8uGZeKyT z5Nf*&6Lcr8+NL;Mb`Bph5mN8RVIqVc2Qk}!)aGT9&hBsJ zxN?geSA0JTX>JUoIGcqE+S^H19Uiv;6X_3Ha9&Ys(0`}KR35w7Gyf-4^{yE$` z%cY+Baa4D|MBh^{W_vwc*H2eB#>c6~KZ{GFPP}hBQn8uKus&W7C=bT#3$Q63`U#@S zYQPTlM7)Z_pyG4nyVUC=Tz<7B9#hSLADxq`j1Q?d5WL9i2szQ?%iQpl5#fMCeF#_1 zMa<~&*AZcL>BuD>E1XB zHU6gPt_DXt-jcZe!lL4KI5p}M^k|Rjw?v&7Fh3pzErrzwqjcsx`5V>0&^VmIpmU=` z^^KG)J~t}h3_?q9j0&keNT*ZRa9^jKpcSUFIdjpeQ33ajhKkh#qx}W29vO{`SYKHU zPf75qC&jFLS|aHIvwO7koF9$m_=#nl6%~;0Mkk}2+nb~HFswdDLX~@UWSHoMWL&;u zOt9F8*@-`(?iqu^qKifDc3WV!d@W79s(+N#Axiq|7_U#gG6wOGsb|K>TvWY|C}q1O zaCg7dqQj@Y8sn8^%O3c&=t6dtkAZ)UPI$*_l6Q{U_-E$+Jg^9`~K4L;X-+Czo0{PBT8Nn6XZy>R_s^koIYcw zqtIQVuHVlibzF^v+yO^XNZr2Qg~2df>h{CMaR73uFOC|RNE7uKN#vyi@;z=ZCU@pq zc@05J8%3-Ak4aEE_aC5VVn4vQ@m09nu<}NUrPT8Y#U7VqeY$!J&fPyrw5}~u!;M#n*;FcKsjl-* zxajd3Q|m1ULe1+`#eujm;fai@>kkYS=N6$2-VMhx9lQE6iCuZAm~EF!Fj;)I9vJkw zgQy~xr#PfuM(928Lp#!D?PeVgI^BJstQNn5|E@CeuSdR6(HQo^YRf?$mkV`PaS-|) z#K2glbJ~oMDn7RWlVv&8(+6Vk={V5k2|07U7(qWdFkkICuuxrpP(UW4=z8i7!kjj& zwjH#=;|dq5#)HrY0NF>h+O>L+YeqcsV!rCPE454 zfVYncsGVbU_wn7(t3*i`+&2+;N=-aBY@o3d?RMtS7P>k8HYsjHew^f5hz4rs{An7Za~L%&0$!aR2_yc#N&f1 z(THz3%%;ju#rZqu!l*qNM&B&SgL>t#T-9+{f%?Z`LG{jI7(Jeo(xB~1x8hJA9OldS z6sRxZQ~fzkXmg@CwTJoDcZcaSV_gb8qTjnJ2_Zhfj7)X5O7`|@vq77cuZc^eHv3+c zREn_8q#n(o{!M;@d3|glB8FBD+w(+&x56}BZz6UOFfWO z97H>lme-G5H2z!8*0U?l;&8e1oyFd;dgO3i!<(&e?&^`zWiR%Pl@()D$KkRQ@aEx| zqM|1_98*iDBcQqwyzXmEPSh^64XFCBl48dTV(xia%xYb(HggOUIa0TcMb|SHabA)T z%`nCF5x71M*ZNn)wNa;02@{tfRHt4WTb!%@F;>OnqF0xXb$QmStH!!7RM(Bo$2OnC zsOP4!1?v8>UiHjajD#C{GY8Z#SJUp&IwT|L8yX zYQvS1iOy@qY`k2|rmbT3mur_RaODo?X4W0!F(X6*_l$QJ1;QKE_VMU*)I;NCKl)&d^;YsxfkIY9)h3MrANohj*#`PBNDLs2(uzm z&ySaF6NkF}2#g0eNro!^Bxc>s$d*mc-gbnX_thh0LePm&^=0s}VvhOF5hdz#IP7>x z>VLO5+nlQZ2zL>BW;X_&Uyi^fcKMO${GS$;Hr*`=H+>|7eb0w_J|GSANL+nEG(Uu^ zx>M4?rXeoW`;NS@Sf*3z@gu`dr1KK|8y=A}bw4iY^xk9D6YDRIG_xv)bi?Y?BZKY& z*$~2|iOmczI=^oa0Bz_e8cZs;n3nch!Y3>MnxOzX&BN!fsTgraQVI|!q-1V-)>*UHp z{xC*wv+FUs(GK-jiF2NM)Xp*bML2d}BQfeOl_dKAh{Kkx5=zF+UG~94^uH0f`br60 zTP|kjWjY*&=xdLbA^HaFG?ZQ;K79xUmFOY*{-ezh{b%@AmdQ!FFGi4_%MoPX2>p>A zBlOqyff0J^(fR7uqYKqvj*g7bFCMLj-M<}eM(8(=9y~(dY>$l4ci7RQ%m{stUHYQG z*@ukK-F7`fcO8v<{2rO9_ra*foi&*{n&J7K2{Jr?FkxVL{>Oxn>IItN`Nj!mc>Znz zI`g1XqlS*i4sc)NA>FO3za@u*k{ssY>hH;Fj8lDjc&;8k9cHveobM0M zEAW`T!CwxK#B``n$4af}Cai85R~R0=oU9$k;SsAUbZ?IxQKYUqQYNvHQS;6tgX*rM zLh5alp!a!Qf|?QB6WV$#I?gxcnyu)?w89qI8M^k^ft{h-jtvC78!-swVd19tJnlxM zu75~EUF9R7x8K++dgEBLSJZ`|71xYF(1scb8rduQ=~%Nlv-vo*>Tl%y&G$)y?Vk*1 z&hCa!SG&0FdOza!7~DF`Wco8`ujr}cWUuHIcvbvKPE_}(1Q^&W`s6sB=iTtHsYJ2W zS2)_WS5DdXS25e$W#VMAQ<=o6d;F@g4`PE%1=8DvNMO#v2^7Iru>)w+r zmc1t_^?WMl?`Ra4z1t-hLw1aIr|6t~g;+Z-lbr0qQ8Vs$;uJqcig?TMP}M8q*7Y^= zwQtAh!Q%&ZjGB%&J4T-G zHVDFna?`}j#~wc%Bi{`ZeX8w*5_Qug3_$P6nA-e~%0&CW#5R~o;Ss;epk%)x8ZMgs47zjcBnp^Bs)~<+ev13sw`E{_peMfyHhu& z%Kn1col#yG5;_&8rRX;qMwNr~W+hAhz#mtmH zPu1-YW8n{}xc`J3fmck{U7osnvTWs}KmF5WB&^5r>aXH0xJ4riRrTUznV;W086*GS z5TNC?k?4-A(qL3fgNE|Vwy^qqvd@!?rJMR1fjX~{`F{CjGD9|{zj}%+zW+4Ern-{h z)wNk>W34bzMYtIdaH|KW$W}np6kM%4C3Z)zn4S8#|3jo?!*-_Z6TUtrvg7vFlnowK zrCH#F)O(1#y9YaUYA22wb@u{mj%MN&FQ_G)Thgm8o(?VPo~WseaV)WohUNUXi9y^^BM`+s%yS zPPmuv5SPk&i#KV^>dm{H64p=G-H*7 zX+evg_qL0F~L(GPnY{PavwpT>I?BxmhP@-K`FzDi?w}`6y*=IBLHuC4SA98M z-$X1nBj=VA^?e_mP{oNG3UR~I+*-W;L@dJO9_5qMeCmnmxc;h7#Ch)0db$NmC+=W= z4%fCSaqWFn8mrV+|A{hZFG~-|+Ud%4w6X^!K-c}EQoT*7?n(FOULdzTw!^!wPRIX6 zu8pdn#q8ND=I&B)Z~hEdi`h$kIo&9+BV9VEH`C?H=}u=(K0->B9X7PZU6STbNulpb zNulCXeZ7U%?K1{;OzUPqM`p*gGF`9uvARz;*JPw@t?5#lYi5|O)an@_wQUAw%@dJi zKeh^xH53_jbf2ibN*_0Fm*cYS7Op%3h+z?D(}^qym!}$0R6pkvAu(%>Q5wSgEn_d1l;7n&6qD{7GVVP zhSc{n2UY`N*@ymR=6X*EYhZQxNyyRt(k@%;#q7UNR#9>T`q7h$kmW+P9}$(~G+=SDoZn|D1&y8Y_9mD1bG~kuYkGg3)&*k}Whd|GL>` zs($}$v`QVW?FaFmhTizHGZVE22<|)JUjzfgzNqcr{&#n*6Ago@S zgH2lXk2!K1B%~VVc-7N$0v`3!94ztYs9~~;Gn-rQ>do2c2CL@;L;E_Gd*@)7P`l=! zR7c>On|ZDASBAk0UjEW-Qvbm3h*2Uo`BKxms5OB zEShDcM?d()DQKbCqC*AWB<-{lre4UpFyySm_kX4pOSk-Ww6nCX@U? zNZoiU79?^|b1H-1bt-z08ze2X;QCV~35R|#LeKa93f~$e0EI|0afKUmgP5SGUvM_{ z%Bd1i?L2iodU@Oh`V@gXcS;S^^;#^rds2BCb|r8dNY}%^Pr1;czBpCiN4XKV+537W zWF_}E3pO6My^O%^;k4nol+lil^W_B;WK$w2*+uAH)Dqa$^ z=P5DkpA@q}yLaFLNn8YQmunvzJQ-O0V8!-O25!UpTx#cOrV7WU1ve%S5m z4D4HCXCah&WvD6Ev6>ba{ zLbWeupx!4U19g{*I_rKis|dM?`tUrNq&3e&+rx&fvdTRZ_4K?G9g&+SuZq`G(jY5g zqNcFfG7o)PNPPjHhQCX>D&E%SJ7|x{)7JSYDINxe-rf|m?C*#ta~idIzFarg&c}|d zx^=$3;dJMG=_l?(l&TM8YSjCl%=j_e3HveSe|3Ij^3{nDz5kTt8a@>zLU(=h^=9+8 z@M*>@0~f>|nQO>G_{s&ccN9{U3y}Fe607q&G5dcKvu>}LmETJ`cyh{(i?ewFZls0O z(+J=51Kfw1DeS!q%(dIFKu;o|t_5Dzx?qDpi0#-n5xDYO>Cw9vNOyMQ0(nkhbEaGw zxFxlBzF%FPi9YWu$y;@wt@2Oe=2qV`{VNthiMliFyHgS>zg^57cO#U|sPoQ6a><3%eT#hR0fg@B z6)ir3qxvs(ajuaVty{$`tJoLs?L~p0JKs~HK3Ehg&Xb1;)3iwy1p7rd> z^r{;dV%H28L)`(a&6 zeVG?M@xOQB*aH_e_DbTA-72%nD4@qhnPRjN87x1 z$wu56QI{>n;3{`b)dTYiovL%5-da-M%nPgfrRXw_MLu>0s?u4V7( z(wkg%hBEhHKU=DIe&0AfjQhfPb`C`@KLb}wi^Q$fW`oXJQwQ>ao~;!zIm|Yn5n1;< zeTK~a(7)o2_sfXhwO2Z-wms4ec>nghGk9m~C;0Dbmj<@GL#{2^Lcb{scW`gZQZ~I} zdh-mq>9sja?(C|&vuvs*8CtGM#=|E10(=2Zdnd!GdkT#5<6tyRg;6#Y$(iAJSC$^5 z|BaBn;+m(v%Hr1X=4Ce3AQ8JWVDukn(Ko*p=zMR18^uyMO4{Oet6m7{p6?N z)r9Q`)j0{qjuZ)m5#s%2GNa!K&pL6ho(y+0q5O84*-HKqetV9GU(*TjlfLTuTr0E`fWq(T;A{UH5x&SYXj^PX;)-l zq~4cD8|AMup~d3$?j7E5O-f2mmA}kpTS{7TQgUKqvizkbFIf^^GB+a)e=b*AS_OP#gSQn>3NSH!wOe>99SPaM-oB+0vu`HCxJU zm6k^Q+hOUj)ark$SJztFt$o%S{H$edOAtgzMFnuawE>l%^KVClB( zwPt5kS`!16))s4Em&IFYDMw0KsTG#2W4Mkc3Uc} zJ(k4Oe%oGK8BWq@ZLn6@x`tOW1}LG85Zut=8;zYm;?NNtGorF(swm1|!j~|CWfF zBY$>#m(^}h3?wz zu^KE*)|8ZPROuSs5K^ivI2{T*Yp-oq^{{eFa&jvICwD-*9Y_TQ6WdyDYq7Rkt87h{ zeoK0Tr6fHwJtw`+lAeZx%w4wRdP~18wceK6Ybmv4G+M`x@59MvRoiOg`Ye5x)eUGD zbIV3l#5Y>X<2q6Ky_Vh)YkMs1qna&c!#gcWb+$D-P&+l&R$Cw1NT0QKWYX?AHP$6) zgZVJR$3ZXA}}Xlb#HPfi^_J|z|Zt1anuahW~HPC5Q{*jk1++j^|E zmgLl?k%@_kC8)igxbhM8P>Z=zQa6=ZQqx_Ta@FX+)b!-^)YSOIq{LKxUhSBenuxl_ zVMI>MqFk+UEw+XcZPwnn zT1$(iXLwJ1wWWSUuPrenvB8p6X>oZI>nzB_F4V~yTosj;@oUECEUC2CqFIMq;!sOy zdS$5hQcG5Ue9eft&_#N33rd~c5trR*t+nAgDyffauvA&br}mDhu!MKnOjAGsl8ij&$iW{0iRR%K00%t=l~`=*Z8h%z&COcU2?NiDZY zc`AmN;%QX1664nXn6M=up%g52KDmtSC6`Pv{y}mzI#mgOIT;Hg&9{>GOfkHjT!^pn zO895UH76QwC)cMN?k11PGWp}aI^Xa* z^7vfC<>dN2!}pT2-G-kc#}yjxBsX7R_+xUV&+vET4)Ta8CjX@&bA|FLgEH?gA z$qgF}&mp&6W_S^~9FHlP)OR*{EIILQ<9|H4n!Jdd z@{aM(AvcrPlGnUz{ENx)9~$0FUQNC!;@@lh?%Deho!%5`u4~8d`+wiq$1XFX#ODq=6%gBL|hR-2qry0J0+?j58 z19|QY!&i{wW*M#~uO)9MH_SEuPeuF}8h(}RU23?8+;yhmK62yPhHbb7rt9}S!-tT& z$P>toYmEO4@>+-Ch2(@n!#U(ckKwiCaIs+zIpZS3>&eY{oI~`vne4jC@YUo5JcJ_t zHL{_ziza(QKr}Hxc{i^6M z?oPww$rX1Qo(f6t3#D5!hklVwR)$mE=oYM?1 zB)5@s$O-d}zl&VG(C|g%@k`Xvlin(F8aa=gL-vtN$d{1o z$mL}F1tz_!h(GyWatHY_a@;x-zLi`^?j$#mKZ?XB_mTU_Ka!KYCOz9sqp!K-{mE;| zN0Q6PlgRbtlgRDl`Q$$Ga`N~hbH26YY_gvmCYO@y$=8rO$#;;I&!qQfay+?(Jf8eI zIi0+dyoCG>IZWRFB%_a7vYp&YP9^t|=aR?!O@5Y;v&rX@!{h+Dn*0ay4)Rsx9&$A~ zE@0BXmz+d?gq%r!n!K9)3b}&(HaR0?((5I6kiR3RZ#4d+@ocp8Cws_8k*h8-{^{h} zKN`*^Z^9$6(*JnLU0V!qA@A5~_!e?+h2i_jrB@q%HsVk2BG+GI{N>qg$zM92XO;8) zLbg{K9zEOm$K7D~7;+tXDmmjuB>sbjhs`nN>u)rCIC;rKh9{BJA2B?O zTuWX|E_}@R=a5tJ_^ssMNvQSnDArB2a^-X6Uj;B zQ_0!nY;q;pLGB>?$m&v)pUvcW@-^foW#nL1x0y&4AM81G*Ctn=l zS4{lt$tmPoavHgTJd3=8oI!qtyoCHVIh(wjyqf$Ac`Z3%o+)o3*-j3Sr;;farE{ca*xk}D#7obkVvTut5<;p2_}ljK_R%jA0UTM?dN z!v8bEvkiY2;ZqF9%{Tg}zr=6?xsg1c+)TDAr4GP@`fJ&~63K<+qsamC6mkhUgIr2p zNiHWl$(7^)xtjb(VT)2(JI(lnZ7|IxzZkxa96s0ZgOTtn4L?O*bGG5Xk;ms4evh1Z zj^QuKZJ!zbfn5Hj;r);o$xqK$h9{EOeqnevIsI$Hc^>nZw-0v=O+JH7U}p) zn7>kT75y=d)c$>BOv5zSF@MjIn|Qw0$>mJ%6LJa9ACCt#b^Vj^0~XC|82&`Eo&LBn zul>ic{J$qFh7XYgADQy}fn4)}VKh4(zx6}I*OBAc{_c!~Z#Mo9kyk%u_-S%G`ETT` zXN~{cWbc!Pd&w1e^cmHJ7iF09Cp>T1in)@MKaTDFVDj#N8~^4~}9Cik*D zyUDGbPyawp*=^DrgXaV!ehzseIgWf1dHlaj_yrMv>L-WX#qjy$2J(9H62{*`E~Wp? z1UGnC$D9DK8oD% zzA5iya@Bi==a7@xo=+zy{A~QsiTIPf-^%S zDZ2gML0-f7cHU-3k6+iI*y>wy8ud2}b8FGZT=vHYlat6t zlhbyY_^IS>wvSWDt8J#f&LlUI^T_GS_^&4`)-PT$sn6fZ@VAju>HiS9jQ)59r4FBD zG3k9ku4MZ7ZMF7qvl{;cFjtrI?I9mat|reW*D^i4%2J0fq`#A#!t<{u_cDA1IiBag zgFK7zABiya)kCQ6sF_xS}P9fv1Rl2@U zB)82n;ZG)~-D38`=aIwQf5xjgb^3LgCcJ~ZWQpMm$zvFPGdb}llU^mc?I3ghYVs_X ziGMG-851FCkB^aih;hE%|35FMwH<8aKr;Fef`q#J7`{6a{xZXOwXBZcRA%@Ya+us63BTF+za0sGmeDU>MXTdyoNE}b zrPZ8tj^RXd0~Q)mpOeUCSDExqB3EoRyntLseXk&=Fu!M$cT}77++;8Pi^#5{jQ=I% zrt3`lTgmAhA8#i2G@AChom^FE!aquGxY?wK7Z>UB^;Ma>_)*o5^1K-$KsBKAwbcBD=6}Df}AQ&hvjl zPFQ686}oo0KKGt(IEn1SzKq15MlM`x_zZIU62mTX9OM6iyk@cSzm;5!IRM^=)vPciMyO>Sj>ToPfGR_Qv%cpN#E=bJ*Ve8Tvj zLeAp-`sL*2gH8G_a^as%_!9DJEaXLB*O7CqhHod2Z!_T=$rUddeubPo&V>JntmHlE zNbd`B;nRk#*Z`9HXuHdBB01|`!^e~BrkM0jA(uU9{8y5D@S7A#-$9=Htl=OzJITbq zp1k)E!`sNE%+KTGS&7E~Z{&uFhI`0ekDK(rj>IoCyeGmBnfCTGx%XkiBMvd;UEOH- z5V9g4LoQ=~IgMQ9GwGj3o>gRc8F>f&&m$*1VbaehZ)!5^kMLg%UqUWp_$$aI+l_xE zxxK;gE#&G43~v+09HN%{Yfa>O@-yT{@~h-#@>}EZ;a(2*g1-X;qZzuON{9nk8^naGzK>v1f3e$U^T*&ZWlRFsRc9_ve2L0{iz2pyhVYrx_P-XZEa{TRvYsl%i?nR$>lbdIm_S;Af*bToxPCC}` zJLEl_|9wU7B9BJBO8fA#{U1(tU1ZKTk(|Tlv1XFH`TSM}xs1(?%ZyBr?c-JQro)VX54kJR@Ym$zgA9*2 z+>}3ImnrXo5xo$!o~{tBijUxpleWOUSdx zTgknQe+xN*^Y;hHW#nhbS>!j!IcrUO>L$07KOrY$9U%JJEsXxVgZ=Ff^v}e+Q~XDd zGy16^+sOfPx-jB*G5!Mjw_%}lXJ;Qj2|RtvVXsr93YpG+qk~{6S@3Cqrcn8 zY2z0_lHB$%l}=

t>Nv1w# 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 TKTp;FZu8I2ygPrX-Ddl5_hi;r literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build/native_hello.c b/ui/examples/native-hello/build/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello/build/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello/build/native_hello.o b/ui/examples/native-hello/build/native_hello.o new file mode 100644 index 0000000000000000000000000000000000000000..c4de3f0a4fc76a9116ecbd98f34ea9aaace55907 GIT binary patch literal 6192 zcmb7|YitzP700hx45rYK6bH;BFcey#*v8A#Jhs@ZjqP}`ky#!IEW_G6_HHw~v)vu@ zkZEPK`9Kt@HHb<`Ma^1$#70y~CDb^dVu>nBC8Q7%wW!U9HOYrX0u@v~tSemlKXcEF z$Ac=hBgVh;Klj{oUo&^-^4Y&0{d<8BWdaxdnT>ml31dCs_NUGyQZZExHMz@+G#FfY>#q>w^W@kn779@=eKS&S zhOY{@Y~|6b2?WuH$Cl#fmY%PwK) zLltG>62>s;*z;ZXLNS@ottt)sR+VIV!=xy~EyR_4Ze{8D5-LAgB8~DVHDw}RUqzqQ#*+tseQ#OOH}5*yxja3W$Vu&@9nA5Q2TNp38Dgld zg0Eq6YGB{!bBx0pSFZf?XGZ1d)WEA)oAT+g$u?eGrychAFjmi+Ictfzb!^PJV=s5v zt6cUPm%YwqZ*Ta^zI*I30*0I+ppId=ECtP`?ZReTT zO?^R(&lSVAC>y*>ZDEbC({nSDTh4iwBmOws$4eLH%D4iaVXJSsWoYwFt9)V$^4m6^ zb=xj1r+jagMtsX;p4?K@dBatQ?NwAq+9TkIkK&GFoZLRyoZs?uEY0gM%BVl~So66m z)ICrJ#bICegF1Z~SY zWKVF6oCDiW{gcmJp_AUb!=ulPF!q#k`R)a2PnkVBh4tGu+#TG^BkM`b9 zZ7XtyT?+)#Et(vYI)rwdx*g9%t^w??<)Dt~# zWp^oJO;cQFqIx7Wo8ULtMq1! zq@w9?#I$N9WhkB9KME>|lw!s;C7Mnvdi0!@sTCd3q_#^mr%WxQoHP0}NS@U05B^RV zDV0pMCmv1pX`&^gX(l=Q$WVKzhvH~7Vd5)8={2V7t=V1|-PRiw*>hP_OFBcTla`Xy zC`pgePc3qq=8$@zv7>E&^P#XvMpKDiEo;_lx@gX1jLa^jH=5A3m|_}APezNHnnIr- z#+h2BEvukvxUs9HIgAX!&O?}t-b6}^?Zs@nu5oGOahQ3Hv(zPDD6huC()HiUuX6c$ ze8{~lKM!RE{#(f(Kc-vIZ(pJ)G7kot8IYy!`NuY++AuVPIYT!bzjlnk_j-+^wD4D114fv%Sf z)PZ=PLe-LiwIGfJAw@D!1-=AbDH&J*ejB<%G9bVO(9iJRfq|zW&Jdx;l7S+Ky*>1e zWZ)}s9yFb+z`z|4`+4ZLWZ+LAjyR!l$-o$hS1NQteIFJh9B zCXn8@DzF_~0Imn|dqd}#n#Y2K&>u=h?tpldLboLYH$bYF2kCvfCK)k7%A+&GAhm{3s)BpFjV`ASijGDoV7il?~}%DpU3IFTZlQgf2VZVr0w6OKeHaUW=DvRShE!Leo}ljZn|DZ zZkWCbDa9u!ck3@a`nMka505T*^oJf@=4tN;8sn~5J)pk1H3_$V2T^W4;3*GF?Tn}1 zqpy4P2Qr@%KkO+#?J1{y2mhSwdmjC3j~+xMTuy#^R@}NE%kfuUK~MdY9-Z-M$Qk`v z;?b|k_Bgu`Kk?`vd-Ow3e$`X{o+tliPx;?H^$&XD@f$d!zeAq-8$9KodGsTXe#g_^ z&phSydx3w>)hD&H9v^w)6CN$pta(n?)b4)M#8+-QlSoE0=WMSY?bdX)+lavYBp}15o#aOkrb-^^{i^C(=&2f|G#Lric^F9 z4@cBYq|*H+g9YRnkVb!7Yhc$+{mgtMah4JA(9?{WMPgKeb zKJ*hfMo<+sn(505m}uUhK%uL(1oVSX1WpsEr|O20rZPF5g61=2>M5(YG}Z)6YEIW+ zXlKztI%D)@qRAOOPe)@h98%P%u2V8=6~m^o7!{*ddy;pY_uEaD4~Ws+nYPo&JOipSKEQct~YFtM>cnLb#~chh~C%Yh{NGf zTSv1aJJdsmyY@E*9gj!uYYeuxbafu?XyOI0_3yMT$MDzNF30fiuw9Pf-)_4c!@tdT sIfj3$?Q#tN7TX2GOr|&6((hO>P~f)/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +# ── Step 1: Generate C sources ──────────────────────────────────────────────── +echo "==> Generating C sources with elc..." +mkdir -p "${DOCKER_CTX}/runtime" + +# App +EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" > "${DOCKER_CTX}/native_hello.c" + +# Vessel +"${ELC}" "${EL_NATIVE_VESSEL}" > "${DOCKER_CTX}/el_native_vessel.c" + +# Vessel header (declarations only — stop before first function body) +"${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + > "${DOCKER_CTX}/el_native_vessel.h" + +# Runtime C sources +cp "${EL_RUNTIME}/el_gtk4.c" \ + "${EL_RUNTIME}/el_seed.c" \ + "${EL_RUNTIME}/el_seed.h" \ + "${EL_RUNTIME}/el_runtime.c" \ + "${EL_RUNTIME}/el_runtime.h" \ + "${EL_RUNTIME}/el_native_target.h" \ + "${DOCKER_CTX}/runtime/" + +echo "==> C sources ready in ${DOCKER_CTX}/" + +mkdir -p "${BUILD_DIR}" + +# ── Step 2: Docker build (ARM64) ───────────────────────────────────────────── +if [ "${BUILD_ARM64}" -eq 1 ]; then + echo "==> Building Linux ARM64 image..." + docker build \ + --platform linux/arm64 \ + -f "${SCRIPT_DIR}/Dockerfile.linux-gtk4" \ + --tag el-native-linux-gtk4-arm64 \ + "${DOCKER_CTX}/" + + echo "==> Extracting ARM64 binary..." + docker create --name el-extract-arm64 el-native-linux-gtk4-arm64 + docker cp el-extract-arm64:/build/native-hello-linux "${BUILD_DIR}/native-hello-linux-arm64" + docker rm el-extract-arm64 + + echo "==> ARM64 binary: ${BUILD_DIR}/native-hello-linux-arm64" + file "${BUILD_DIR}/native-hello-linux-arm64" +fi + +# ── Step 3: Docker build (x86_64) ──────────────────────────────────────────── +if [ "${BUILD_X86}" -eq 1 ]; then + echo "==> Building Linux x86_64 image (QEMU emulation — slower)..." + docker build \ + --platform linux/amd64 \ + -f "${SCRIPT_DIR}/Dockerfile.linux-gtk4" \ + --tag el-native-linux-gtk4-x86_64 \ + "${DOCKER_CTX}/" + + echo "==> Extracting x86_64 binary..." + docker create --name el-extract-x86 el-native-linux-gtk4-x86_64 + docker cp el-extract-x86:/build/native-hello-linux "${BUILD_DIR}/native-hello-linux-x86_64" + docker rm el-extract-x86 + + echo "==> x86_64 binary: ${BUILD_DIR}/native-hello-linux-x86_64" + file "${BUILD_DIR}/native-hello-linux-x86_64" +fi + +echo "" +echo "==> Done. Binaries in ${BUILD_DIR}/" +ls -lh "${BUILD_DIR}"/native-hello-linux-* 2>/dev/null || true diff --git a/ui/examples/native-hello/manifest.el b/ui/examples/native-hello/manifest.el new file mode 100644 index 0000000..859274c --- /dev/null +++ b/ui/examples/native-hello/manifest.el @@ -0,0 +1,19 @@ +package "native-hello" { + version "0.1.0" + description "Minimal native UI test for the el native widget system." + authors ["Neuron Technologies"] + edition "2026" +} + +build { + entry "src/main.el" + target "debug" +} + +app { + window_title "Hello from el-native" + window_width 480 + window_height 320 + min_width 360 + min_height 240 +} diff --git a/ui/examples/native-hello/src/App.el b/ui/examples/native-hello/src/App.el new file mode 100644 index 0000000..6b05afb --- /dev/null +++ b/ui/examples/native-hello/src/App.el @@ -0,0 +1,141 @@ +// App.el — native-hello root component. +// +// Demonstrates the el-native widget system: +// - Window created from manifest.el +// - VStack layout +// - Label, Button, TextField +// - Click and change event callbacks +// - Style token application +// +// This is the manual API that the el-ui-compiler's native codegen target will +// eventually emit. For now it shows exactly what apps (and the compiler) need +// to produce. + +import "../../vessels/el-native/src/main.el" + +// ── State ───────────────────────────────────────────────────────────────────── + +// Global state — holds the widget handles so callbacks can reference them. +// In a real component system, these would be scoped to the component instance. + +let g_window: Int = -1 +let g_label: Int = -1 +let g_input: Int = -1 +let g_button: Int = -1 +let g_counter: Int = 0 +let g_counter_lbl: Int = -1 + +// ── Callbacks ───────────────────────────────────────────────────────────────── +// +// Callbacks must be top-level functions with the signature: +// fn name(widget: Int, data: String) -> Void +// The C bridge resolves them by symbol name via dlsym. + +fn on_greet_click(widget: Int, data: String) -> Void { + let name: String = widget_get_text(g_input) + let greeting: String = if str_len(name) > 0 { + "Hello, " + name + "!" + } else { + "Hello, World!" + } + widget_set_text(g_label, greeting) +} + +fn on_counter_click(widget: Int, data: String) -> Void { + let g_counter = g_counter + 1 + widget_set_text(g_counter_lbl, "Clicks: " + int_to_str(g_counter)) +} + +fn on_name_change(widget: Int, data: String) -> Void { + // Enable the greet button only when there is text in the field. + // (data is the current text value) + let has_text: Bool = str_len(data) > 0 + widget_set_disabled(g_button, !has_text) +} + +// ── App build ───────────────────────────────────────────────────────────────── + +fn app_build(window: Int) -> Void { + // Root vertical stack — full-window content. + let root: Int = vstack(0) + widget_set_padding_all(root, 24) + widget_set_bg_color_hex(root, "#0f172a") + widget_set_flex(root, 1) + + // Title label. + let title: Int = label("el-native — native widget demo") + style_label_heading(title) + widget_add_child(root, title) + + // Spacer (empty label acts as a gap). + let gap1: Int = label("") + widget_set_height(gap1, 16) + widget_add_child(root, gap1) + + // Subtitle. + let subtitle: Int = label("AppKit controls from el code, no ObjC in the app layer.") + style_label_muted(subtitle) + widget_add_child(root, subtitle) + + // Divider gap. + let gap2: Int = label("") + widget_set_height(gap2, 24) + widget_add_child(root, gap2) + + // Name input row. + let input_row: Int = hstack(8) + let name_label: Int = label("Name:") + style_label_body(name_label) + widget_set_width(name_label, 60) + + let input: Int = text_field("Enter your name…") + style_label_body(input) + widget_set_flex(input, 1) + widget_on_change(input, "on_name_change") + let g_input = input // stash handle + + let greet_btn: Int = button("Greet") + style_button_primary(greet_btn) + widget_set_disabled(greet_btn, true) // disabled until name is typed + widget_on_click(greet_btn, "on_greet_click") + let g_button = greet_btn + + widget_add_child(input_row, name_label) + widget_add_child(input_row, input) + widget_add_child(input_row, greet_btn) + widget_add_child(root, input_row) + + // Greeting output label. + let gap3: Int = label("") + widget_set_height(gap3, 12) + widget_add_child(root, gap3) + + let greeting: Int = label("Waiting for name…") + style_label_body(greeting) + widget_set_color_hex(greeting, "#60a5fa") + widget_set_font(greeting, "system", 16, true) + let g_label = greeting + widget_add_child(root, greeting) + + // Counter row. + let gap4: Int = label("") + widget_set_height(gap4, 24) + widget_add_child(root, gap4) + + let counter_row: Int = hstack(12) + + let counter_lbl: Int = label("Clicks: 0") + style_label_body(counter_lbl) + let g_counter_lbl = counter_lbl + + let counter_btn: Int = button("Click me") + style_button_primary(counter_btn) + widget_on_click(counter_btn, "on_counter_click") + + widget_add_child(counter_row, counter_lbl) + widget_add_child(counter_row, counter_btn) + widget_add_child(root, counter_row) + + // Attach root to window. + widget_add_child(window, root) +} diff --git a/ui/examples/native-hello/src/main.el b/ui/examples/native-hello/src/main.el new file mode 100644 index 0000000..4e133f9 --- /dev/null +++ b/ui/examples/native-hello/src/main.el @@ -0,0 +1,43 @@ +// main.el — Entry point for native-hello. +// +// Boot sequence: +// 1. Initialize the native widget system. +// 2. Read the manifest.el app{} block for window config. +// 3. Create the window. +// 4. Build the widget tree (App.el: app_build). +// 5. Show the window. +// 6. Start the run loop (never returns). + +import "App.el" + +// ── Boot ────────────────────────────────────────────────────────────────────── + +native_init() + +// Determine the manifest path relative to where we are invoked. +// When running from the example directory: manifest.el is at ./manifest.el. +// When invoked from elsewhere: pass EL_MANIFEST env var. +let manifest_env: String = env("EL_MANIFEST") +let manifest_path: String = if str_len(manifest_env) > 0 { + manifest_env +} else { + "manifest.el" +} + +// Create window from manifest. +let win: Int = window_from_manifest(manifest_path) +let g_window = win + +if win < 0 { + println("Error: failed to create window. Is EL_TARGET_MACOS defined?") + exit_program(1) +} + +// Build the widget tree. +app_build(win) + +// Show the window. +window_show(win) + +// Hand control to AppKit. Never returns. +native_run_loop() diff --git a/ui/examples/profile-card/manifest.el b/ui/examples/profile-card/manifest.el new file mode 100644 index 0000000..00d69ec --- /dev/null +++ b/ui/examples/profile-card/manifest.el @@ -0,0 +1,25 @@ +package "profile-card" { + version "0.1.0" + description "el-ui profile card example — styling, layout, i18n, config, auth" + authors ["Neuron Technologies"] + edition "2026" +} + +build { + entry "src/App.el" + target "debug" +} + +dependencies { + el-style { path "../../vessels/el-style" } + el-layout { path "../../vessels/el-layout" } + el-i18n { path "../../vessels/el-i18n" } + el-config { path "../../vessels/el-config" } + el-secrets { path "../../vessels/el-secrets" } +} + +app { + window_title "Profile Card" + window_width 800 + window_height 600 +} diff --git a/ui/tools/dist/el-ui-native-codegen b/ui/tools/dist/el-ui-native-codegen new file mode 100755 index 0000000..97ced9d --- /dev/null +++ b/ui/tools/dist/el-ui-native-codegen @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# el-ui-native-codegen — el-ui component DSL → el-native API calls +# Auto-generated by build.sh — do not edit. +exec python3 "/Users/will/Development/neuron-technologies/foundation/el/ui/tools/native-codegen/el_ui_native_codegen.py" "$@" diff --git a/ui/tools/native-codegen/build.sh b/ui/tools/native-codegen/build.sh new file mode 100755 index 0000000..47a227c --- /dev/null +++ b/ui/tools/native-codegen/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# build.sh — Package el_ui_native_codegen.py as a self-contained executable. +# +# Usage: +# ./build.sh # creates dist/el-ui-native-codegen +# ./build.sh test # run against test fixtures +# ./build.sh clean # remove build artifacts +# +# The output is a plain shell wrapper that invokes the Python script with +# the correct path. No dependencies beyond Python 3 (guaranteed available). +# +# The dist binary can be called as: +# el-ui-native-codegen App.el # writes to stdout +# el-ui-native-codegen App.el -o generated.el # writes to file +# el-ui-native-codegen App.el --vessel-path ../../vessels/el-native/src/main.el + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIST_DIR="${SCRIPT_DIR}/../dist" +BINARY="${DIST_DIR}/el-ui-native-codegen" +PY_SCRIPT="${SCRIPT_DIR}/el_ui_native_codegen.py" + +build() { + mkdir -p "${DIST_DIR}" + + echo "==> Creating ${BINARY}..." + cat > "${BINARY}" < Done: ${BINARY}" +} + +test_codegen() { + build + echo "" + echo "==> Running test: test_app.el" + echo "────────────────────────────────────────" + "${BINARY}" "${SCRIPT_DIR}/test_app.el" + echo "" + echo "==> Running test: test_native_hello.el" + echo "────────────────────────────────────────" + "${BINARY}" "${SCRIPT_DIR}/test_native_hello.el" + echo "" + echo "==> All tests passed." +} + +clean() { + rm -f "${BINARY}" + echo "Cleaned." +} + +case "${1:-build}" in + build) build ;; + test) test_codegen ;; + clean) clean ;; + *) echo "Usage: $0 [build|test|clean]"; exit 1 ;; +esac diff --git a/ui/tools/native-codegen/el_ui_native_codegen.py b/ui/tools/native-codegen/el_ui_native_codegen.py new file mode 100644 index 0000000..f041f97 --- /dev/null +++ b/ui/tools/native-codegen/el_ui_native_codegen.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 +""" +el_ui_native_codegen.py — el-ui component → el-native codegen pass. + +Reads a .el component file using the el-ui native component DSL and emits +valid el code that calls the el-native vessel API (vstack, label, button, +widget_set_text, etc.). + +Usage: + python3 el_ui_native_codegen.py # writes to stdout + python3 el_ui_native_codegen.py -o out.el + +The output is a standalone .el file that can be compiled with elc and linked +against the el-native vessel. It is semantically equivalent to what a human +would write by hand (as demonstrated in native-hello/src/App.el). + +Input format (native component DSL): + component App { + state { + counter: Int = 0 + input_text: String = "" + } + + fn increment(widget: Int, data: String) -> Void { + state.counter = state.counter + 1 + } + + template { + +

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 '} + case $link in #( + /*) app_path=$link ;; #( + # Make relative path absolute, based on the path of the symlink: + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@" + +exec "$JAVACMD" "$@" diff --git a/ui/examples/native-hello-android/local.properties.template b/ui/examples/native-hello-android/local.properties.template new file mode 100644 index 0000000..b9f1e6d --- /dev/null +++ b/ui/examples/native-hello-android/local.properties.template @@ -0,0 +1,7 @@ +# local.properties — machine-local SDK/NDK paths. DO NOT commit. Copy to local.properties. +# +# Generate: Android Studio creates this automatically, or set manually: +# sdk.dir=/Users//Library/Android/sdk +# ndk.dir=/Users//Library/Android/sdk/ndk/ + +sdk.dir=/Users/will/Library/Android/sdk diff --git a/ui/examples/native-hello-android/settings.gradle b/ui/examples/native-hello-android/settings.gradle new file mode 100644 index 0000000..d9f3ff7 --- /dev/null +++ b/ui/examples/native-hello-android/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "native-hello-android" +include ':app' diff --git a/ui/examples/native-hello-ios/.gitignore b/ui/examples/native-hello-ios/.gitignore new file mode 100644 index 0000000..22a50dd --- /dev/null +++ b/ui/examples/native-hello-ios/.gitignore @@ -0,0 +1,4 @@ +build/ +*.xcuserstate +xcuserdata/ +NativeHello.xcodeproj/xcuserdata/ diff --git a/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj b/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d089ff0 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj @@ -0,0 +1,377 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 4C6D9832BE495DD69A8573D9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FFDE02477855A55BC5F02CE /* main.m */; }; + 396FD073065B58F39358D798 /* el_uikit.m in Sources */ = {isa = PBXBuildFile; fileRef = 606F96EC1EDC54CD99E7D722 /* el_uikit.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 92BBC3E2AA055FCE840BF779 /* el_seed.c in Sources */ = {isa = PBXBuildFile; fileRef = 618FBE18EAA850D9B493A20F /* el_seed.c */; }; + 136C7763096C5037A69077AD /* el_runtime.c in Sources */ = {isa = PBXBuildFile; fileRef = A8CCF8316D83597CBB332064 /* el_runtime.c */; }; + 25136DF2A63F507EA5C7EF99 /* el_native_vessel.c in Sources */ = {isa = PBXBuildFile; fileRef = 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */; }; + C05D581DBD4E52F6B7F7303E /* native_hello.c in Sources */ = {isa = PBXBuildFile; fileRef = EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */; }; + C3D9A9987F0050EB859D9218 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF42D2E0FA0655149E573F7C /* UIKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8FFDE02477855A55BC5F02CE /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 606F96EC1EDC54CD99E7D722 /* el_uikit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = el_uikit.m; sourceTree = ""; }; + 618FBE18EAA850D9B493A20F /* el_seed.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_seed.c; sourceTree = ""; }; + A8CCF8316D83597CBB332064 /* el_runtime.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_runtime.c; sourceTree = ""; }; + 530AA788858357FBA923217E /* el_runtime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = el_runtime.h; sourceTree = ""; }; + E1A7B02609E95D1686523B80 /* el_native_target.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = el_native_target.h; sourceTree = ""; }; + 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_native_vessel.c; sourceTree = ""; }; + EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = native_hello.c; sourceTree = ""; }; + 08B6475D8C7952F48EF46B10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FF42D2E0FA0655149E573F7C /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + BDC23A5F3FCD5D1098144064 /* NativeHello.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeHello.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5673C72A848C5FFF94E7CF8F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C3D9A9987F0050EB859D9218 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05039DD2316A5B41A35E094E /* root */ = { + isa = PBXGroup; + children = ( + EBC5644CBC315AFDA6A6A59E /* NativeHello */, + D1E4BC2FC3925E6FAFCD38F6 /* Frameworks */, + 177F2C27230B527E80CEAED5 /* Products */, + ); + sourceTree = ""; + }; + 177F2C27230B527E80CEAED5 /* Products */ = { + isa = PBXGroup; + children = ( + BDC23A5F3FCD5D1098144064 /* NativeHello.app */, + ); + name = Products; + sourceTree = ""; + }; + EBC5644CBC315AFDA6A6A59E /* NativeHello */ = { + isa = PBXGroup; + children = ( + 8FFDE02477855A55BC5F02CE /* main.m */, + 606F96EC1EDC54CD99E7D722 /* el_uikit.m */, + 618FBE18EAA850D9B493A20F /* el_seed.c */, + A8CCF8316D83597CBB332064 /* el_runtime.c */, + 530AA788858357FBA923217E /* el_runtime.h */, + E1A7B02609E95D1686523B80 /* el_native_target.h */, + 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */, + EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */, + 08B6475D8C7952F48EF46B10 /* Info.plist */, + ); + path = NativeHello; + sourceTree = ""; + }; + D1E4BC2FC3925E6FAFCD38F6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + FF42D2E0FA0655149E573F7C /* UIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F1275C1CDFD05A40B9D6C717 /* NativeHello */ = { + isa = PBXNativeTarget; + buildConfigurationList = 07AFC970D6B05512B39EDE6A /* Build configuration list for PBXNativeTarget "NativeHello" */; + buildPhases = ( + DF29C8DEE5CB5F1C83967F1F /* Sources */, + 5673C72A848C5FFF94E7CF8F /* Frameworks */, + D6D57F9604C45AAEA747645E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NativeHello; + productName = NativeHello; + productReference = BDC23A5F3FCD5D1098144064 /* NativeHello.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D8A823993D3254C7B6541F91 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1500; + TargetAttributes = { + F1275C1CDFD05A40B9D6C717 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 3DAB235BBB945E0CBB3EF554 /* Build configuration list for PBXProject "NativeHello" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05039DD2316A5B41A35E094E; + productRefGroup = 177F2C27230B527E80CEAED5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F1275C1CDFD05A40B9D6C717 /* NativeHello */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D6D57F9604C45AAEA747645E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DF29C8DEE5CB5F1C83967F1F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C6D9832BE495DD69A8573D9 /* main.m in Sources */, + 396FD073065B58F39358D798 /* el_uikit.m in Sources */, + 92BBC3E2AA055FCE840BF779 /* el_seed.c in Sources */, + 136C7763096C5037A69077AD /* el_runtime.c in Sources */, + 25136DF2A63F507EA5C7EF99 /* el_native_vessel.c in Sources */, + C05D581DBD4E52F6B7F7303E /* native_hello.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 071FC6E313815E66BB76FC93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_EXCEPTION = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + }; + name = Debug; + }; + 64E467FC48E757C0BDE1DFF6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_EXCEPTION = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5DAFA921569F54D081B65492 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "EL_TARGET_IOS=1", + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/NativeHello", + "$(inherited)", + ); + INFOPLIST_FILE = NativeHello/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CFLAGS = ( + "-DEL_TARGET_IOS", + "$(inherited)", + ); + OTHER_LDFLAGS = ( + "-ldl", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = ai.neurontechnologies.el.nativehello; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 81898B3044815511B8C31D64 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "EL_TARGET_IOS=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/NativeHello", + "$(inherited)", + ); + INFOPLIST_FILE = NativeHello/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CFLAGS = ( + "-DEL_TARGET_IOS", + "$(inherited)", + ); + OTHER_LDFLAGS = ( + "-ldl", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = ai.neurontechnologies.el.nativehello; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3DAB235BBB945E0CBB3EF554 /* Build configuration list for PBXProject "NativeHello" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 071FC6E313815E66BB76FC93 /* Debug */, + 64E467FC48E757C0BDE1DFF6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 07AFC970D6B05512B39EDE6A /* Build configuration list for PBXNativeTarget "NativeHello" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5DAFA921569F54D081B65492 /* Debug */, + 81898B3044815511B8C31D64 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = D8A823993D3254C7B6541F91 /* Project object */; +} diff --git a/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme b/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme new file mode 100644 index 0000000..6eefb67 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/examples/native-hello-ios/NativeHello/Info.plist b/ui/examples/native-hello-ios/NativeHello/Info.plist new file mode 100644 index 0000000..8ab6a41 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ui/examples/native-hello-ios/NativeHello/el_native_target.h b/ui/examples/native-hello-ios/NativeHello/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c b/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c new file mode 100644 index 0000000..0682965 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int el_vessel_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello-ios/NativeHello/el_runtime.c b/ui/examples/native-hello-ios/NativeHello/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _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(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +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) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + 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) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + 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("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * 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 { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* 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; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + 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'; + curl_easy_setopt(c, CURLOPT_URL, url); + 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"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + 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)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + 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); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + 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); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + 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"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + 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"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + 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; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + 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) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + 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); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + 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) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); 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); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +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_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + 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) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* 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) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +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) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + 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 default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + 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)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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 + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +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. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* 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) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + 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); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(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 + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * 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"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + 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). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + 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_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + 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/ui/examples/native-hello-ios/NativeHello/el_runtime.h b/ui/examples/native-hello-ios/NativeHello/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * 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_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#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 + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +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); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +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, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * 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. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"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 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +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); +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 ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +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 + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +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 ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +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/ui/examples/native-hello-ios/NativeHello/el_seed.c b/ui/examples/native-hello-ios/NativeHello/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +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); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +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_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(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); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +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_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +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); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +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_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +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); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +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); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello-ios/NativeHello/el_uikit.m b/ui/examples/native-hello-ios/NativeHello/el_uikit.m new file mode 100644 index 0000000..01a97a1 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_uikit.m @@ -0,0 +1,1118 @@ +/* + * el_uikit.m — UIKit backend for the el native widget system. + * + * This file implements the iOS UIKit widget layer that el_seed.c calls + * through to when EL_TARGET_IOS is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_uikit_* C-callable functions declared here + * → UIView/UIWindow/UIStackView/UIButton/etc. via ObjC + * + * Widget handles: every widget (window, view, control) is assigned an int64_t + * slot index into _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved as null handle = -1 convention). + * + * Threading: All UIKit calls MUST run on the main thread. el_uikit_sync_main + * bounces work to the main thread synchronously if called from a background thread. + * + * Callback mechanism: when a widget fires an event (button tap, text change), + * the bridge looks up the registered callback function name, then calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This resolves the El function symbol at runtime, matching the __thread_create + * pattern already established in el_seed.c. + * + * iOS lifecycle: UIApplicationMain never returns. The el program's "main" body + * (window creation, layout, show) must execute inside applicationDidFinishLaunchingWithOptions. + * The caller sets el_main_entry_fn before calling __native_run_loop so the + * delegate can invoke it at the right time. + * + * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): + * clang -std=gnu11 -ObjC -fno-objc-arc -DEL_TARGET_IOS -framework UIKit \ + * -c el_uikit.m -o el_uikit.o + * Then link el_uikit.o alongside el_seed.c. + * + * Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC + * forbids explicit retain/release on struct-embedded object pointers and cannot + * insert automatic retain/release through C struct boundaries. MRC is the + * correct choice for this kind of C/ObjC bridge. + * + * Link flags required: + * -framework UIKit + */ + +#ifdef EL_TARGET_IOS + +#import +#import +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_UIKIT_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + id obj; /* UIWindow* or UIView* subclass, retained */ + char* cb_click; /* El function name for click/submit events */ + char* cb_change; /* El function name for value-change events */ +} ElWidget; + +static ElWidget _el_widgets[EL_UIKIT_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Side table for ObjC delegate objects keyed by widget slot (MRC retained). */ +static id _el_delegates[EL_UIKIT_MAX_WIDGETS]; + +/* Allocate a slot and return its index. Returns -1 if full. */ +static int64_t el_widget_alloc(ElWidgetKind kind, id obj) { + for (int i = 1; i < EL_UIKIT_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].obj = [obj retain]; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_UIKIT_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + [w->obj release]; + w->obj = nil; + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── Dispatch helpers ─────────────────────────────────────────────────────── */ + +/* Run a block on the main thread, waiting for completion. Safe to call from + * any thread including the main thread. Uses dispatch_sync so callers can + * rely on the block having run before the function returns. */ +static void el_uikit_sync_main(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. The El function must have the + * signature: fn handler(handle: Int, data: String) -> Void + * which compiles to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_uikit_invoke_cb(const char* fn_name, int64_t handle, const char* data) { + if (!fn_name || !*fn_name) return; + void* sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── ObjC helper: ElEventProxy — button tap and text event target ─────────── */ + +/* + * ElEventProxy is a lightweight ObjC action target / delegate aggregator. + * One instance is allocated per widget that has events. Stored in _el_delegates[]. + */ +@interface ElEventProxy : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +/* UIButton tap */ +- (void)elButtonTapped:(id)sender; +/* UITextField UIControlEventEditingChanged */ +- (void)elTextChanged:(id)sender; +@end + +@implementation ElEventProxy + +- (void)elButtonTapped:(id)sender { + (void)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, ""); + } +} + +- (void)elTextChanged:(id)sender { + UITextField* tf = (UITextField*)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + const char* val = [tf.text UTF8String]; + if (w->cb_change) { + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); + } +} + +/* UITextFieldDelegate — return key fires on_submit (stored in cb_click). */ +- (BOOL)textFieldShouldReturn:(UITextField*)textField { + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + const char* val = [textField.text UTF8String]; + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); + } + [textField resignFirstResponder]; + return YES; +} + +@end + +/* ── UITextView change delegate ──────────────────────────────────────────── */ + +@interface ElTextViewDelegate : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +@end + +@implementation ElTextViewDelegate + +- (void)textViewDidChange:(UITextView*)textView { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w || !w->cb_change) return; + const char* val = [textView.text UTF8String]; + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); +} + +@end + +/* ── iOS application lifecycle ───────────────────────────────────────────── */ + +/* + * el_main_entry_fn — set by el's main() (via __native_run_loop) before calling + * UIApplicationMain. The app delegate invokes it in didFinishLaunching so that + * the el program's window/widget setup runs at the correct lifecycle point. + */ +void (*el_main_entry_fn)(void) = NULL; + +/* argc/argv forwarded from el_runtime_init_args (or main). */ +static int _el_argc = 0; +static char** _el_argv = NULL; + +/* + * el_uikit_set_args — call from el_runtime_init_args / main() before + * __native_run_loop to forward argc/argv to UIApplicationMain. + */ +void el_uikit_set_args(int argc, char** argv) { + _el_argc = argc; + _el_argv = argv; +} + +@interface ElAppDelegate : UIResponder +@property (nonatomic, strong) UIWindow *keyWindow; +@end + +@implementation ElAppDelegate + +- (BOOL)application:(UIApplication*)app + didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + (void)app; (void)launchOptions; + /* + * The el program called __native_init and set up el_main_entry_fn before + * UIApplicationMain was entered. We call it here so window creation and + * widget layout happen on the main thread at the right lifecycle moment. + */ + if (el_main_entry_fn) { + el_main_entry_fn(); + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication*)app { (void)app; } +- (void)applicationDidEnterBackground:(UIApplication*)app { (void)app; } +- (void)applicationWillEnterForeground:(UIApplication*)app { (void)app; } +- (void)applicationDidBecomeActive:(UIApplication*)app { (void)app; } + +@end + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_uikit_init — one-time initialisation. Idempotent. Call before any other + * el_uikit_* function. On iOS there is no UIApplication equivalent of + * [NSApplication sharedApplication] to call here — the real initialisation + * happens inside UIApplicationMain which is entered from el_uikit_run_loop. + * This function records that init was requested. + */ +static int _el_uikit_init_done = 0; + +void el_uikit_init(void) { + _el_uikit_init_done = 1; +} + +/* + * el_uikit_run_loop — enters UIApplicationMain. Never returns. + * Must be called from the process main thread as the final step of main(). + * Sets el_main_entry_fn to the provided function pointer before entering + * UIApplicationMain so the app delegate can call back into el code. + */ +void el_uikit_run_loop(void (*entry_fn)(void)) { + el_main_entry_fn = entry_fn; + UIApplicationMain(_el_argc, _el_argv, nil, @"ElAppDelegate"); +} + +/* + * el_uikit_window_create — create a UIWindow and configure a root + * UINavigationController + UIViewController. Returns window handle. + * + * On iOS there is exactly one full-screen window. width/height from the + * manifest are stored but the window always fills the screen. The title + * is applied to the root view controller's navigationItem. + */ +int64_t el_uikit_window_create(const char* title, int width, int height, + int min_width, int min_height) { + (void)width; (void)height; (void)min_width; (void)min_height; + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScreen* screen = [UIScreen mainScreen]; + UIWindow* win = [[UIWindow alloc] initWithFrame:[screen bounds]]; + win.backgroundColor = [UIColor systemBackgroundColor]; + + /* Root view controller with a vertical UIStackView as content. */ + UIViewController* rootVC = [[UIViewController alloc] init]; + NSString* titleStr = [NSString stringWithUTF8String:title ? title : ""]; + rootVC.title = titleStr; + + UIStackView* rootStack = [[UIStackView alloc] initWithFrame:CGRectZero]; + rootStack.axis = UILayoutConstraintAxisVertical; + rootStack.alignment = UIStackViewAlignmentFill; + rootStack.distribution = UIStackViewDistributionFill; + rootStack.spacing = 0.0; + rootStack.translatesAutoresizingMaskIntoConstraints = NO; + [rootVC.view addSubview:rootStack]; + + /* Pin root stack to safe area. */ + UILayoutGuide* safe = rootVC.view.safeAreaLayoutGuide; + [NSLayoutConstraint activateConstraints:@[ + [rootStack.topAnchor constraintEqualToAnchor:safe.topAnchor], + [rootStack.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor], + [rootStack.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor], + [rootStack.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor], + ]]; + + UINavigationController* navCtrl = + [[UINavigationController alloc] initWithRootViewController:rootVC]; + win.rootViewController = navCtrl; + + /* Store the window. The root stack is accessible via + * ((UINavigationController*)win.rootViewController) + * .topViewController.view.subviews[0] + * but we reach it by casting obj to UIWindow* and digging at add_child time. */ + handle = el_widget_alloc(EL_WIDGET_WINDOW, win); + [win release]; + [rootVC release]; + [rootStack release]; + [navCtrl release]; + }); + return handle; +} + +/* + * el_uikit_window_show — make the window key and visible. + */ +void el_uikit_window_show(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + [win makeKeyAndVisible]; + }); +} + +/* + * el_uikit_window_set_title — update nav bar title at runtime. + */ +void el_uikit_window_set_title(int64_t handle, const char* title) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + UINavigationController* nav = (UINavigationController*)win.rootViewController; + if ([nav isKindOfClass:[UINavigationController class]]) { + nav.topViewController.title = + [NSString stringWithUTF8String:title ? title : ""]; + } + }); +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t make_uistack(UILayoutConstraintAxis axis, CGFloat spacing) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIStackView* sv = [[UIStackView alloc] initWithFrame:CGRectZero]; + sv.axis = axis; + sv.alignment = UIStackViewAlignmentFill; + sv.distribution = UIStackViewDistributionFill; + sv.spacing = spacing; + sv.translatesAutoresizingMaskIntoConstraints = NO; + ElWidgetKind kind = (axis == UILayoutConstraintAxisVertical) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + handle = el_widget_alloc(kind, sv); + [sv release]; + }); + return handle; +} + +int64_t el_uikit_vstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisVertical, (CGFloat)spacing); +} + +int64_t el_uikit_hstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisHorizontal, (CGFloat)spacing); +} + +int64_t el_uikit_zstack_create(void) { + /* ZStack = plain UIView; children are added with addSubview and use + * Auto Layout or absolute frames set by the caller. */ + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIView* v = [[UIView alloc] initWithFrame:CGRectZero]; + v.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); + [v release]; + }); + return handle; +} + +int64_t el_uikit_scroll_create(void) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScrollView* sv = [[UIScrollView alloc] initWithFrame:CGRectZero]; + sv.translatesAutoresizingMaskIntoConstraints = NO; + sv.alwaysBounceVertical = YES; + sv.showsHorizontalScrollIndicator = NO; + handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); + [sv release]; + }); + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_uikit_label_create(const char* text) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UILabel* lbl = [[UILabel alloc] initWithFrame:CGRectZero]; + lbl.text = [NSString stringWithUTF8String:text ? text : ""]; + lbl.numberOfLines = 0; + lbl.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); + [lbl release]; + }); + return handle; +} + +int64_t el_uikit_button_create(const char* label) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIButton* btn = [UIButton buttonWithType:UIButtonTypeSystem]; + [btn setTitle:[NSString stringWithUTF8String:label ? label : ""] + forState:UIControlStateNormal]; + btn.translatesAutoresizingMaskIntoConstraints = NO; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + [btn addTarget:proxy + action:@selector(elButtonTapped:) + forControlEvents:UIControlEventTouchUpInside]; + + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; /* retain in side table */ + handle = h; + /* btn is autoreleased from buttonWithType: — no release needed */ + }); + return handle; +} + +int64_t el_uikit_text_field_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextField* tf = [[UITextField alloc] initWithFrame:CGRectZero]; + tf.translatesAutoresizingMaskIntoConstraints = NO; + if (placeholder && *placeholder) { + tf.placeholder = [NSString stringWithUTF8String:placeholder]; + } + tf.borderStyle = UITextBorderStyleRoundedRect; + tf.autocorrectionType = UITextAutocorrectionTypeNo; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + tf.delegate = proxy; + [tf addTarget:proxy + action:@selector(elTextChanged:) + forControlEvents:UIControlEventEditingChanged]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; + handle = h; + [tf release]; + }); + return handle; +} + +int64_t el_uikit_text_area_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextView* tv = [[UITextView alloc] initWithFrame:CGRectZero]; + tv.translatesAutoresizingMaskIntoConstraints = NO; + tv.font = [UIFont systemFontOfSize:14.0]; + tv.layer.borderColor = [[UIColor systemGray4Color] CGColor]; + tv.layer.borderWidth = 1.0; + tv.layer.cornerRadius = 6.0; + tv.clipsToBounds = YES; + /* UITextView has no native placeholder; set accessibility hint. */ + if (placeholder && *placeholder) { + tv.accessibilityHint = [NSString stringWithUTF8String:placeholder]; + } + + ElTextViewDelegate* del = [[ElTextViewDelegate alloc] init]; + tv.delegate = del; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, tv); + del.widgetHandle = h; + _el_delegates[h] = del; + handle = h; + [tv release]; + }); + return handle; +} + +int64_t el_uikit_image_create(const char* path_or_name) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIImage* img = nil; + if (path_or_name && *path_or_name) { + /* Try filesystem path first, then bundle asset name. */ + img = [UIImage imageWithContentsOfFile: + [NSString stringWithUTF8String:path_or_name]]; + if (!img) { + img = [UIImage imageNamed: + [NSString stringWithUTF8String:path_or_name]]; + } + } + UIImageView* iv = [[UIImageView alloc] initWithFrame:CGRectZero]; + iv.translatesAutoresizingMaskIntoConstraints = NO; + iv.contentMode = UIViewContentModeScaleAspectFit; + if (img) { + iv.image = img; + } + handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); + [iv release]; + }); + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_uikit_widget_set_text(int64_t handle, const char* text) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = [NSString stringWithUTF8String:text ? text : ""]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).text = s; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).text = s; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitle:s forState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).text = s; + break; + case EL_WIDGET_WINDOW: + el_uikit_window_set_title(handle, text); + break; + default: break; + } + }); +} + +const char* el_uikit_widget_get_text(int64_t handle) { + __block const char* result = ""; + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = nil; + switch (w->kind) { + case EL_WIDGET_LABEL: + s = ((UILabel*)w->obj).text; + break; + case EL_WIDGET_TEXTFIELD: + s = ((UITextField*)w->obj).text; + break; + case EL_WIDGET_BUTTON: + s = [(UIButton*)w->obj titleForState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + s = ((UITextView*)w->obj).text; + break; + default: break; + } + if (s) result = strdup([s UTF8String]); + }); + return result; +} + +void el_uikit_widget_set_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIColor* c = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).textColor = c; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitleColor:c forState:UIControlStateNormal]; + ((UIButton*)w->obj).tintColor = c; + break; + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: + ((UIView*)w->obj).backgroundColor = c; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind == EL_WIDGET_WINDOW) { + UIWindow* win = (UIWindow*)w->obj; + win.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + return; + } + UIView* v = (UIView*)w->obj; + v.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + }); +} + +void el_uikit_widget_set_font(int64_t handle, const char* family, int size, int bold) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIFont* font; + if (bold) { + font = [UIFont boldSystemFontOfSize:(CGFloat)size]; + } else if (family && *family && strcmp(family, "system") != 0) { + font = [UIFont fontWithName:[NSString stringWithUTF8String:family] + size:(CGFloat)size]; + if (!font) font = [UIFont systemFontOfSize:(CGFloat)size]; + } else { + font = [UIFont systemFontOfSize:(CGFloat)size]; + } + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).font = font; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).font = font; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).font = font; + break; + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).titleLabel.font = font; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIEdgeInsets insets = UIEdgeInsetsMake( + (CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right); + if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { + UIStackView* sv = (UIStackView*)w->obj; + sv.layoutMargins = insets; + sv.layoutMarginsRelativeArrangement = YES; + } else if (w->kind == EL_WIDGET_TEXTAREA) { + UITextView* tv = (UITextView*)w->obj; + tv.textContainerInset = insets; + } else { + UIView* v = (UIView*)w->obj; + v.layoutMargins = insets; + } + }); +} + +void el_uikit_widget_set_width(int64_t handle, int width) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.widthAnchor + constraintEqualToConstant:(CGFloat)width]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_height(int64_t handle, int height) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.heightAnchor + constraintEqualToConstant:(CGFloat)height]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_flex(int64_t handle, int flex) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + /* flex > 0 → low hugging priority so the view expands to fill space. + * flex == 0 → high hugging priority so the view stays at intrinsic size. */ + UILayoutPriority p = (flex > 0) + ? UILayoutPriorityDefaultLow + : UILayoutPriorityDefaultHigh; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisHorizontal]; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisVertical]; + }); +} + +void el_uikit_widget_set_corner_radius(int64_t handle, int radius) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + v.layer.cornerRadius = (CGFloat)radius; + v.clipsToBounds = YES; + }); +} + +void el_uikit_widget_set_disabled(int64_t handle, int disabled) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + switch (w->kind) { + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).editable = !disabled; + break; + default: + ((UIView*)w->obj).userInteractionEnabled = !disabled; + break; + } + }); +} + +void el_uikit_widget_set_hidden(int64_t handle, int hidden) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + ((UIView*)w->obj).hidden = (BOOL)hidden; + }); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_uikit_widget_add_child — attach child widget to parent. + * + * - Window: child is added to the root UIStackView inside the root VC. + * - VStack/HStack: child is added as an arranged subview. + * - ZStack: child is added as a plain subview (caller manages layout). + * - ScrollView: child is added as a subview and pinned to content layout guide. + * - Generic view: child is added as a plain subview. + */ +void el_uikit_widget_add_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* can't add window as subview */ + + UIView* childView = (UIView*)cw->obj; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Dig to the root stack view: window → navCtrl → topVC → view → subviews[0] */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + UIView* rootVCView = nav.topViewController.view; + UIStackView* stack = nil; + for (UIView* sub in rootVCView.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + stack = (UIStackView*)sub; + break; + } + } + if (stack) { + [stack addArrangedSubview:childView]; + } else { + [rootVCView addSubview:childView]; + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: { + UIStackView* sv = (UIStackView*)pw->obj; + [sv addArrangedSubview:childView]; + break; + } + case EL_WIDGET_SCROLL: { + UIScrollView* sv = (UIScrollView*)pw->obj; + [sv addSubview:childView]; + /* Pin child to content layout guide so scroll view knows content size. */ + UILayoutGuide* cg = sv.contentLayoutGuide; + childView.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [childView.topAnchor constraintEqualToAnchor:cg.topAnchor], + [childView.leadingAnchor constraintEqualToAnchor:cg.leadingAnchor], + [childView.trailingAnchor constraintEqualToAnchor:cg.trailingAnchor], + [childView.bottomAnchor constraintEqualToAnchor:cg.bottomAnchor], + /* Width matches scroll view's frame (vertical scroll only). */ + [childView.widthAnchor constraintEqualToAnchor:sv.frameLayoutGuide.widthAnchor], + ]]; + break; + } + case EL_WIDGET_ZSTACK: + default: { + UIView* pv = (UIView*)pw->obj; + [pv addSubview:childView]; + break; + } + } + }); +} + +/* + * el_uikit_widget_remove_child — detach a child widget from its parent. + */ +void el_uikit_widget_remove_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!cw || cw->kind == EL_WIDGET_WINDOW) return; + UIView* childView = (UIView*)cw->obj; + /* If parent is a stack view, remove from arranged subviews too. */ + if (pw && (pw->kind == EL_WIDGET_VSTACK || pw->kind == EL_WIDGET_HSTACK)) { + UIStackView* sv = (UIStackView*)pw->obj; + [sv removeArrangedSubview:childView]; + } else if (pw && pw->kind == EL_WIDGET_WINDOW) { + /* Find and remove from root stack. */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + for (UIView* sub in nav.topViewController.view.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + [(UIStackView*)sub removeArrangedSubview:childView]; + break; + } + } + } + [childView removeFromSuperview]; + }); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_uikit_widget_on_click(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_uikit_widget_on_change(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_uikit_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit = return key → stored in cb_click slot. */ + el_uikit_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_uikit_widget_destroy(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind != EL_WIDGET_WINDOW) { + UIView* v = (UIView*)w->obj; + /* If it's in a stack view, also remove from arranged subviews. */ + if ([v.superview isKindOfClass:[UIStackView class]]) { + [(UIStackView*)v.superview removeArrangedSubview:v]; + } + [v removeFromSuperview]; + } + [_el_delegates[handle] release]; + _el_delegates[handle] = nil; + el_widget_free(handle); + }); +} + +/* ── Manifest reader ──────────────────────────────────────────────────────── */ + +/* + * el_uikit_manifest_read — read an app manifest file. Tries the given path + * first; if not found, looks in the main bundle. Returns a C string with the + * file contents (heap-allocated; caller must not free — matches AppKit bridge + * convention of returning static/permanent lifetime strings from manifest). + * Returns NULL on failure. + */ +const char* el_uikit_manifest_read(const char* path) { + if (!path || !*path) return NULL; + + NSString* pathStr = [NSString stringWithUTF8String:path]; + NSString* content = [NSString stringWithContentsOfFile:pathStr + encoding:NSUTF8StringEncoding + error:nil]; + if (!content) { + /* Try bundle resource lookup — strip to last component without extension. */ + NSString* baseName = [[pathStr lastPathComponent] + stringByDeletingPathExtension]; + NSString* ext = [pathStr pathExtension]; + NSString* bundlePath = [[NSBundle mainBundle] + pathForResource:baseName ofType:(ext.length ? ext : nil)]; + if (bundlePath) { + content = [NSString stringWithContentsOfFile:bundlePath + encoding:NSUTF8StringEncoding + error:nil]; + } + } + if (!content) return NULL; + return strdup([content UTF8String]); +} + +/* ── __widget_* C API (called from el_seed.c) ─────────────────────────────── */ +/* + * These are the symbols that el compiled programs actually call. They forward + * directly to the el_uikit_* implementations above. el_val_t is int64_t (the + * same type used by the AppKit bridge). + */ + +void __native_init(void) { + el_uikit_init(); +} + +void __native_run_loop(void) { + /* el_main_entry_fn must be set by the caller before this. On iOS the + * entry function pointer pattern is inverted vs macOS: the el program + * packages up its "build UI" work into a lambda / function and registers + * it via el_main_entry_fn, then calls __native_run_loop. + * el_uikit_run_loop takes that fn pointer and passes it to UIApplicationMain. */ + el_uikit_run_loop(el_main_entry_fn); +} + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_uikit_window_create( + (const char*)(uintptr_t)title, + (int)width, (int)height, + (int)min_width, (int)min_height); +} + +void __window_show(el_val_t handle) { + el_uikit_window_show((int64_t)handle); +} + +void __window_set_title(el_val_t handle, el_val_t title) { + el_uikit_window_set_title((int64_t)handle, + (const char*)(uintptr_t)title); +} + +el_val_t __vstack_create(el_val_t spacing) { + return (el_val_t)el_uikit_vstack_create((int)spacing); +} + +el_val_t __hstack_create(el_val_t spacing) { + return (el_val_t)el_uikit_hstack_create((int)spacing); +} + +el_val_t __zstack_create(void) { + return (el_val_t)el_uikit_zstack_create(); +} + +el_val_t __scroll_create(void) { + return (el_val_t)el_uikit_scroll_create(); +} + +el_val_t __label_create(el_val_t text) { + return (el_val_t)el_uikit_label_create((const char*)(uintptr_t)text); +} + +el_val_t __button_create(el_val_t label) { + return (el_val_t)el_uikit_button_create((const char*)(uintptr_t)label); +} + +el_val_t __text_field_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_field_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __text_area_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_area_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __image_create(el_val_t path_or_name) { + return (el_val_t)el_uikit_image_create( + (const char*)(uintptr_t)path_or_name); +} + +void __widget_set_text(el_val_t handle, el_val_t text) { + el_uikit_widget_set_text((int64_t)handle, (const char*)(uintptr_t)text); +} + +el_val_t __widget_get_text(el_val_t handle) { + return (el_val_t)(uintptr_t)el_uikit_widget_get_text((int64_t)handle); +} + +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_uikit_widget_set_color((int64_t)handle, + (float)r, (float)g, (float)b, (float)a); +} + +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_uikit_widget_set_bg_color((int64_t)handle, + (float)r, (float)g, (float)b, (float)a); +} + +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold) { + el_uikit_widget_set_font((int64_t)handle, + (const char*)(uintptr_t)family, (int)size, (int)bold); +} + +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_uikit_widget_set_padding((int64_t)handle, + (int)top, (int)right, (int)bottom, (int)left); +} + +void __widget_set_width(el_val_t handle, el_val_t width) { + el_uikit_widget_set_width((int64_t)handle, (int)width); +} + +void __widget_set_height(el_val_t handle, el_val_t height) { + el_uikit_widget_set_height((int64_t)handle, (int)height); +} + +void __widget_set_flex(el_val_t handle, el_val_t flex) { + el_uikit_widget_set_flex((int64_t)handle, (int)flex); +} + +void __widget_set_corner_radius(el_val_t handle, el_val_t radius) { + el_uikit_widget_set_corner_radius((int64_t)handle, (int)radius); +} + +void __widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_uikit_widget_set_disabled((int64_t)handle, (int)disabled); +} + +void __widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_uikit_widget_set_hidden((int64_t)handle, (int)hidden); +} + +void __widget_add_child(el_val_t parent, el_val_t child) { + el_uikit_widget_add_child((int64_t)parent, (int64_t)child); +} + +void __widget_remove_child(el_val_t parent, el_val_t child) { + el_uikit_widget_remove_child((int64_t)parent, (int64_t)child); +} + +void __widget_destroy(el_val_t handle) { + el_uikit_widget_destroy((int64_t)handle); +} + +void __widget_on_click(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_click((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +void __widget_on_change(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_change((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +void __widget_on_submit(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_submit((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +el_val_t __manifest_read(el_val_t path) { + return (el_val_t)(uintptr_t)el_uikit_manifest_read( + (const char*)(uintptr_t)path); +} + +#endif /* EL_TARGET_IOS */ diff --git a/ui/examples/native-hello-ios/NativeHello/main.m b/ui/examples/native-hello-ios/NativeHello/main.m new file mode 100644 index 0000000..4ca6b86 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/main.m @@ -0,0 +1,48 @@ +/* + * main.m — entry point for native-hello-ios. + * + * iOS UIKit lifecycle: + * UIApplicationMain never returns. The compiled el program's boot body + * (native_init → window_from_manifest → app_build → window_show) must + * run inside applicationDidFinishLaunchingWithOptions on the main thread. + * + * How it works: + * 1. el_uikit_set_args() forwards argc/argv to UIApplicationMain. + * 2. el_main_entry_fn is set to el_app_boot (defined below) before + * UIApplicationMain is entered. + * 3. ElAppDelegate.didFinishLaunchingWithOptions calls el_main_entry_fn(), + * which runs the el program's widget setup at the correct lifecycle point. + * 4. __native_run_loop() inside the compiled el main is a no-op on iOS — + * UIApplicationMain already owns the run loop. + * + * The generated native_hello.c exports el_app_main() (renamed from main by + * the gen step in build.sh). We call it from el_app_boot so it runs after + * UIApplicationMain sets up the application object. + * + * Compile with -fno-objc-arc (required by el_uikit.m). + */ + +#import + +/* From el_uikit.m */ +void el_uikit_set_args(int argc, char **argv); +extern void (*el_main_entry_fn)(void); + +/* Forward declaration of the renamed compiled el main. */ +int el_app_main(int argc, char **argv); + +/* Boot function registered as el_main_entry_fn. Called by ElAppDelegate + * inside didFinishLaunchingWithOptions on the main thread. */ +static void el_app_boot(void) { + char *argv[] = {"el-app", NULL}; + el_app_main(1, argv); +} + +int main(int argc, char *argv[]) { + el_uikit_set_args(argc, argv); + /* Register the boot function so ElAppDelegate can call it. */ + el_main_entry_fn = el_app_boot; + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, @"ElAppDelegate"); + } +} diff --git a/ui/examples/native-hello-ios/NativeHello/native_hello.c b/ui/examples/native-hello-ios/NativeHello/native_hello.c new file mode 100644 index 0000000..e18f274 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int el_app_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello-ios/build.sh b/ui/examples/native-hello-ios/build.sh new file mode 100755 index 0000000..3c1d92d --- /dev/null +++ b/ui/examples/native-hello-ios/build.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# build.sh — Build and install native-hello-ios to the iOS Simulator. +# +# Usage: +# ./build.sh # gen C sources + xcodebuild for simulator +# ./build.sh gen # regenerate C sources only +# ./build.sh build # xcodebuild only (skip elc step) +# ./build.sh install # gen + build + boot simulator + install + launch +# ./build.sh clean # remove build/ directory +# +# Requirements: +# - Xcode with iOS Simulator SDK installed +# - elc at ../../../lang/dist/platform/elc (or in PATH) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el" +EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el" +SOURCES_DIR="${SCRIPT_DIR}/NativeHello" +BUILD_DIR="${SCRIPT_DIR}/build" + +# Locate elc. +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +gen() { + echo "==> Generating native_hello.c..." + EL_MANIFEST="${EL_MANIFEST}" \ + "${ELC}" "${EL_APP_ENTRY}" \ + > "${SOURCES_DIR}/native_hello.c" + # Rename the compiled el main() to el_app_main so it doesn't conflict with + # main.m's main(). main.m's main() is the real entry point for UIApplicationMain. + # Use a simple sed substitution on the exact signature elc emits. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_app_main(int _argc, char** _argv) {/' \ + "${SOURCES_DIR}/native_hello.c" + + echo "==> Generating el_native_vessel.c..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${SOURCES_DIR}/el_native_vessel.c" + # Rename vessel main() similarly. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${SOURCES_DIR}/el_native_vessel.c" + + echo "==> Copying runtime files..." + cp "${EL_RUNTIME}/el_uikit.m" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_seed.c" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_runtime.c" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_runtime.h" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_native_target.h" "${SOURCES_DIR}/" + + echo "==> Source generation complete." +} + +xcode_build() { + echo "==> Building with xcodebuild (iphonesimulator)..." + xcodebuild \ + -project "${SCRIPT_DIR}/NativeHello.xcodeproj" \ + -scheme NativeHello \ + -sdk iphonesimulator \ + -configuration Debug \ + -derivedDataPath "${BUILD_DIR}" \ + CLANG_ENABLE_OBJC_ARC=NO \ + OTHER_CFLAGS="-DEL_TARGET_IOS" \ + OTHER_LDFLAGS="-ldl" \ + build + + APP_PATH=$(find "${BUILD_DIR}" -name "NativeHello.app" -maxdepth 6 | head -1) + if [ -n "${APP_PATH}" ]; then + echo "==> Build complete." + echo " App bundle: ${APP_PATH}" + else + echo "==> Build complete (app bundle in ${BUILD_DIR})." + fi +} + +install_and_launch() { + gen + xcode_build + + APP_PATH=$(find "${BUILD_DIR}" -name "NativeHello.app" -maxdepth 6 | head -1) + if [ -z "${APP_PATH}" ]; then + echo "Error: could not find NativeHello.app in ${BUILD_DIR}" + exit 1 + fi + + # Boot the default simulator if not already booted. + SIM_UDID=$(xcrun simctl list devices available --json \ + | python3 -c " +import json,sys +d=json.load(sys.stdin)['devices'] +for runtime,devs in d.items(): + for dev in devs: + if 'iPhone' in dev['name'] and dev['isAvailable']: + print(dev['udid']) + exit() +") + if [ -z "${SIM_UDID}" ]; then + echo "Error: no available iPhone simulator found." + exit 1 + fi + echo "==> Using simulator: ${SIM_UDID}" + xcrun simctl boot "${SIM_UDID}" 2>/dev/null || true + open -a Simulator + + echo "==> Installing app..." + xcrun simctl install "${SIM_UDID}" "${APP_PATH}" + + echo "==> Launching app..." + xcrun simctl launch "${SIM_UDID}" "ai.neurontechnologies.el.nativehello" +} + +clean() { + echo "==> Cleaning..." + rm -rf "${BUILD_DIR}" + echo "==> Clean complete." +} + +case "${1:-all}" in + gen) gen ;; + build) xcode_build ;; + install) install_and_launch ;; + clean) clean ;; + all|*) gen && xcode_build ;; +esac diff --git a/ui/examples/native-hello/-o b/ui/examples/native-hello/-o new file mode 100644 index 0000000..e69de29 diff --git a/ui/examples/native-hello/Dockerfile.linux-gtk4 b/ui/examples/native-hello/Dockerfile.linux-gtk4 new file mode 100644 index 0000000..2079162 --- /dev/null +++ b/ui/examples/native-hello/Dockerfile.linux-gtk4 @@ -0,0 +1,88 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y \ + build-essential \ + libgtk-4-dev \ + pkg-config \ + libcurl4-openssl-dev \ + binutils \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# All sources arrive in /build — runtime/ subdirectory contains the el runtime, +# top-level contains elc-generated app/vessel C files. +COPY runtime/ runtime/ +COPY el_native_vessel.c el_native_vessel.h native_hello.c ./ + +# Compile el_gtk4.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_gtk4.c -o /build/el_gtk4.o && \ + echo "el_gtk4.c compiled OK" + +# Compile el_seed.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_seed.c -o /build/el_seed.o && \ + echo "el_seed.c compiled OK" + +# Compile el_runtime.c +RUN gcc -std=c11 -I/build/runtime \ + -c /build/runtime/el_runtime.c -o /build/el_runtime.o && \ + echo "el_runtime.c compiled OK" + +# Patch el_runtime.o: hide __-prefixed symbols that el_seed.o also defines. +# This is the Linux objcopy equivalent of macOS nmedit -s. +# el_seed.o is canonical for all __ symbols; el_runtime.o provides high-level +# builtins (str_len, json_get_string, etc.) that el_seed.o does NOT export. +RUN nm /build/el_seed.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.seed_T.txt && \ + nm /build/el_runtime.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.rt_T.txt && \ + comm -12 /build/.seed_T.txt /build/.rt_T.txt > /build/.dups.txt && \ + if [ -s /build/.dups.txt ]; then \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.dups.txt); \ + eval "objcopy $ARGS /build/el_runtime.o"; \ + echo "Patched $(wc -l < /build/.dups.txt) duplicate symbols in el_runtime.o"; \ + else \ + echo "No duplicate symbols found"; \ + fi + +# Compile vessel +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -c /build/el_native_vessel.c -o /build/el_native_vessel.o && \ + echo "vessel compiled OK" + +# Compile app (before patching vessel, so we know which symbols to localise) +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -include /build/el_native_vessel.h \ + -c /build/native_hello.c -o /build/native_hello.o && \ + echo "app compiled OK" + +# Patch el_native_vessel.o: +# 1. Localise main() so it doesn't conflict with the app's main. +# 2. Localise any T/B symbols the app also defines (elc emits shared globals +# like TOKEN_PRIMARY in every translation unit; GNU ld rejects duplicates +# unlike macOS ld which picks the first one silently). +RUN nm /build/native_hello.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.app_syms.txt && \ + nm /build/el_native_vessel.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.vessel_syms.txt && \ + comm -12 /build/.app_syms.txt /build/.vessel_syms.txt > /build/.vessel_dups.txt && \ + echo "main" >> /build/.vessel_dups.txt && \ + sort -u /build/.vessel_dups.txt -o /build/.vessel_dups.txt && \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.vessel_dups.txt) && \ + eval "objcopy $ARGS /build/el_native_vessel.o" && \ + echo "Patched $(wc -l < /build/.vessel_dups.txt) symbols in el_native_vessel.o" + +# Link — add -lm for math functions (exp, log, etc.) used by el_runtime.c +RUN gcc \ + /build/native_hello.o \ + /build/el_native_vessel.o \ + /build/el_gtk4.o \ + /build/el_runtime.o \ + /build/el_seed.o \ + $(pkg-config --libs gtk4) -ldl -lpthread -lcurl -lm \ + -o /build/native-hello-linux && \ + echo "==> Linked: native-hello-linux" && \ + file /build/native-hello-linux diff --git a/ui/examples/native-hello/Dockerfile.pi b/ui/examples/native-hello/Dockerfile.pi new file mode 100644 index 0000000..a4a30a2 --- /dev/null +++ b/ui/examples/native-hello/Dockerfile.pi @@ -0,0 +1,99 @@ +FROM --platform=linux/arm64 ubuntu:22.04 + +# GTK4 + SDL2 for Raspberry Pi (ARM64 / aarch64) +# Targets Pi 3 / 4 / 5 running 64-bit Raspberry Pi OS or Ubuntu 22.04. +# +# GTK4 requires a desktop environment / Wayland compositor at runtime. +# SDL2 works on bare framebuffer or KMS/DRM without a full desktop. +# +# Usage: +# docker build --platform linux/arm64 \ +# -f Dockerfile.pi --tag el-native-pi-arm64 ./build-docker/ +# docker create --name pi-extract el-native-pi-arm64 +# docker cp pi-extract:/build/native-hello-linux-pi ./build/native-hello-pi-arm64 +# docker rm pi-extract +# +# The GTK4 build is the default. SDL2 is available as an alternative +# for headless / bare-metal Pi deployments. + +RUN apt-get update && apt-get install -y \ + build-essential \ + libgtk-4-dev \ + libsdl2-dev \ + libsdl2-ttf-dev \ + libsdl2-image-dev \ + pkg-config \ + libcurl4-openssl-dev \ + binutils \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# All sources arrive in /build +COPY runtime/ runtime/ +COPY el_native_vessel.c el_native_vessel.h native_hello.c ./ + +# ── GTK4 build ──────────────────────────────────────────────────────────────── + +# Compile el_gtk4.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_gtk4.c -o /build/el_gtk4.o && \ + echo "el_gtk4.c compiled OK" + +# Compile el_seed.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_seed.c -o /build/el_seed.o && \ + echo "el_seed.c compiled OK" + +# Compile el_runtime.c +RUN gcc -std=c11 -I/build/runtime \ + -c /build/runtime/el_runtime.c -o /build/el_runtime.o && \ + echo "el_runtime.c compiled OK" + +# Patch el_runtime.o: localise __-prefixed duplicate symbols (el_seed.o is canonical) +RUN nm /build/el_seed.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.seed_T.txt && \ + nm /build/el_runtime.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.rt_T.txt && \ + comm -12 /build/.seed_T.txt /build/.rt_T.txt > /build/.dups.txt && \ + if [ -s /build/.dups.txt ]; then \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.dups.txt); \ + eval "objcopy $ARGS /build/el_runtime.o"; \ + echo "Patched $(wc -l < /build/.dups.txt) duplicate symbols in el_runtime.o"; \ + fi + +# Compile vessel +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -c /build/el_native_vessel.c -o /build/el_native_vessel.o && \ + echo "vessel compiled OK" + +# Compile app +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -include /build/el_native_vessel.h \ + -c /build/native_hello.c -o /build/native_hello.o && \ + echo "app compiled OK" + +# Patch el_native_vessel.o: localise shared globals (main + TOKEN_* etc.) +RUN nm /build/native_hello.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.app_syms.txt && \ + nm /build/el_native_vessel.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.vessel_syms.txt && \ + comm -12 /build/.app_syms.txt /build/.vessel_syms.txt > /build/.vessel_dups.txt && \ + echo "main" >> /build/.vessel_dups.txt && \ + sort -u /build/.vessel_dups.txt -o /build/.vessel_dups.txt && \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.vessel_dups.txt) && \ + eval "objcopy $ARGS /build/el_native_vessel.o" && \ + echo "Patched $(wc -l < /build/.vessel_dups.txt) symbols in el_native_vessel.o" + +# Link (GTK4) +RUN gcc \ + /build/native_hello.o \ + /build/el_native_vessel.o \ + /build/el_gtk4.o \ + /build/el_runtime.o \ + /build/el_seed.o \ + $(pkg-config --libs gtk4) -ldl -lpthread -lcurl -lm \ + -o /build/native-hello-linux-pi && \ + echo "==> Linked: native-hello-linux-pi (GTK4)" && \ + file /build/native-hello-linux-pi diff --git a/ui/examples/native-hello/build-docker/el_native_vessel.c b/ui/examples/native-hello/build-docker/el_native_vessel.c new file mode 100644 index 0000000..28bf38c --- /dev/null +++ b/ui/examples/native-hello/build-docker/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build-docker/el_native_vessel.h b/ui/examples/native-hello/build-docker/el_native_vessel.h new file mode 100644 index 0000000..b62c288 --- /dev/null +++ b/ui/examples/native-hello/build-docker/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build-docker/native_hello.c b/ui/examples/native-hello/build-docker/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello/build-docker/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello/build-docker/runtime/el_gtk4.c b/ui/examples/native-hello/build-docker/runtime/el_gtk4.c new file mode 100644 index 0000000..1ae01d0 --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_gtk4.c @@ -0,0 +1,1027 @@ +/* + * el_gtk4.c — GTK4 backend for the el native widget system (Linux). + * + * This file implements the Linux GTK4 widget layer that el_seed.c calls + * through to when EL_TARGET_LINUX is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_gtk4_* C-callable functions declared here + * → GtkWidget* / GtkWindow* via GTK4 C API + * + * Widget handles: every widget is assigned an int64_t slot index into + * _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved; invalid handle = -1 convention). + * + * Threading: GTK4 must run on the main thread. el_gtk4_sync_main() uses + * g_main_context_invoke_full() to dispatch from any thread, waiting for + * completion via a GMutex/GCond pair — the same role as dispatch_sync() in + * the AppKit bridge. + * + * Callback mechanism: on signal, the bridge calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * matching the el_appkit.m / __thread_create pattern in el_seed.c. + * + * Compile: + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + * el_gtk4.c -c -o el_gtk4.o + * Then link el_gtk4.o alongside el_seed.c with: + * $(pkg-config --libs gtk4) -ldl + * + * GTK4 version notes: + * • gtk_widget_get_style_context() is deprecated in GTK 4.10+. + * A #if GTK_MINOR_VERSION guard selects the correct CSS-provider path. + * • gtk_editable_get_text() / gtk_editable_set_text() are the unified + * GTK4 text API for GtkEntry and any GtkEditable. + */ + +#ifdef EL_TARGET_LINUX + +/* _GNU_SOURCE: expose RTLD_DEFAULT (dlfcn.h) and strdup (string.h) on Linux. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include + +/* G_APPLICATION_DEFAULT_FLAGS was added in GLib 2.74. + * Ubuntu 22.04 ships GLib 2.72 — use the older alias when running on it. */ +#ifndef G_APPLICATION_DEFAULT_FLAGS +#define G_APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE +#endif + +/* el_val_t must already be defined by el_runtime.h / el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +#ifndef EL_CSTR +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#endif +#ifndef EL_STR +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#endif + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_GTK4_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + GtkWidget* widget; /* Strong reference (g_object_ref'd on alloc). */ + char* cb_click; /* El function name for click/submit events. */ + char* cb_change;/* El function name for value-change events. */ +} ElWidget; + +static ElWidget _el_widgets[EL_GTK4_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Allocate a slot; returns slot index or -1 if full. Takes a strong ref. */ +static int64_t el_widget_alloc(ElWidgetKind kind, GtkWidget* w) { + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].widget = (GtkWidget*)g_object_ref(w); + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +/* Look up a slot. Returns NULL for invalid/free handle. */ +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_GTK4_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +/* Release a slot. Unrefs the widget; callers must remove from parent first. */ +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->widget) { g_object_unref(w->widget); w->widget = NULL; } + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* Forward declaration — defined below in the GtkApplication globals section. */ +static int _el_app_running; + +/* ── CSS / styling helpers ────────────────────────────────────────────────── */ + +/* + * apply_css — attach a CSS snippet to a single widget with APPLICATION priority. + * GTK 4.10 deprecated gtk_widget_get_style_context(); use gtk_widget_add_css_class() + * + a display-level provider for new GTK, or the style-context path for older GTK. + * + * We use the style-context path behind a version guard because it reliably + * scopes the CSS to just this widget, which is what the el property setters + * need (each call replaces/adds to the widget's own stylesheet). + */ +static void apply_css(GtkWidget* widget, const char* css_str) { + if (!widget || !css_str) return; + GtkCssProvider* p = gtk_css_provider_new(); + +#if GTK_CHECK_VERSION(4, 12, 0) + /* GTK 4.12+: load_from_string takes a plain string (no length). */ + gtk_css_provider_load_from_string(p, css_str); +#else + gtk_css_provider_load_from_data(p, css_str, -1); +#endif + +#if GTK_MINOR_VERSION >= 10 + /* 4.10+: style context is deprecated; use display-scoped provider + css class. + * We add a unique widget name so the selector targets only this widget. */ + static guint64 _css_id_counter = 0; + char name_buf[32]; + snprintf(name_buf, sizeof(name_buf), "el-w-%" G_GUINT64_FORMAT, ++_css_id_counter); + gtk_widget_set_name(widget, name_buf); + + /* Wrap the CSS to scope to this widget's name. */ + char* scoped = NULL; + int n = asprintf(&scoped, "#%s { %s }", name_buf, css_str); + (void)n; + if (scoped) { + GtkCssProvider* sp = gtk_css_provider_new(); +#if GTK_CHECK_VERSION(4, 12, 0) + gtk_css_provider_load_from_string(sp, scoped); +#else + gtk_css_provider_load_from_data(sp, scoped, -1); +#endif + gtk_style_context_add_provider_for_display( + gtk_widget_get_display(widget), + GTK_STYLE_PROVIDER(sp), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(sp); + free(scoped); + } + g_object_unref(p); +#else + /* GTK < 4.10: style context is available and widget-scoped. */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_style_context_add_provider( + gtk_widget_get_style_context(widget), + GTK_STYLE_PROVIDER(p), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + G_GNUC_END_IGNORE_DEPRECATIONS + g_object_unref(p); +#endif +} + +/* ── Main-thread dispatch ────────────────────────────────────────────────── */ + +/* + * Dispatch a function on the GTK main thread, blocking until it completes. + * If already on the main thread, calls the function directly. + * + * Use a GMainContext + GMutex/GCond pair so we can synchronously wait for + * the idle callback to execute, mirroring dispatch_sync(main_queue, ...). + */ + +typedef struct { + void (*fn)(void*); + void* data; + GMutex mutex; + GCond cond; + int done; +} ElSyncCall; + +static gboolean _el_sync_dispatch(gpointer user_data) { + ElSyncCall* call = (ElSyncCall*)user_data; + call->fn(call->data); + g_mutex_lock(&call->mutex); + call->done = 1; + g_cond_signal(&call->cond); + g_mutex_unlock(&call->mutex); + return G_SOURCE_REMOVE; +} + +static void el_gtk4_sync_main(void (*fn)(void*), void* data) { + /* + * Pre-run phase (el boot sequence, before native_run_loop): + * The GMainContext is not yet iterating. We are on the main thread. + * Call directly — no dispatch needed. + * + * Post-run phase (callbacks from GTK signal handlers or other threads): + * Dispatch to the main context and wait for completion. + */ + if (!_el_app_running) { + fn(data); + return; + } + + /* Check if we're already on the thread running the default main context. */ + GMainContext* main_ctx = g_main_context_default(); + if (g_main_context_is_owner(main_ctx)) { + fn(data); + return; + } + ElSyncCall call; + call.fn = fn; + call.data = data; + call.done = 0; + g_mutex_init(&call.mutex); + g_cond_init(&call.cond); + + g_main_context_invoke(main_ctx, _el_sync_dispatch, &call); + + g_mutex_lock(&call.mutex); + while (!call.done) { + g_cond_wait(&call.cond, &call.mutex); + } + g_mutex_unlock(&call.mutex); + g_mutex_clear(&call.mutex); + g_cond_clear(&call.cond); +} + +/* Convenience macro: declare a local struct, fill it, dispatch. */ +#define EL_SYNC(type, var, ...) \ + type var = { __VA_ARGS__ }; \ + el_gtk4_sync_main(_el_sync_##var, &var) + +/* ── El callback invocation ──────────────────────────────────────────────── */ + +/* + * El callback functions have signature (per el_seed.c conventions): + * void fn_name(int64_t handle, int64_t data) + * where data is a (const char*) cast to int64_t — same as AppKit bridge. + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_gtk4_invoke_cb(const char* fn_name, int64_t handle, + const char* data) { + if (!fn_name || !*fn_name) return; + void* sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── GtkApplication (global) ─────────────────────────────────────────────── */ + +static GtkApplication* _el_app = NULL; +static int _el_app_running = 0; /* set to 1 after activate fires */ + +/* ── Signal callbacks ────────────────────────────────────────────────────── */ + +/* Slot index stored as widget data so signal handlers can look it up. */ +#define EL_SLOT_KEY "el-slot" + +static void _el_on_button_clicked(GtkButton* btn, gpointer user_data) { + (void)btn; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + el_gtk4_invoke_cb(w->cb_click, slot, ""); + } +} + +static void _el_on_entry_changed(GtkEditable* editable, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_change) { + const char* text = gtk_editable_get_text(editable); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + } +} + +static void _el_on_entry_activate(GtkEntry* entry, gpointer user_data) { + (void)entry; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + const char* text = gtk_editable_get_text(GTK_EDITABLE(entry)); + el_gtk4_invoke_cb(w->cb_click, slot, text ? text : ""); + } +} + +/* GtkTextBuffer "changed" signal for textarea. */ +static void _el_on_textbuffer_changed(GtkTextBuffer* buf, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (!w || !w->cb_change) return; + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + char* text = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + g_free(text); +} + +/* ── GtkApplication activate callback ───────────────────────────────────── */ + +static void _el_app_activate(GtkApplication* app, gpointer user_data) { + /* + * Register all pre-created windows with the application, and mark the + * app as running so subsequent calls use el_gtk4_sync_main dispatching. + * + * On macOS the GTK4 Quartz backend requires g_application_run() on the + * main thread. Windows created before run use gtk_window_new(); we + * register them here via gtk_application_add_window() so they participate + * in the application lifecycle (app quits when last window closes, etc.). + */ + (void)user_data; + _el_app_running = 1; + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_WINDOW && _el_widgets[i].widget) { + gtk_application_add_window(app, GTK_WINDOW(_el_widgets[i].widget)); + } + } +} + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_gtk4_init — initialize GTK4 + GtkApplication. Idempotent. + * Must be called once before any other el_gtk4_* function. + */ +void el_gtk4_init(void) { + static int done = 0; + if (done) return; + done = 1; + + _el_app = gtk_application_new("ai.neuralplatform.el", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(_el_app, "activate", G_CALLBACK(_el_app_activate), NULL); +} + +/* + * el_gtk4_run_loop — start the GTK main loop. Never returns. + * el code calls __native_run_loop() which dispatches here. + * + * On macOS the GTK4 Quartz backend requires g_application_run() to be called + * from the main thread. Since the el boot sequence (native_init → window_create + * → widget_build → native_run_loop) runs on the main thread, g_application_run + * is always called from the main thread here. + * + * Windows created before this call use gtk_window_new() (no GApplication + * needed); the activate handler registers them with the GtkApplication. + */ +void el_gtk4_run_loop(void) { + if (!_el_app) el_gtk4_init(); + int status = g_application_run(G_APPLICATION(_el_app), 0, NULL); + (void)status; +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +typedef struct { + const char* title; + int width, height, min_width, min_height; + int64_t result; +} _ElWindowCreateArgs; + +static void _el_window_create_main(void* vp) { + _ElWindowCreateArgs* a = (_ElWindowCreateArgs*)vp; + /* + * Use gtk_window_new() rather than gtk_application_window_new(). + * gtk_application_window_new() requires the GApplication to be running + * (i.e., after g_application_run / activate), which is not yet the case + * when the el boot sequence creates windows before native_run_loop(). + * The activate handler in el_gtk4_run_loop registers these windows with + * the GtkApplication via gtk_application_add_window(). + */ + GtkWidget* win = gtk_window_new(); + gtk_window_set_title(GTK_WINDOW(win), a->title ? a->title : ""); + gtk_window_set_default_size(GTK_WINDOW(win), a->width, a->height); + + /* Set minimum size via content-area size hint. */ + if (a->min_width > 0 || a->min_height > 0) { + gtk_widget_set_size_request(win, a->min_width, a->min_height); + } + + /* Use a vertical GtkBox as the root content widget so children stack + * vertically by default — mirrors the NSStackView root in AppKit. */ + GtkWidget* root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_window_set_child(GTK_WINDOW(win), root); + + a->result = el_widget_alloc(EL_WIDGET_WINDOW, win); +} + +int64_t el_gtk4_window_create(const char* title, int width, int height, + int min_width, int min_height) { + _ElWindowCreateArgs a = { title, width, height, min_width, min_height, -1 }; + el_gtk4_sync_main(_el_window_create_main, &a); + return a.result; +} + +typedef struct { int64_t handle; } _ElHandleArgs; + +static void _el_window_show_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + gtk_window_present(GTK_WINDOW(w->widget)); +} + +void el_gtk4_window_show(int64_t handle) { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_window_show_main, &a); +} + +typedef struct { int64_t handle; const char* title; } _ElSetTitleArgs; + +static void _el_window_set_title_main(void* vp) { + _ElSetTitleArgs* a = (_ElSetTitleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + gtk_window_set_title(GTK_WINDOW(w->widget), a->title ? a->title : ""); +} + +void el_gtk4_window_set_title(int64_t handle, const char* title) { + _ElSetTitleArgs a = { handle, title }; + el_gtk4_sync_main(_el_window_set_title_main, &a); +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +typedef struct { + GtkOrientation orient; + int spacing; + int64_t result; +} _ElStackCreateArgs; + +static void _el_stack_create_main(void* vp) { + _ElStackCreateArgs* a = (_ElStackCreateArgs*)vp; + GtkWidget* box = gtk_box_new(a->orient, a->spacing); + ElWidgetKind kind = (a->orient == GTK_ORIENTATION_VERTICAL) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + a->result = el_widget_alloc(kind, box); +} + +int64_t el_gtk4_vstack_create(int spacing) { + _ElStackCreateArgs a = { GTK_ORIENTATION_VERTICAL, spacing, -1 }; + el_gtk4_sync_main(_el_stack_create_main, &a); + return a.result; +} + +int64_t el_gtk4_hstack_create(int spacing) { + _ElStackCreateArgs a = { GTK_ORIENTATION_HORIZONTAL, spacing, -1 }; + el_gtk4_sync_main(_el_stack_create_main, &a); + return a.result; +} + +typedef struct { int64_t result; } _ElResultArgs; + +static void _el_zstack_create_main(void* vp) { + _ElResultArgs* a = (_ElResultArgs*)vp; + GtkWidget* overlay = gtk_overlay_new(); + a->result = el_widget_alloc(EL_WIDGET_ZSTACK, overlay); +} + +int64_t el_gtk4_zstack_create(void) { + _ElResultArgs a = { -1 }; + el_gtk4_sync_main(_el_zstack_create_main, &a); + return a.result; +} + +static void _el_scroll_create_main(void* vp) { + _ElResultArgs* a = (_ElResultArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + a->result = el_widget_alloc(EL_WIDGET_SCROLL, sw); +} + +int64_t el_gtk4_scroll_create(void) { + _ElResultArgs a = { -1 }; + el_gtk4_sync_main(_el_scroll_create_main, &a); + return a.result; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +typedef struct { const char* text; int64_t result; } _ElTextArgs; + +static void _el_label_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* lbl = gtk_label_new(a->text ? a->text : ""); + gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f); + gtk_label_set_wrap(GTK_LABEL(lbl), FALSE); + a->result = el_widget_alloc(EL_WIDGET_LABEL, lbl); +} + +int64_t el_gtk4_label_create(const char* text) { + _ElTextArgs a = { text, -1 }; + el_gtk4_sync_main(_el_label_create_main, &a); + return a.result; +} + +static void _el_button_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* btn = gtk_button_new_with_label(a->text ? a->text : ""); + int64_t slot = el_widget_alloc(EL_WIDGET_BUTTON, btn); + if (slot >= 0) { + g_signal_connect(btn, "clicked", + G_CALLBACK(_el_on_button_clicked), + (gpointer)(intptr_t)slot); + } + a->result = slot; +} + +int64_t el_gtk4_button_create(const char* label) { + _ElTextArgs a = { label, -1 }; + el_gtk4_sync_main(_el_button_create_main, &a); + return a.result; +} + +static void _el_text_field_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* entry = gtk_entry_new(); + if (a->text && *a->text) { + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), a->text); + } + int64_t slot = el_widget_alloc(EL_WIDGET_TEXTFIELD, entry); + if (slot >= 0) { + gpointer slot_ptr = (gpointer)(intptr_t)slot; + g_signal_connect(entry, "changed", + G_CALLBACK(_el_on_entry_changed), slot_ptr); + g_signal_connect(entry, "activate", + G_CALLBACK(_el_on_entry_activate), slot_ptr); + } + a->result = slot; +} + +int64_t el_gtk4_text_field_create(const char* placeholder) { + _ElTextArgs a = { placeholder, -1 }; + el_gtk4_sync_main(_el_text_field_create_main, &a); + return a.result; +} + +static void _el_text_area_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(sw, TRUE); + + GtkWidget* tv = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(tv), GTK_WRAP_WORD_CHAR); + gtk_widget_set_hexpand(tv, TRUE); + gtk_widget_set_vexpand(tv, TRUE); + + /* GTK4 GtkTextView has no native placeholder; set accessible description. */ + if (a->text && *a->text) { + gtk_accessible_update_property(GTK_ACCESSIBLE(tv), + GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, a->text, -1); + } + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), tv); + + /* Store the scroll window as the widget; text view accessible via child. */ + int64_t slot = el_widget_alloc(EL_WIDGET_TEXTAREA, sw); + if (slot >= 0) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + g_signal_connect(buf, "changed", + G_CALLBACK(_el_on_textbuffer_changed), + (gpointer)(intptr_t)slot); + } + a->result = slot; +} + +int64_t el_gtk4_text_area_create(const char* placeholder) { + _ElTextArgs a = { placeholder, -1 }; + el_gtk4_sync_main(_el_text_area_create_main, &a); + return a.result; +} + +static void _el_image_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* img = NULL; + if (a->text && *a->text) { + /* Try as a filesystem path first. */ + if (g_file_test(a->text, G_FILE_TEST_EXISTS)) { + img = gtk_picture_new_for_filename(a->text); + } + if (!img) { + /* Fall back to icon name. */ + img = gtk_image_new_from_icon_name(a->text); + } + } + if (!img) { + img = gtk_picture_new(); + } + a->result = el_widget_alloc(EL_WIDGET_IMAGE, img); +} + +int64_t el_gtk4_image_create(const char* path_or_name) { + _ElTextArgs a = { path_or_name, -1 }; + el_gtk4_sync_main(_el_image_create_main, &a); + return a.result; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +typedef struct { int64_t handle; const char* text; } _ElWidgetTextArgs; + +static void _el_widget_set_text_main(void* vp) { + _ElWidgetTextArgs* a = (_ElWidgetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + const char* s = a->text ? a->text : ""; + switch (w->kind) { + case EL_WIDGET_LABEL: + gtk_label_set_text(GTK_LABEL(w->widget), s); + break; + case EL_WIDGET_TEXTFIELD: + gtk_editable_set_text(GTK_EDITABLE(w->widget), s); + break; + case EL_WIDGET_BUTTON: + gtk_button_set_label(GTK_BUTTON(w->widget), s); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + gtk_text_buffer_set_text(buf, s, -1); + } + break; + } + case EL_WIDGET_WINDOW: + gtk_window_set_title(GTK_WINDOW(w->widget), s); + break; + default: break; + } +} + +void el_gtk4_widget_set_text(int64_t handle, const char* text) { + _ElWidgetTextArgs a = { handle, text }; + el_gtk4_sync_main(_el_widget_set_text_main, &a); +} + +typedef struct { int64_t handle; const char* result; } _ElWidgetGetTextArgs; + +static void _el_widget_get_text_main(void* vp) { + _ElWidgetGetTextArgs* a = (_ElWidgetGetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) { a->result = ""; return; } + switch (w->kind) { + case EL_WIDGET_LABEL: + a->result = gtk_label_get_text(GTK_LABEL(w->widget)); + break; + case EL_WIDGET_TEXTFIELD: + a->result = gtk_editable_get_text(GTK_EDITABLE(w->widget)); + break; + case EL_WIDGET_BUTTON: + a->result = gtk_button_get_label(GTK_BUTTON(w->widget)); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + /* Returns g_malloc'd string — caller treats it as short-lived. */ + a->result = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + } else { + a->result = ""; + } + break; + } + default: + a->result = ""; + break; + } +} + +const char* el_gtk4_widget_get_text(int64_t handle) { + _ElWidgetGetTextArgs a = { handle, "" }; + el_gtk4_sync_main(_el_widget_get_text_main, &a); + return a.result ? strdup(a.result) : strdup(""); +} + +typedef struct { + int64_t handle; + float r, g, b, a; +} _ElColorArgs; + +static void _el_widget_set_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[128]; + snprintf(css, sizeof(css), "color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_color(int64_t handle, float r, float g, float b, float a) { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_color_main, &args); +} + +static void _el_widget_set_bg_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + char css[128]; + snprintf(css, sizeof(css), "background-color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_bg_color_main, &args); +} + +typedef struct { + int64_t handle; + const char* family; + int size; + int bold; +} _ElFontArgs; + +static void _el_widget_set_font_main(void* vp) { + _ElFontArgs* a = (_ElFontArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[256]; + const char* fam = (a->family && *a->family && strcmp(a->family, "system") != 0) + ? a->family : "sans-serif"; + snprintf(css, sizeof(css), + "font-family: %s; font-size: %dpx; font-weight: %s;", + fam, a->size, a->bold ? "bold" : "normal"); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_font(int64_t handle, const char* family, int size, int bold) { + _ElFontArgs a = { handle, family, size, bold }; + el_gtk4_sync_main(_el_widget_set_font_main, &a); +} + +typedef struct { int64_t handle; int top, right, bottom, left; } _ElPaddingArgs; + +static void _el_widget_set_padding_main(void* vp) { + _ElPaddingArgs* a = (_ElPaddingArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[128]; + snprintf(css, sizeof(css), "padding: %dpx %dpx %dpx %dpx;", + a->top, a->right, a->bottom, a->left); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + _ElPaddingArgs a = { handle, top, right, bottom, left }; + el_gtk4_sync_main(_el_widget_set_padding_main, &a); +} + +typedef struct { int64_t handle; int value; } _ElIntPropArgs; + +static void _el_widget_set_width_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gint cur_h = -1; + gtk_widget_get_size_request(w->widget, NULL, &cur_h); + gtk_widget_set_size_request(w->widget, a->value, cur_h); +} + +void el_gtk4_widget_set_width(int64_t handle, int width) { + _ElIntPropArgs a = { handle, width }; + el_gtk4_sync_main(_el_widget_set_width_main, &a); +} + +static void _el_widget_set_height_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gint cur_w = -1; + gtk_widget_get_size_request(w->widget, &cur_w, NULL); + gtk_widget_set_size_request(w->widget, cur_w, a->value); +} + +void el_gtk4_widget_set_height(int64_t handle, int height) { + _ElIntPropArgs a = { handle, height }; + el_gtk4_sync_main(_el_widget_set_height_main, &a); +} + +static void _el_widget_set_flex_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gboolean expand = (a->value > 0) ? TRUE : FALSE; + gtk_widget_set_hexpand(w->widget, expand); + gtk_widget_set_vexpand(w->widget, expand); +} + +void el_gtk4_widget_set_flex(int64_t handle, int flex) { + _ElIntPropArgs a = { handle, flex }; + el_gtk4_sync_main(_el_widget_set_flex_main, &a); +} + +static void _el_widget_set_corner_radius_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + char css[64]; + snprintf(css, sizeof(css), "border-radius: %dpx;", a->value); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_corner_radius(int64_t handle, int radius) { + _ElIntPropArgs a = { handle, radius }; + el_gtk4_sync_main(_el_widget_set_corner_radius_main, &a); +} + +static void _el_widget_set_disabled_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + gtk_widget_set_sensitive(w->widget, !a->value); +} + +void el_gtk4_widget_set_disabled(int64_t handle, int disabled) { + _ElIntPropArgs a = { handle, disabled }; + el_gtk4_sync_main(_el_widget_set_disabled_main, &a); +} + +static void _el_widget_set_hidden_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gtk_widget_set_visible(w->widget, !a->value); +} + +void el_gtk4_widget_set_hidden(int64_t handle, int hidden) { + _ElIntPropArgs a = { handle, hidden }; + el_gtk4_sync_main(_el_widget_set_hidden_main, &a); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +typedef struct { int64_t parent; int64_t child; } _ElParentChildArgs; + +static void _el_widget_add_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add a window as child */ + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Append to the root VBox that window_create installs as child. */ + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_append(GTK_BOX(root), child_widget); + } else { + /* Fallback: replace root child. */ + gtk_window_set_child(GTK_WINDOW(pw->widget), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_append(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + /* First child becomes the base; subsequent children are overlays. */ + if (!gtk_overlay_get_child(GTK_OVERLAY(pw->widget))) { + gtk_overlay_set_child(GTK_OVERLAY(pw->widget), child_widget); + } else { + gtk_overlay_add_overlay(GTK_OVERLAY(pw->widget), child_widget); + } + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), + child_widget); + break; + default: + /* Generic fallback — shouldn't be reached for standard widget kinds. */ + break; + } +} + +void el_gtk4_widget_add_child(int64_t parent, int64_t child) { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_add_child_main, &a); +} + +static void _el_widget_remove_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_remove(GTK_BOX(root), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_remove(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + gtk_overlay_remove_overlay(GTK_OVERLAY(pw->widget), child_widget); + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), NULL); + break; + default: + break; + } +} + +void el_gtk4_widget_remove_child(int64_t parent, int64_t child) { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_remove_child_main, &a); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +/* + * on_click / on_change / on_submit just store the function name string. + * The actual g_signal_connect calls happen at widget creation time. + * Here we update the stored callback so the already-connected signal handler + * will call the new function name. + */ + +void el_gtk4_widget_on_click(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_gtk4_widget_on_change(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_gtk4_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, "submit" = Enter key activate → stored in cb_click. */ + el_gtk4_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +static void _el_widget_destroy_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + if (w->kind == EL_WIDGET_WINDOW) { + gtk_window_destroy(GTK_WINDOW(w->widget)); + } else { + /* Unparent the widget before freeing the slot. */ + GtkWidget* parent = gtk_widget_get_parent(w->widget); + if (parent) { + if (GTK_IS_BOX(parent)) { + gtk_box_remove(GTK_BOX(parent), w->widget); + } else if (GTK_IS_OVERLAY(parent)) { + gtk_overlay_remove_overlay(GTK_OVERLAY(parent), w->widget); + } else if (GTK_IS_SCROLLED_WINDOW(parent)) { + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(parent), NULL); + } + /* gtk_window_get_child case: the parent is a GtkBox inside a window */ + } + } + el_widget_free(a->handle); +} + +void el_gtk4_widget_destroy(int64_t handle) { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_widget_destroy_main, &a); +} + +#endif /* EL_TARGET_LINUX */ diff --git a/ui/examples/native-hello/build-docker/runtime/el_native_target.h b/ui/examples/native-hello/build-docker/runtime/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello/build-docker/runtime/el_runtime.c b/ui/examples/native-hello/build-docker/runtime/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _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(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +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) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + 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) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + 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("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * 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 { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* 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; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + 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'; + curl_easy_setopt(c, CURLOPT_URL, url); + 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"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + 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)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + 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); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + 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); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + 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"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + 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"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + 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; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +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); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +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); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + 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 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)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + 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 0; + } + 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; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + 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) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + 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); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + 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) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); 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); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +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_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + 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) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* 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) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +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) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + 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, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + 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 default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + 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)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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 + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +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. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* 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) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + 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); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(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 + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * 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"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + 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). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + 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_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + 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/ui/examples/native-hello/build-docker/runtime/el_runtime.h b/ui/examples/native-hello/build-docker/runtime/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * 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_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#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 + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +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); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +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, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * 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. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"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 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +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); +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 ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +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 + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +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 ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +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. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +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/ui/examples/native-hello/build-docker/runtime/el_seed.c b/ui/examples/native-hello/build-docker/runtime/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +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); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +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_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(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); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +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_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +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); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +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_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +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); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +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); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello/build-docker/runtime/el_seed.h b/ui/examples/native-hello/build-docker/runtime/el_seed.h new file mode 100644 index 0000000..08767df --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_seed.h @@ -0,0 +1,324 @@ +/* + * el_seed.h — El language seed runtime header + * + * Declares all OS-boundary primitives available to compiled El programs. + * All functions use the __ prefix convention. Signatures use el_val_t (= int64_t) + * as the universal value type. + * + * el_seed.c is the complete C boundary for the El runtime. The heavy runtime + * (el_runtime.c) has been retired — everything lives in el_seed.c plus the + * native El runtime (runtime/ *.el files). + * + * Link requirements: + * -lcurl — HTTP client (__http_do, __http_do_to_file) + * -lpthread — threading (__thread_create, __thread_join, __mutex_new, ...) + * + * Canonical compile (via elb): + * elb builds and links el_seed.c automatically. + */ + +#pragma once + +#include +#include + +/* ── Value model ───────────────────────────────────────────────────────────── + * All El values are el_val_t (int64_t). On 64-bit systems a pointer fits. + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t (stored directly) + * Bool -> el_val_t (0 = false, nonzero = true) + * Void -> void + */ +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#define EL_INT(v) (v) +#define EL_NULL ((el_val_t)0) + +/* Float values share the el_val_t slot via bit-cast. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; u.i = (int64_t)v; return u.f; +} +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; u.f = f; return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); /* returns Int (byte value) */ +el_val_t __str_alloc(el_val_t n); /* malloc(n+1), zero-init, return String */ +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); /* s[i]=c, return s */ +el_val_t __str_cmp(el_val_t a, el_val_t b); /* strcmp result as Int */ +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); /* strncmp */ +el_val_t __str_concat_raw(el_val_t a, el_val_t b); /* malloc+strcpy concat */ +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); /* substring copy */ +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); /* f is bit-cast double */ +el_val_t __str_to_float(el_val_t s); /* strtod, bit-cast result */ + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s); +void __print(el_val_t s); +el_val_t __readline(void); + +/* ── 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_exists(el_val_t path); +el_val_t __fs_list_raw(el_val_t path); /* newline-separated filenames */ +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +/* Unified HTTP call. headers_json is a JSON object of header name->value pairs + * (e.g. {"Authorization":"Bearer ...","Content-Type":"application/json"}). + * Use "" or "{}" for no extra headers. timeout_ms <= 0 uses the default. */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); + +/* Stream response body directly to a file. Returns 1 on success, 0 on failure. */ +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path); + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ + +/* Blocking HTTP server. handler_name is the El function name to dispatch to. + * v1 handler: (method, path, body) -> String + * v2 handler: (method, path, headers_map, body) -> String or envelope */ +void __http_serve(el_val_t port, el_val_t handler_name); +void __http_serve_v2(el_val_t port, el_val_t handler_name); + +/* Build a structured HTTP response envelope. + * headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ + +/* Returns the raw file descriptor for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. + * Returns -1 if called outside a handler context. */ +el_val_t __http_conn_fd(void); + +/* Sends SSE response headers on conn_id (the fd from __http_conn_fd), + * keeping the connection open for streaming. Returns 1 on success, 0 on + * write failure. Call once at the start of an SSE handler. */ +el_val_t __http_sse_open(el_val_t conn_id); + +/* Writes one SSE event frame: "data: \n\n". data must not contain + * newlines. Returns 1 on success, 0 if the client disconnected. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); + +/* Closes the SSE connection. The handler must return http_sse_sentinel() + * so the HTTP worker does not double-close the fd. */ +el_val_t __http_sse_close(el_val_t conn_id); + +/* ── Threading ───────────────────────────────────────────────────────────── */ + +/* Create a thread that calls the named El function with a String argument. + * fn_name is resolved via dlsym(RTLD_DEFAULT, fn_name). Returns a thread + * handle Int that can be passed to __thread_join. Returns -1 on failure. */ +el_val_t __thread_create(el_val_t fn_name, el_val_t arg); + +/* Wait for thread tid (returned by __thread_create) to finish. + * Returns the thread's return value as a String. */ +el_val_t __thread_join(el_val_t tid); + +/* Allocate a new mutex. Returns a handle Int (index into internal table). */ +el_val_t __mutex_new(void); + +void __mutex_lock(el_val_t m); +void __mutex_unlock(el_val_t m); + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd); /* popen, capture all stdout, return String */ +void __exec_bg(el_val_t cmd); /* fire and forget */ + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key); /* getenv, return "" if not set */ +void __exit_program(el_val_t code); +el_val_t __args_json(void); /* CLI args as JSON array string */ + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void); /* clock_gettime REALTIME, nanoseconds */ +void __sleep_ms(el_val_t ms); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t __uuid_v4(void); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f); +el_val_t __log_f(el_val_t f); +el_val_t __ln_f(el_val_t f); +el_val_t __sin_f(el_val_t f); +el_val_t __cos_f(el_val_t f); +el_val_t __pi_f(void); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json_str, el_val_t key); +el_val_t __json_parse(el_val_t s); +el_val_t __json_stringify(el_val_t v); +el_val_t __json_parse_map(el_val_t json_str); /* alias for __json_parse */ +el_val_t __json_stringify_val(el_val_t val); /* alias for __json_stringify */ +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_get_string(el_val_t json_str, el_val_t key); +el_val_t __json_get_int(el_val_t json_str, el_val_t key); +el_val_t __json_get_float(el_val_t json_str, el_val_t key); +el_val_t __json_get_bool(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); + +/* ── State K/V ───────────────────────────────────────────────────────────── */ + +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); + +/* ── HTML/URL ────────────────────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* ── Engram ──────────────────────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t __engram_remove_layer(el_val_t layer_id); +el_val_t __engram_list_layers(void); +el_val_t __engram_get_node(el_val_t id); +void __engram_strengthen(el_val_t node_id); +void __engram_forget(el_val_t node_id); +el_val_t __engram_node_count(void); +el_val_t __engram_search(el_val_t query, el_val_t limit); +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset); +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t __engram_neighbors(el_val_t node_id); +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t __engram_edge_count(void); +el_val_t __engram_activate(el_val_t query, el_val_t depth); +el_val_t __engram_save(el_val_t path); +el_val_t __engram_load(el_val_t path); +el_val_t __engram_get_node_json(el_val_t id); +el_val_t __engram_search_json(el_val_t query, el_val_t limit); +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t __engram_activate_json(el_val_t query, el_val_t depth); +el_val_t __engram_stats_json(void); +el_val_t __engram_list_layers_json(void); +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +/* __sha256_hex — return the SHA-256 hex digest of a string. + * The returned string is 64 hex characters (lowercase). */ +el_val_t __sha256_hex(el_val_t s); + +/* ── args init (called from main) ────────────────────────────────────────── */ +/* Store argc/argv for __args_json. Call once at the start of main(). */ +void el_seed_init_args(int argc, char** argv); + +/* ── Native widget system (macOS AppKit) ─────────────────────────────────── */ +/* + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m + * (-framework Cocoa). Widget handles are opaque Int values (int64_t slot + * indices). The null/invalid handle value is -1. + * + * Lifecycle: + * 1. Call __native_init() once at program start. + * 2. Build widget tree with __window_create / __vstack_create / etc. + * 3. Attach children with __widget_add_child(parent, child). + * 4. Register callbacks with __widget_on_click / __widget_on_change. + * 5. Call __window_show(win) to make the window visible. + * 6. Call __native_run_loop() — never returns. AppKit owns the thread. + * + * Callback El function signature: + * fn my_handler(widget: Int, data: String) -> Void + */ +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +/* Parse the app{} block from manifest.el at path. Returns a JSON string: + * {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +#ifdef __cplusplus +} +#endif diff --git a/ui/examples/native-hello/build-gtk4.sh b/ui/examples/native-hello/build-gtk4.sh new file mode 100755 index 0000000..0cb2ad9 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# build-gtk4.sh — Compile and run native-hello using the GTK4 bridge on macOS. +# +# Usage: +# ./build-gtk4.sh # compile and run +# ./build-gtk4.sh compile # compile only +# ./build-gtk4.sh clean # remove build artefacts +# +# Requirements: +# - elc in PATH (or ../../../lang/dist/platform/elc) +# - clang (Xcode Command Line Tools) +# - GTK4 via Homebrew: brew install gtk4 +# - macOS 12+ +# +# This is the same pipeline as build.sh but targeting the GTK4 backend: +# - Links with $(pkg-config --libs gtk4) instead of -framework Cocoa +# - Compiles el_gtk4.c (plain C) instead of el_appkit.m (ObjC) +# - Defines EL_TARGET_LINUX (the GTK4 target flag) +# +# Multi-file compilation strategy: see build.sh comments — identical rationale. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +BUILD_DIR="${SCRIPT_DIR}/build-gtk4" + +# Locate elc +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +# GTK4 pkg-config flags +GTK4_CFLAGS=$(pkg-config --cflags gtk4) +GTK4_LIBS=$(pkg-config --libs gtk4) + +CLANG_FLAGS_COMMON=( + -std=c11 + -DEL_TARGET_LINUX + -I "${EL_RUNTIME}" +) + +clean() { + rm -rf "${BUILD_DIR}" + echo "Cleaned." +} + +compile() { + mkdir -p "${BUILD_DIR}" + + # ── 1. Vessel forward-declarations header ───────────────────────────────── + # elc --emit-header emits full function bodies, not just prototypes. + # Extract only the forward-declaration lines: stop at the first function + # definition (a line that ends with ') {' opening a body block). + # macOS awk does not support \s — use POSIX [[:space:]] instead. + echo "==> Generating el-native vessel header (declarations only)..." + "${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + > "${BUILD_DIR}/el_native_vessel.h" + + # ── 2. Compile el-native vessel to C ────────────────────────────────────── + echo "==> Compiling el-native vessel to C..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${BUILD_DIR}/el_native_vessel.c" + + # ── 3. Compile app entry to C ───────────────────────────────────────────── + echo "==> Compiling app entry to C..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" \ + > "${BUILD_DIR}/native_hello.c" + + # ── 4. Compile el_gtk4.c (plain C, GTK4 backend) ───────────────────────── + echo "==> Compiling el_gtk4.c (GTK4 bridge)..." + # shellcheck disable=SC2086 + clang \ + -std=c11 \ + -DEL_TARGET_LINUX \ + -I "${EL_RUNTIME}" \ + ${GTK4_CFLAGS} \ + -c "${EL_RUNTIME}/el_gtk4.c" \ + -o "${BUILD_DIR}/el_gtk4.o" + + # ── 5a. Compile el_seed.c ───────────────────────────────────────────────── + # el_seed.c provides the OS-boundary __-prefixed primitives + thin wrappers. + # With -DEL_TARGET_LINUX, the EL_TARGET_LINUX section in el_seed.c is + # compiled in (GTK4 forwarders) and the EL_TARGET_MACOS section is excluded. + echo "==> Compiling el_seed.c (EL_TARGET_LINUX)..." + # shellcheck disable=SC2086 + clang "${CLANG_FLAGS_COMMON[@]}" \ + ${GTK4_CFLAGS} \ + -c "${EL_RUNTIME}/el_seed.c" \ + -o "${BUILD_DIR}/el_seed.o" + + # ── 5b. Compile el_runtime.c and patch out duplicate __-prefixed symbols ── + # Same strategy as build.sh: el_runtime.c has duplicates of the __-prefixed + # OS wrappers that el_seed.c defines. Hide el_runtime.o's copies with nmedit. + echo "==> Compiling el_runtime.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_runtime.c" \ + -o "${BUILD_DIR}/el_runtime.o" + + echo "==> Patching el_runtime.o (hiding __-prefixed symbols duplicated in el_seed.o)..." + nm "${BUILD_DIR}/el_seed.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.seed_T.txt" + nm "${BUILD_DIR}/el_runtime.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.rt_T.txt" + comm -23 "${BUILD_DIR}/.rt_T.txt" "${BUILD_DIR}/.seed_T.txt" \ + > "${BUILD_DIR}/.rt_keep.txt" + nmedit -s "${BUILD_DIR}/.rt_keep.txt" "${BUILD_DIR}/el_runtime.o" + + # ── 6. Compile el-native vessel object ──────────────────────────────────── + # Vessel code calls __-prefixed builtins. el_native_target.h provides the + # EL_TARGET_LINUX block of declarations (same function signatures as MACOS). + echo "==> Compiling el-native vessel object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -c "${BUILD_DIR}/el_native_vessel.c" \ + -o "${BUILD_DIR}/el_native_vessel.o" + + echo "==> Patching el_native_vessel.o (hiding vessel main)..." + nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' \ + | grep -v '^_main$' \ + > "${BUILD_DIR}/.vessel_keep.txt" + nmedit -s "${BUILD_DIR}/.vessel_keep.txt" "${BUILD_DIR}/el_native_vessel.o" + + # ── 7. Compile app object ───────────────────────────────────────────────── + # Inject el_native_target.h (float ops + EL_TARGET_LINUX native decls) and + # the vessel's forward-declaration header. + echo "==> Compiling app object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -include "${BUILD_DIR}/el_native_vessel.h" \ + -c "${BUILD_DIR}/native_hello.c" \ + -o "${BUILD_DIR}/native_hello.o" + + # ── 8. Link ─────────────────────────────────────────────────────────────── + # el_seed.o = OS-boundary primitives (__-prefixed) + GTK4 forwarders + # el_runtime.o = high-level builtins (str_len, json_*, etc.), patched + # el_native_vessel.o = el-level widget API, patched + # el_gtk4.o = GTK4 C bridge + # GTK4_LIBS = $(pkg-config --libs gtk4) + # -ldl = dlsym for callback dispatch + # -lpthread = threading + echo "==> Linking native-hello-gtk4..." + # shellcheck disable=SC2086 + clang "${CLANG_FLAGS_COMMON[@]}" \ + "${BUILD_DIR}/native_hello.o" \ + "${BUILD_DIR}/el_native_vessel.o" \ + "${BUILD_DIR}/el_gtk4.o" \ + "${BUILD_DIR}/el_runtime.o" \ + "${BUILD_DIR}/el_seed.o" \ + ${GTK4_LIBS} \ + -ldl \ + -lpthread \ + -lcurl \ + -o "${BUILD_DIR}/native-hello-gtk4" + + echo "==> Build complete: ${BUILD_DIR}/native-hello-gtk4" +} + +run() { + compile + echo "==> Launching native-hello-gtk4..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${BUILD_DIR}/native-hello-gtk4" +} + +case "${1:-run}" in + clean) clean ;; + compile) compile ;; + run|*) run ;; +esac diff --git a/ui/examples/native-hello/build-gtk4/.rt_T.txt b/ui/examples/native-hello/build-gtk4/.rt_T.txt new file mode 100644 index 0000000..7657b58 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.rt_T.txt @@ -0,0 +1,397 @@ +___args_json +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___http_conn_fd +___http_do +___http_do_map +___http_do_map_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_raw +___json_parse_map +___json_set +___json_stringify_val +___mutex_lock +___mutex_new +___mutex_unlock +___print +___print_raw +___println +___read_n +___readline +___sha256_hex +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___thread_create +___thread_join +___url_decode +___url_encode +___uuid_v4 +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_request_end +_el_request_start +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build-gtk4/.rt_keep.txt b/ui/examples/native-hello/build-gtk4/.rt_keep.txt new file mode 100644 index 0000000..6aa9058 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.rt_keep.txt @@ -0,0 +1,340 @@ +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___http_do_map +___http_do_map_to_file +___print_raw +___read_n +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build-gtk4/.seed_T.txt b/ui/examples/native-hello/build-gtk4/.seed_T.txt new file mode 100644 index 0000000..260f1fe --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.seed_T.txt @@ -0,0 +1,137 @@ +___args_json +___button_create +___cos_f +___engram_activate +___engram_activate_json +___engram_add_layer +___engram_compile_layered_json +___engram_connect +___engram_edge_between +___engram_edge_count +___engram_forget +___engram_get_node +___engram_get_node_json +___engram_list_layers +___engram_list_layers_json +___engram_load +___engram_neighbors +___engram_neighbors_filtered +___engram_neighbors_json +___engram_node +___engram_node_count +___engram_node_full +___engram_node_layered +___engram_remove_layer +___engram_save +___engram_scan_nodes +___engram_scan_nodes_by_type_json +___engram_scan_nodes_json +___engram_search +___engram_search_json +___engram_stats_json +___engram_strengthen +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___hstack_create +___html_sanitize +___http_conn_fd +___http_do +___http_do_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___image_create +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_bool +___json_get_float +___json_get_int +___json_get_raw +___json_get_string +___json_parse +___json_parse_map +___json_set +___json_stringify +___json_stringify_val +___label_create +___ln_f +___log_f +___manifest_read +___mutex_lock +___mutex_new +___mutex_unlock +___native_init +___native_run_loop +___pi_f +___print +___println +___readline +___scroll_create +___sha256_hex +___sin_f +___sleep_ms +___sqrt_f +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___text_area_create +___text_field_create +___thread_create +___thread_join +___time_now_ns +___url_decode +___url_encode +___uuid_v4 +___vstack_create +___widget_add_child +___widget_destroy +___widget_get_text +___widget_on_change +___widget_on_click +___widget_on_submit +___widget_remove_child +___widget_set_bg_color +___widget_set_color +___widget_set_corner_radius +___widget_set_disabled +___widget_set_flex +___widget_set_font +___widget_set_height +___widget_set_hidden +___widget_set_padding +___widget_set_text +___widget_set_width +___window_create +___window_set_title +___window_show +___zstack_create +_el_request_end +_el_request_start +_el_seed_init_args +_el_seed_set_http_conn_fd diff --git a/ui/examples/native-hello/build-gtk4/.vessel_keep.txt b/ui/examples/native-hello/build-gtk4/.vessel_keep.txt new file mode 100644 index 0000000..a8a7964 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.vessel_keep.txt @@ -0,0 +1,55 @@ +_button +_color_hex_a +_color_hex_b +_color_hex_g +_color_hex_r +_hex_channel +_hex_nibble +_hstack +_image +_label +_manifest_height +_manifest_min_height +_manifest_min_width +_manifest_read +_manifest_title +_manifest_width +_native_init +_native_run_loop +_scroll +_style_button_primary +_style_label_body +_style_label_heading +_style_label_muted +_style_surface +_text_area +_text_field +_vstack +_vstack_tight +_widget_add_child +_widget_destroy +_widget_get_text +_widget_on_change +_widget_on_click +_widget_on_submit +_widget_remove_child +_widget_set_bg_color +_widget_set_bg_color_hex +_widget_set_color +_widget_set_color_hex +_widget_set_corner_radius +_widget_set_disabled +_widget_set_flex +_widget_set_font +_widget_set_height +_widget_set_hidden +_widget_set_padding +_widget_set_padding_all +_widget_set_padding_xy +_widget_set_text +_widget_set_width +_window_create +_window_from_manifest +_window_set_title +_window_show +_zstack diff --git a/ui/examples/native-hello/build-gtk4/el_gtk4.o b/ui/examples/native-hello/build-gtk4/el_gtk4.o new file mode 100644 index 0000000000000000000000000000000000000000..34d1efcf173a8d21aa8c484458cb3957559902f2 GIT binary patch literal 33464 zcmeI54Rlr4b?2{mV1x}Zj&Y4^405n**+vddNsJS2{Tz`7*RNB?r7bgtJcJ~WijagP zY`M@GwIU6%X^T^)Wm?*Zm`;U zp1b(j@3<~l_G|PhrHG<>rQtdFl*hS%empmd^7+2j{yxvs?|gp4M>cLKhQr&)_xLyE zrz}UGj-s#YWp*^+IsSJ(zqPkFLCELVKDpt^A|c38UyvY*GDWtpa3Y+~H~04Sw0CYd zT3Ws!*9T1(7qW@Oyjt}OMDpv}+1=FKm*3s_e0yh0KA&4#VDfVL2_G7o=W+3ey?n24 zjiT@Oq>?P(2W>GwzWDE?Kz?`>N%YmNQS^skZBG=PTo^^S@V=fOJ*d$A)PXzrp@?tz zxiJ|1(e*tw$FDa=FW1t{wKdtXwkTSKJd5t&r#>2ZhN>K_uZhOU{2)Is*FZaZdf6!N zhko^RepKnGz}2WGdzEwt?{KC&*=Wzgnqm)pr*4P~0KvT~dd^8tO0y{nfZS9Bo`S`kBnK(N|6{9X&R< zbhM7NE?s8njFmAqF*|y{D|@!jbMj7K?};cH5q$_9D9t2#pr0+OrEJM1-Zc8SbZ7P| zI=DLAm>+fT^P;igjJ+@5{q)B?{WjhQ?fnkL`Pht$zaYALkvu5FwWrE@H+f5+7nesb zPoq=iJ$<{+clu7B=k(n^zv*Q@FZd;SIN2%V`ks%_#x-8|jnN-X&W*-oN7P}=>M&M2 zC%fy#Inf3m9vGW%cv_l8YplaXmIHa zb}?h^=lj|1p66^alb@+2Zm>se%Y~CCJJ*)j25zPG7OqFrxVn>gAXp4MP`8$wpmYl!R8!&iEht^TF{>FPxvbLG