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 0000000..033e24c Binary files /dev/null and b/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.properties b/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a363877 --- /dev/null +++ b/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ui/examples/native-hello-android/gradlew b/ui/examples/native-hello-android/gradlew new file mode 100755 index 0000000..ba00a16 --- /dev/null +++ b/ui/examples/native-hello-android/gradlew @@ -0,0 +1,122 @@ +#!/bin/sh +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Gradle start up script for POSIX generated by "gradle init". +# + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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 0000000..34d1efc Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/el_gtk4.o differ 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 0000000..3a45efd Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/el_native_vessel.o differ diff --git a/ui/examples/native-hello/build-gtk4/el_runtime.o b/ui/examples/native-hello/build-gtk4/el_runtime.o new file mode 100644 index 0000000..1ea0c85 Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/el_runtime.o differ 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 0000000..a9a2678 Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/el_seed.o differ 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 0000000..fc502d8 Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/native-hello-gtk4 differ diff --git a/ui/examples/native-hello/build-gtk4/native_hello.c b/ui/examples/native-hello/build-gtk4/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/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-gtk4/native_hello.o b/ui/examples/native-hello/build-gtk4/native_hello.o new file mode 100644 index 0000000..c4de3f0 Binary files /dev/null and b/ui/examples/native-hello/build-gtk4/native_hello.o differ diff --git a/ui/examples/native-hello/build-sdl2.sh b/ui/examples/native-hello/build-sdl2.sh new file mode 100755 index 0000000..99cdc3a --- /dev/null +++ b/ui/examples/native-hello/build-sdl2.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# build-sdl2.sh — Compile and run native-hello on macOS using the SDL2 backend. +# +# Usage: +# ./build-sdl2.sh # compile and run +# ./build-sdl2.sh compile # compile only +# ./build-sdl2.sh clean # remove build artefacts +# +# Requirements: +# - elc in PATH (or ../../../lang/dist/platform/elc) +# - SDL2, SDL2_ttf, SDL2_image via Homebrew (brew install sdl2 sdl2_ttf sdl2_image) +# - clang (Xcode Command Line Tools) +# - macOS 12+ +# +# This is the SDL2 counterpart to build.sh (AppKit). The pipeline is identical; +# only the platform bridge object and compile flags differ: +# - el_sdl2.c instead of el_appkit.m +# - -DEL_TARGET_SDL2 instead of -DEL_TARGET_MACOS +# - sdl2-config --libs -lSDL2_ttf -lSDL2_image -ldl instead of -framework Cocoa + +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" + +# 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 + +# 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 0000000..cd65d2c Binary files /dev/null and b/ui/examples/native-hello/build-win32/el_native_vessel.o differ diff --git a/ui/examples/native-hello/build-win32/el_runtime_win32.o b/ui/examples/native-hello/build-win32/el_runtime_win32.o new file mode 100644 index 0000000..d6dc828 Binary files /dev/null and b/ui/examples/native-hello/build-win32/el_runtime_win32.o differ 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 0000000..87e7214 Binary files /dev/null and b/ui/examples/native-hello/build-win32/el_win32.o differ 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 0000000..2a1d6fd Binary files /dev/null and b/ui/examples/native-hello/build-win32/native-hello.exe differ diff --git a/ui/examples/native-hello/build-win32/native_hello.c b/ui/examples/native-hello/build-win32/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello/build-win32/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-win32/native_hello.o b/ui/examples/native-hello/build-win32/native_hello.o new file mode 100644 index 0000000..55a081b Binary files /dev/null and b/ui/examples/native-hello/build-win32/native_hello.o differ diff --git a/ui/examples/native-hello/build.sh b/ui/examples/native-hello/build.sh new file mode 100755 index 0000000..f53a78a --- /dev/null +++ b/ui/examples/native-hello/build.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# build.sh — Compile and run native-hello on macOS. +# +# Usage: +# ./build.sh # compile and run +# ./build.sh compile # compile only +# ./build.sh clean # remove build artefacts +# ./build.sh platforms # detect available platform bridges on this machine +# +# Requirements: +# - elc in PATH (or ../../../lang/dist/platform/elc) +# - clang with Xcode Command Line Tools (for Cocoa framework) +# - macOS 12+ +# +# Multi-file compilation strategy: +# elc does not resolve cross-directory imports (../../vessels/...) at bundle +# time. The el-native vessel is compiled as a separate translation unit. +# +# el_seed.c and el_runtime.c MUST both be linked: +# - el_seed.c = OS-boundary layer (__-prefixed primitives) + thin wrappers +# - el_runtime.c = high-level el builtins (str_len, json_get_string, etc.) +# They share many __-prefixed symbols. The macOS linker rejects duplicate +# definitions, so we use nmedit to hide the el_runtime.c copies of the __ +# symbols (keeping el_seed.c's versions canonical). +# +# el_native_target.h is a conflict-free header that adds: +# - float_div / float_mul etc. (missing from current el_runtime.h) +# - Native widget builtin declarations (#ifdef EL_TARGET_MACOS) +# without redefining el_to_float / el_from_float from el_runtime.h. +# +# Vessel header generation: elc --emit-header includes function BODIES. +# We extract only the forward declarations using awk (stop at first body). +# NOTE: macOS awk does not support \s — use [[:space:]] instead. +# +# Vessel main(): the vessel's compiled .c file has its own main(). We use +# nmedit to hide it from the vessel object before linking. + +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" + +# 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 + +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 0000000..4f19980 Binary files /dev/null and b/ui/examples/native-hello/build/el_appkit.o differ diff --git a/ui/examples/native-hello/build/el_native_vessel.c b/ui/examples/native-hello/build/el_native_vessel.c new file mode 100644 index 0000000..28bf38c --- /dev/null +++ b/ui/examples/native-hello/build/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/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 0000000..3a45efd Binary files /dev/null and b/ui/examples/native-hello/build/el_native_vessel.o differ diff --git a/ui/examples/native-hello/build/el_runtime.o b/ui/examples/native-hello/build/el_runtime.o new file mode 100644 index 0000000..1ea0c85 Binary files /dev/null and b/ui/examples/native-hello/build/el_runtime.o differ 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 0000000..c485e5e Binary files /dev/null and b/ui/examples/native-hello/build/el_sdl2.o differ 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 0000000..7abc102 Binary files /dev/null and b/ui/examples/native-hello/build/el_seed.o differ diff --git a/ui/examples/native-hello/build/native-hello b/ui/examples/native-hello/build/native-hello new file mode 100755 index 0000000..cf0ddb3 Binary files /dev/null and b/ui/examples/native-hello/build/native-hello differ diff --git a/ui/examples/native-hello/build/native-hello-linux-arm64 b/ui/examples/native-hello/build/native-hello-linux-arm64 new file mode 100755 index 0000000..bb26b3d Binary files /dev/null and b/ui/examples/native-hello/build/native-hello-linux-arm64 differ diff --git a/ui/examples/native-hello/build/native-hello-linux-x86_64 b/ui/examples/native-hello/build/native-hello-linux-x86_64 new file mode 100755 index 0000000..07628d3 Binary files /dev/null and b/ui/examples/native-hello/build/native-hello-linux-x86_64 differ diff --git a/ui/examples/native-hello/build/native-hello-pi-arm64 b/ui/examples/native-hello/build/native-hello-pi-arm64 new file mode 100755 index 0000000..bb26b3d Binary files /dev/null and b/ui/examples/native-hello/build/native-hello-pi-arm64 differ diff --git a/ui/examples/native-hello/build/native-hello-sdl2 b/ui/examples/native-hello/build/native-hello-sdl2 new file mode 100755 index 0000000..028bba8 Binary files /dev/null and b/ui/examples/native-hello/build/native-hello-sdl2 differ 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 0000000..c4de3f0 Binary files /dev/null and b/ui/examples/native-hello/build/native_hello.o differ diff --git a/ui/examples/native-hello/docker-build-linux.sh b/ui/examples/native-hello/docker-build-linux.sh new file mode 100755 index 0000000..46f2214 --- /dev/null +++ b/ui/examples/native-hello/docker-build-linux.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# docker-build-linux.sh — Build native-hello for Linux using Docker. +# +# Usage: +# ./docker-build-linux.sh # build ARM64 (native on Apple Silicon) +# ./docker-build-linux.sh --x86 # also build x86_64 via QEMU +# ./docker-build-linux.sh --x86-only # only build x86_64 +# +# Output binaries are placed in ./build/: +# native-hello-linux-arm64 — Linux ARM64 (Raspberry Pi 3/4/5, Apple Silicon VMs) +# native-hello-linux-x86_64 — Linux x86_64 (desktop Linux, CI servers) +# +# Prerequisites: +# - Docker (Rancher Desktop / Docker Desktop) +# - elc at ../../../lang/dist/platform/elc or in PATH +# +# How it works: +# 1. elc generates C sources from the el program (macOS, ARM64 native elc) +# 2. Docker builds those C sources inside Ubuntu 22.04 + GTK4 +# 3. The resulting ELF binary is extracted from the image +# +# Linux toolchain notes: +# - GNU ld rejects duplicate symbol definitions that macOS ld accepts silently. +# The Dockerfile uses objcopy --localize-symbol to hide duplicates between +# el_seed.o / el_runtime.o and el_native_vessel.o / native_hello.o. +# - el_gtk4.c requires _GNU_SOURCE for RTLD_DEFAULT (dlfcn.h). +# - G_APPLICATION_DEFAULT_FLAGS was added in GLib 2.74; Ubuntu 22.04 ships +# GLib 2.72, so the Dockerfile adds a compatibility define. + +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" +DOCKER_CTX="${SCRIPT_DIR}/build-docker" + +BUILD_X86=0 +BUILD_ARM64=1 + +for arg in "$@"; do + case "${arg}" in + --x86) BUILD_X86=1 ;; + --x86-only) BUILD_X86=1; BUILD_ARM64=0 ;; + esac +done + +# ── 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 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 { + +