From 688b8508fbe376e32caf1747b49fc00ee485183f Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 1 Jul 2026 11:21:23 -0500 Subject: [PATCH] feat(runtime): native platform backends and UI vessels onto main --- elp/src/elp.elh | 6 +- elp/src/grammar.elh | 56 +- elp/src/realizer.elh | 10 +- elp/src/semantics.elh | 30 +- engram/src/server.el | 109 +- lang/el-compiler/runtime/ElBridge.java | 711 + .../runtime/PLATFORM_BRIDGE_SPEC.md | 554 + lang/el-compiler/runtime/detect-platforms | 208 + lang/el-compiler/runtime/el_android.c | 949 ++ lang/el-compiler/runtime/el_appkit.m | 1018 ++ lang/el-compiler/runtime/el_gtk4.c | 1327 ++ lang/el-compiler/runtime/el_lvgl.c | 844 ++ lang/el-compiler/runtime/el_native_target.h | 574 + lang/el-compiler/runtime/el_runtime_win32.c | 1146 ++ lang/el-compiler/runtime/el_sdl2.c | 1360 ++ lang/el-compiler/runtime/el_uikit.m | 1118 ++ lang/el-compiler/runtime/el_win32.c | 1363 ++ lang/el-compiler/runtime/new-platform | 711 + ui/examples/native-hello-android/.gitignore | 6 + .../native-hello-android/app/build.gradle | 55 + .../app/proguard-rules.pro | 4 + .../app/src/main/AndroidManifest.xml | 23 + .../src/main/java/com/neuron/el/ElApp.java | 18 + .../src/main/java/com/neuron/el/ElBridge.java | 711 + .../main/java/com/neuron/el/MainActivity.java | 38 + .../app/src/main/jni/CMakeLists.txt | 57 + .../app/src/main/jni/el_android.c | 964 ++ .../app/src/main/jni/el_native_target.h | 573 + .../app/src/main/jni/el_native_vessel.c | 459 + .../app/src/main/jni/el_runtime.c | 11733 ++++++++++++++++ .../app/src/main/jni/el_runtime.h | 883 ++ .../app/src/main/jni/el_seed.c | 1942 +++ .../app/src/main/jni/native_hello.c | 123 + .../app/src/main/res/layout/activity_main.xml | 13 + .../app/src/main/res/values/strings.xml | 4 + ui/examples/native-hello-android/build.gradle | 4 + ui/examples/native-hello-android/build.sh | 83 + .../native-hello-android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + ui/examples/native-hello-android/gradlew | 122 + .../local.properties.template | 7 + .../native-hello-android/settings.gradle | 17 + ui/examples/native-hello-ios/.gitignore | 4 + .../NativeHello.xcodeproj/project.pbxproj | 377 + .../xcschemes/NativeHello.xcscheme | 78 + .../native-hello-ios/NativeHello/Info.plist | 38 + .../NativeHello/el_native_target.h | 573 + .../NativeHello/el_native_vessel.c | 459 + .../native-hello-ios/NativeHello/el_runtime.c | 11733 ++++++++++++++++ .../native-hello-ios/NativeHello/el_runtime.h | 883 ++ .../native-hello-ios/NativeHello/el_seed.c | 1942 +++ .../native-hello-ios/NativeHello/el_uikit.m | 1118 ++ .../native-hello-ios/NativeHello/main.m | 48 + .../NativeHello/native_hello.c | 123 + ui/examples/native-hello-ios/build.sh | 136 + ui/examples/native-hello/-o | 0 .../native-hello/Dockerfile.linux-gtk4 | 88 + ui/examples/native-hello/Dockerfile.pi | 99 + .../build-docker/el_native_vessel.c | 459 + .../build-docker/el_native_vessel.h | 69 + .../native-hello/build-docker/native_hello.c | 123 + .../build-docker/runtime/el_gtk4.c | 1027 ++ .../build-docker/runtime/el_native_target.h | 573 + .../build-docker/runtime/el_runtime.c | 11733 ++++++++++++++++ .../build-docker/runtime/el_runtime.h | 883 ++ .../build-docker/runtime/el_seed.c | 1942 +++ .../build-docker/runtime/el_seed.h | 324 + ui/examples/native-hello/build-gtk4.sh | 182 + ui/examples/native-hello/build-gtk4/.rt_T.txt | 397 + .../native-hello/build-gtk4/.rt_keep.txt | 340 + .../native-hello/build-gtk4/.seed_T.txt | 137 + .../native-hello/build-gtk4/.vessel_keep.txt | 55 + ui/examples/native-hello/build-gtk4/el_gtk4.o | Bin 0 -> 33464 bytes .../build-gtk4/el_native_vessel.c | 459 + .../build-gtk4/el_native_vessel.h | 69 + .../build-gtk4/el_native_vessel.o | Bin 0 -> 14692 bytes .../native-hello/build-gtk4/el_runtime.o | Bin 0 -> 244380 bytes ui/examples/native-hello/build-gtk4/el_seed.o | Bin 0 -> 50056 bytes .../native-hello/build-gtk4/native-hello-gtk4 | Bin 0 -> 303136 bytes .../native-hello/build-gtk4/native_hello.c | 123 + .../native-hello/build-gtk4/native_hello.o | Bin 0 -> 6192 bytes ui/examples/native-hello/build-sdl2.sh | 169 + ui/examples/native-hello/build-win32.sh | 135 + .../build-win32/el_native_vessel.c | 459 + .../build-win32/el_native_vessel.h | 69 + .../build-win32/el_native_vessel.o | Bin 0 -> 14736 bytes .../build-win32/el_runtime_win32.o | Bin 0 -> 43103 bytes .../native-hello/build-win32/el_win32.o | Bin 0 -> 31904 bytes .../native-hello/build-win32/native-hello.exe | Bin 0 -> 246847 bytes .../native-hello/build-win32/native_hello.c | 123 + .../native-hello/build-win32/native_hello.o | Bin 0 -> 5580 bytes ui/examples/native-hello/build.sh | 198 + ui/examples/native-hello/build/.rt_T.txt | 397 + ui/examples/native-hello/build/.rt_keep.txt | 340 + ui/examples/native-hello/build/.seed_T.txt | 137 + .../native-hello/build/.vessel_keep.txt | 55 + ui/examples/native-hello/build/el_appkit.o | Bin 0 -> 58520 bytes .../native-hello/build/el_native_vessel.c | 459 + .../native-hello/build/el_native_vessel.h | 69 + .../native-hello/build/el_native_vessel.o | Bin 0 -> 14692 bytes ui/examples/native-hello/build/el_runtime.o | Bin 0 -> 244380 bytes ui/examples/native-hello/build/el_sdl2.o | Bin 0 -> 31624 bytes ui/examples/native-hello/build/el_seed.o | Bin 0 -> 50056 bytes ui/examples/native-hello/build/native-hello | Bin 0 -> 324000 bytes .../build/native-hello-linux-arm64 | Bin 0 -> 300664 bytes .../build/native-hello-linux-x86_64 | Bin 0 -> 299048 bytes .../native-hello/build/native-hello-pi-arm64 | Bin 0 -> 300664 bytes .../native-hello/build/native-hello-sdl2 | Bin 0 -> 297632 bytes ui/examples/native-hello/build/native_hello.c | 123 + ui/examples/native-hello/build/native_hello.o | Bin 0 -> 6192 bytes .../native-hello/docker-build-linux.sh | 128 + ui/examples/native-hello/manifest.el | 19 + ui/examples/native-hello/src/App.el | 141 + ui/examples/native-hello/src/main.el | 43 + ui/examples/profile-card/manifest.el | 25 + ui/tools/dist/el-ui-native-codegen | 4 + ui/tools/native-codegen/build.sh | 62 + .../native-codegen/el_ui_native_codegen.py | 822 ++ ui/tools/native-codegen/test_app.el | 28 + ui/tools/native-codegen/test_native_hello.el | 52 + ui/vessels/el-native/src/main.el | 386 + ui/vessels/el-native/src/main.elh | 57 + 123 files changed, 70937 insertions(+), 99 deletions(-) create mode 100644 lang/el-compiler/runtime/ElBridge.java create mode 100644 lang/el-compiler/runtime/PLATFORM_BRIDGE_SPEC.md create mode 100755 lang/el-compiler/runtime/detect-platforms create mode 100644 lang/el-compiler/runtime/el_android.c create mode 100644 lang/el-compiler/runtime/el_appkit.m create mode 100644 lang/el-compiler/runtime/el_gtk4.c create mode 100644 lang/el-compiler/runtime/el_lvgl.c create mode 100644 lang/el-compiler/runtime/el_native_target.h create mode 100644 lang/el-compiler/runtime/el_runtime_win32.c create mode 100644 lang/el-compiler/runtime/el_sdl2.c create mode 100644 lang/el-compiler/runtime/el_uikit.m create mode 100644 lang/el-compiler/runtime/el_win32.c create mode 100755 lang/el-compiler/runtime/new-platform create mode 100644 ui/examples/native-hello-android/.gitignore create mode 100644 ui/examples/native-hello-android/app/build.gradle create mode 100644 ui/examples/native-hello-android/app/proguard-rules.pro create mode 100644 ui/examples/native-hello-android/app/src/main/AndroidManifest.xml create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java create mode 100644 ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java create mode 100644 ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_android.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_native_target.h create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_runtime.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_runtime.h create mode 100644 ui/examples/native-hello-android/app/src/main/jni/el_seed.c create mode 100644 ui/examples/native-hello-android/app/src/main/jni/native_hello.c create mode 100644 ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml create mode 100644 ui/examples/native-hello-android/app/src/main/res/values/strings.xml create mode 100644 ui/examples/native-hello-android/build.gradle create mode 100755 ui/examples/native-hello-android/build.sh create mode 100644 ui/examples/native-hello-android/gradle.properties create mode 100644 ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.properties create mode 100755 ui/examples/native-hello-android/gradlew create mode 100644 ui/examples/native-hello-android/local.properties.template create mode 100644 ui/examples/native-hello-android/settings.gradle create mode 100644 ui/examples/native-hello-ios/.gitignore create mode 100644 ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj create mode 100644 ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme create mode 100644 ui/examples/native-hello-ios/NativeHello/Info.plist create mode 100644 ui/examples/native-hello-ios/NativeHello/el_native_target.h create mode 100644 ui/examples/native-hello-ios/NativeHello/el_native_vessel.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_runtime.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_runtime.h create mode 100644 ui/examples/native-hello-ios/NativeHello/el_seed.c create mode 100644 ui/examples/native-hello-ios/NativeHello/el_uikit.m create mode 100644 ui/examples/native-hello-ios/NativeHello/main.m create mode 100644 ui/examples/native-hello-ios/NativeHello/native_hello.c create mode 100755 ui/examples/native-hello-ios/build.sh create mode 100644 ui/examples/native-hello/-o create mode 100644 ui/examples/native-hello/Dockerfile.linux-gtk4 create mode 100644 ui/examples/native-hello/Dockerfile.pi create mode 100644 ui/examples/native-hello/build-docker/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-docker/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-docker/native_hello.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_gtk4.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_native_target.h create mode 100644 ui/examples/native-hello/build-docker/runtime/el_runtime.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_runtime.h create mode 100644 ui/examples/native-hello/build-docker/runtime/el_seed.c create mode 100644 ui/examples/native-hello/build-docker/runtime/el_seed.h create mode 100755 ui/examples/native-hello/build-gtk4.sh create mode 100644 ui/examples/native-hello/build-gtk4/.rt_T.txt create mode 100644 ui/examples/native-hello/build-gtk4/.rt_keep.txt create mode 100644 ui/examples/native-hello/build-gtk4/.seed_T.txt create mode 100644 ui/examples/native-hello/build-gtk4/.vessel_keep.txt create mode 100644 ui/examples/native-hello/build-gtk4/el_gtk4.o create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-gtk4/el_native_vessel.o create mode 100644 ui/examples/native-hello/build-gtk4/el_runtime.o create mode 100644 ui/examples/native-hello/build-gtk4/el_seed.o create mode 100755 ui/examples/native-hello/build-gtk4/native-hello-gtk4 create mode 100644 ui/examples/native-hello/build-gtk4/native_hello.c create mode 100644 ui/examples/native-hello/build-gtk4/native_hello.o create mode 100755 ui/examples/native-hello/build-sdl2.sh create mode 100755 ui/examples/native-hello/build-win32.sh create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.c create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.h create mode 100644 ui/examples/native-hello/build-win32/el_native_vessel.o create mode 100644 ui/examples/native-hello/build-win32/el_runtime_win32.o create mode 100644 ui/examples/native-hello/build-win32/el_win32.o create mode 100755 ui/examples/native-hello/build-win32/native-hello.exe create mode 100644 ui/examples/native-hello/build-win32/native_hello.c create mode 100644 ui/examples/native-hello/build-win32/native_hello.o create mode 100755 ui/examples/native-hello/build.sh create mode 100644 ui/examples/native-hello/build/.rt_T.txt create mode 100644 ui/examples/native-hello/build/.rt_keep.txt create mode 100644 ui/examples/native-hello/build/.seed_T.txt create mode 100644 ui/examples/native-hello/build/.vessel_keep.txt create mode 100644 ui/examples/native-hello/build/el_appkit.o create mode 100644 ui/examples/native-hello/build/el_native_vessel.c create mode 100644 ui/examples/native-hello/build/el_native_vessel.h create mode 100644 ui/examples/native-hello/build/el_native_vessel.o create mode 100644 ui/examples/native-hello/build/el_runtime.o create mode 100644 ui/examples/native-hello/build/el_sdl2.o create mode 100644 ui/examples/native-hello/build/el_seed.o create mode 100755 ui/examples/native-hello/build/native-hello create mode 100755 ui/examples/native-hello/build/native-hello-linux-arm64 create mode 100755 ui/examples/native-hello/build/native-hello-linux-x86_64 create mode 100755 ui/examples/native-hello/build/native-hello-pi-arm64 create mode 100755 ui/examples/native-hello/build/native-hello-sdl2 create mode 100644 ui/examples/native-hello/build/native_hello.c create mode 100644 ui/examples/native-hello/build/native_hello.o create mode 100755 ui/examples/native-hello/docker-build-linux.sh create mode 100644 ui/examples/native-hello/manifest.el create mode 100644 ui/examples/native-hello/src/App.el create mode 100644 ui/examples/native-hello/src/main.el create mode 100644 ui/examples/profile-card/manifest.el create mode 100755 ui/tools/dist/el-ui-native-codegen create mode 100755 ui/tools/native-codegen/build.sh create mode 100644 ui/tools/native-codegen/el_ui_native_codegen.py create mode 100644 ui/tools/native-codegen/test_app.el create mode 100644 ui/tools/native-codegen/test_native_hello.el create mode 100644 ui/vessels/el-native/src/main.el create mode 100644 ui/vessels/el-native/src/main.elh diff --git a/elp/src/elp.elh b/elp/src/elp.elh index ab8a621..f9d2323 100644 --- a/elp/src/elp.elh +++ b/elp/src/elp.elh @@ -1,7 +1,7 @@ // auto-generated by elc --emit-header — do not edit extern fn sem_get(json: String, key: String) -> String -extern fn generate_frame(frame: Any) -> String -extern fn generate_frame_lang(frame: Any, lang_code: String) -> String -extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> Any +extern fn generate_frame(frame: [String]) -> String +extern fn generate_frame_lang(frame: [String], lang_code: String) -> String +extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String] extern fn generate(semantic_form_json: String) -> String extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String diff --git a/elp/src/grammar.elh b/elp/src/grammar.elh index ef8cf80..f678627 100644 --- a/elp/src/grammar.elh +++ b/elp/src/grammar.elh @@ -1,22 +1,22 @@ -// auto-generated by elc --emit-header - do not edit -extern fn slots_get(slots: Any, key: String) -> String -extern fn slots_set(slots: Any, key: String, val: String) -> Any -extern fn make_slots(k0: String, v0: String) -> Any -extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> Any -extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> Any -extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> Any -extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> Any -extern fn rule_id(rule: Any) -> String -extern fn rule_lhs(rule: Any) -> String -extern fn rule_rhs_len(rule: Any) -> Int -extern fn rule_rhs(rule: Any, idx: Int) -> String -extern fn make_rule(id: String, lhs: String, r0: String) -> Any -extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> Any -extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> Any -extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> Any -extern fn build_rules() -> Any -extern fn get_rules() -> Any -extern fn find_rule(rule_id_str: String) -> Any +// auto-generated by elc --emit-header — do not edit +extern fn slots_get(slots: [String], key: String) -> String +extern fn slots_set(slots: [String], key: String, val: String) -> [String] +extern fn make_slots(k0: String, v0: String) -> [String] +extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] +extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] +extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] +extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] +extern fn rule_id(rule: [String]) -> String +extern fn rule_lhs(rule: [String]) -> String +extern fn rule_rhs_len(rule: [String]) -> Int +extern fn rule_rhs(rule: [String], idx: Int) -> String +extern fn make_rule(id: String, lhs: String, r0: String) -> [String] +extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] +extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] +extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] +extern fn build_rules() -> [[String]] +extern fn get_rules() -> [[String]] +extern fn find_rule(rule_id_str: String) -> [String] extern fn make_leaf(label: String, word: String) -> String extern fn make_node1(label: String, child0: String) -> String extern fn make_node2(label: String, child0: String, child1: String) -> String @@ -24,15 +24,15 @@ extern fn make_node3(label: String, child0: String, child1: String, child2: Stri extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String extern fn nlg_is_ws(c: String) -> Bool extern fn skip_ws(s: String, pos: Int) -> Int -extern fn scan_token(s: String, start: Int) -> Any +extern fn scan_token(s: String, start: Int) -> [String] extern fn render_tree(tree: String) -> String -extern fn gram_word_order(profile: Any) -> String -extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: Any) -> String -extern fn gram_build_vp(verb: String, aux: String, profile: Any) -> String -extern fn gram_question_strategy(profile: Any) -> String +extern fn gram_word_order(profile: [String]) -> String +extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String +extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String +extern fn gram_question_strategy(profile: [String]) -> String extern fn is_pronoun(word: String) -> Bool -extern fn build_np(referent: String, slots: Any) -> String +extern fn build_np(referent: String, slots: [String]) -> String extern fn build_pp(loc: String) -> String -extern fn build_vp_body(slots: Any) -> String -extern fn build_vp_from_slots(slots: Any) -> String -extern fn generate_tree(rule_id_str: String, slots: Any) -> String +extern fn build_vp_body(slots: [String]) -> String +extern fn build_vp_from_slots(slots: [String]) -> String +extern fn generate_tree(rule_id_str: String, slots: [String]) -> String diff --git a/elp/src/realizer.elh b/elp/src/realizer.elh index 5bf5696..445f6fe 100644 --- a/elp/src/realizer.elh +++ b/elp/src/realizer.elh @@ -1,10 +1,10 @@ -// auto-generated by elc --emit-header - do not edit +// auto-generated by elc --emit-header — do not edit extern fn agent_person(agent: String) -> String extern fn agent_number(agent: String) -> String extern fn realize_np(referent: String, number: String) -> String -extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: Any) -> Any -extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: Any) -> String +extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] +extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String extern fn capitalize_first(s: String) -> String extern fn add_punct(s: String, intent: String) -> String -extern fn realize_lang(form: Any, profile: Any) -> String -extern fn realize(form: Any) -> String +extern fn realize_lang(form: [String], profile: [String]) -> String +extern fn realize(form: [String]) -> String diff --git a/elp/src/semantics.elh b/elp/src/semantics.elh index 68304f8..80f930e 100644 --- a/elp/src/semantics.elh +++ b/elp/src/semantics.elh @@ -1,18 +1,18 @@ -// auto-generated by elc --emit-header - do not edit -extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> Any -extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> Any -extern fn sem_frame_simple(intent: String, subject: String) -> Any -extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> Any -extern fn sem_intent(frame: Any) -> String -extern fn sem_subject(frame: Any) -> String -extern fn sem_object(frame: Any) -> String -extern fn sem_modifiers(frame: Any) -> String -extern fn sem_lang(frame: Any) -> String +// auto-generated by elc --emit-header — do not edit +extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String] +extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String] +extern fn sem_frame_simple(intent: String, subject: String) -> [String] +extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String] +extern fn sem_intent(frame: [String]) -> String +extern fn sem_subject(frame: [String]) -> String +extern fn sem_object(frame: [String]) -> String +extern fn sem_modifiers(frame: [String]) -> String +extern fn sem_lang(frame: [String]) -> String extern fn sem_first_modifier(mods: String) -> String extern fn sem_intent_to_realize(intent: String) -> String -extern fn sem_to_spec(frame: Any) -> Any -extern fn sem_to_spec_full(frame: Any, verb: String, tense: String, aspect: String) -> Any +extern fn sem_to_spec(frame: [String]) -> [String] +extern fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String] extern fn sem_realize_greet(subject: String) -> String -extern fn sem_realize(frame: Any) -> String -extern fn sem_realize_full(frame: Any, verb: String, tense: String, aspect: String) -> String -extern fn sem_realize_lang(frame: Any, lang_code: String) -> String +extern fn sem_realize(frame: [String]) -> String +extern fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String +extern fn sem_realize_lang(frame: [String], lang_code: String) -> String diff --git a/engram/src/server.el b/engram/src/server.el index 38049bd..e676d32 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -180,43 +180,6 @@ fn route_forget(method: String, path: String, body: String) -> String { ok_json() } -// route_create_ise — POST /api/neuron/state-events -// Creates an InternalStateEvent node from a JSON body with a "content" field. -// Returns {"ok":true,"id":""}. Used by the soul daemon's ise_post() to -// record internal state transitions in the authoritative Engram store. -// This route was in the original server.el but was lost during a refactor; -// its absence would break ISE recording on the next Engram restart. -// (Restored 2026-06-30 self-review) -// importance=0.3 matches awareness.el in-process fallback (engram_node_full ISE -// defaults). Original had 0.5 which was a mismatch. (Corrected 2026-06-30) -fn route_create_ise(method: String, path: String, body: String) -> String { - let content: String = json_get_string(body, "content") - if str_eq(content, "") { return err_json("missing content") } - let sal: Float = 0.3 - let imp: Float = 0.3 - let conf: Float = 0.8 - let id: String = engram_node_full(content, "InternalStateEvent", "state-event", - sal, imp, conf, "Episodic", "[\"internal-state\",\"InternalStateEvent\"]") - "{\"ok\":true,\"id\":\"" + id + "\"}" -} - -// route_sync — GET /api/sync -// Returns the full graph snapshot as JSON (nodes + edges), suitable for loading -// via engram_load_merge. Used by the soul daemon's periodic refresh cycle to -// keep its in-process Engram store in sync with this authoritative HTTP store. -// Saves a temporary snapshot to avoid holding a large in-memory string while -// streaming — caller reads the file through the HTTP response body. -// (Restored 2026-06-30 self-review) -fn route_sync(method: String, path: String, body: String) -> String { - let dir: String = env("ENGRAM_DATA_DIR") - if str_eq(dir, "") { let dir = "/tmp/engram" } - let snap_path: String = dir + "/sync-export.json" - engram_save(snap_path) - let snap: String = fs_read(snap_path) - if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" } - snap -} - fn route_save(method: String, path: String, body: String) -> String { let p: String = json_get_string(body, "path") if str_eq(p, "") { @@ -243,6 +206,59 @@ fn route_health(method: String, path: String, body: String) -> String { "{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}" } +// route_sync — return a snapshot of non-ISE/non-Working nodes for the soul daemon +// to merge into its in-process graph via engram_load_merge. +// +// The soul calls GET /api/sync every SOUL_REFRESH_MS (default 10 min) to pull +// new Knowledge/Memory/BacklogItem nodes from the authoritative HTTP Engram into +// its in-process working store. Previously this returned 404 "not found", causing +// the soul to write the error JSON to a temp file and attempt an empty merge. +// +// Strategy: save the current snapshot to disk, read it back, return the full +// snapshot JSON. The soul's engram_load_merge handles large files gracefully +// (it skips nodes already present by ID). Auth-exempt: same-host internal call. +// (2026-06-27 self-review: added this route to fix silent 10-min sync failures) +fn route_sync(method: String, path: String, body: String) -> String { + let dir: String = env("ENGRAM_DATA_DIR") + if str_eq(dir, "") { let dir = "/tmp/engram" } + let snap_path: String = dir + "/snapshot.json" + engram_save(snap_path) + let snap: String = fs_read(snap_path) + if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" } + return snap +} + +// route_emit_ise — write an InternalStateEvent node from the soul daemon. +// +// Endpoint: POST /api/neuron/state-events +// Body: {"content": ""} +// +// Auth: exempt (internal endpoint, soul daemon on same host, no _auth needed). +// The soul's ise_post() sends {"content":"..."} without _auth; enforcing auth +// here would silently drop all heartbeat/curiosity ISEs. Unauthenticated POST +// to this endpoint is acceptable: ISE writes are observability-only, append-only, +// and come from a trusted process on localhost. +// +// Salience/importance set to match engram_node_full ISE defaults used by the +// in-process fallback path in awareness.el (salience=0.3, importance=0.3, +// confidence=0.8, tier=Episodic). High temporal_decay_rate (1.617) — ISEs +// are inherently transient; they should decay faster than structural knowledge. +// (2026-06-26 self-review: added this route after discovering ise_post was +// silently failing — the soul posts here but the endpoint didn't exist.) +fn route_emit_ise(method: String, path: String, body: String) -> String { + let content: String = json_get_string(body, "content") + if str_eq(content, "") { return err_json("missing content") } + let sal: Float = 0.3 + let imp: Float = 0.3 + let conf: Float = 0.8 + let id: String = engram_node_full( + content, "InternalStateEvent", "state-event", + sal, imp, conf, + "Episodic", "[\"internal-state\",\"InternalStateEvent\"]" + ) + "{\"ok\":true,\"id\":\"" + id + "\"}" +} + // ── Auth ────────────────────────────────────────────────────────────────────── fn check_auth_ok(method: String, body: String) -> Bool { @@ -269,12 +285,9 @@ fn handle_request(method: String, path: String, body: String) -> String { } } - // Internal state events bypass auth — only the local soul daemon calls - // this route, and ise_post() does not include an _auth field in its body. - // Placing this before the auth gate preserves the old binary's behavior - // and ensures the soul daemon can always write ISEs. - if str_eq(method, "POST") && str_starts_with(clean, "/api/neuron/state-events") { - return route_create_ise(method, path, body) + // ISE posting is auth-exempt (internal soul daemon, same host, no _auth key) + if str_eq(method, "POST") && str_eq(clean, "/api/neuron/state-events") { + return route_emit_ise(method, path, body) } // Auth (when ENGRAM_API_KEY is set) @@ -331,11 +344,6 @@ fn handle_request(method: String, path: String, body: String) -> String { return route_strengthen(method, path, body) } - // Sync — soul daemon fetches here for periodic in-process graph refresh - if str_eq(method, "GET") && (str_eq(clean, "/api/sync") || str_eq(clean, "/sync")) { - return route_sync(method, path, body) - } - // Persistence if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) { return route_save(method, path, body) @@ -344,6 +352,11 @@ fn handle_request(method: String, path: String, body: String) -> String { return route_load(method, path, body) } + // Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph + if str_eq(method, "GET") && str_eq(clean, "/api/sync") { + return route_sync(method, path, body) + } + "{\"error\":\"not found\",\"path\":\"" + clean + "\"}" } diff --git a/lang/el-compiler/runtime/ElBridge.java b/lang/el-compiler/runtime/ElBridge.java new file mode 100644 index 0000000..52cb402 --- /dev/null +++ b/lang/el-compiler/runtime/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/lang/el-compiler/runtime/PLATFORM_BRIDGE_SPEC.md b/lang/el-compiler/runtime/PLATFORM_BRIDGE_SPEC.md new file mode 100644 index 0000000..82d37e4 --- /dev/null +++ b/lang/el-compiler/runtime/PLATFORM_BRIDGE_SPEC.md @@ -0,0 +1,554 @@ +# Platform Bridge Specification — el-native + +This document is the authoritative reference for anyone implementing a new platform bridge for the el-native widget system. Read it top to bottom before writing a single line of code. + +--- + +## What a Platform Bridge Is + +The el compiler (`elc`) emits C code. That C code calls `__`-prefixed functions for everything OS-related: printing, file I/O, threading, and — when building a native UI app — widget creation and event handling. These `__` functions are the *OS boundary*. + +For native UI, the bridge is the translation layer between that fixed C API and whatever platform toolkit you are targeting. The bridge owns: + +1. A **slot table** of up to 4096 widget objects, indexed by `int64_t` handle. +2. Implementations of all 33 `__*` widget functions declared in `el_native_target.h`. +3. Callback dispatch from platform events back into El function symbols resolved via `dlsym` (or a platform equivalent). + +The thin wrappers in `el_seed.c` (`#ifdef EL_TARGET_*` blocks) marshal between `el_val_t` and native C types, then call through to the bridge. The bridge itself never touches `el_val_t` — it works only with plain C types (`int64_t`, `const char*`, `int`, `float`). + +**Existing bridges:** + +| File | Platform | Toolkit | Language | +|------|----------|---------|----------| +| `el_appkit.m` | macOS | AppKit | ObjC (MRC) | +| `el_gtk4.c` | Linux | GTK4 | C | +| `el_win32.c` | Windows | Win32/ComCtl | C | +| `el_uikit.m` | iOS | UIKit | ObjC (MRC) | +| `el_android.c` + `ElBridge.java` | Android | View/JNI | C + Java | +| `el_sdl2.c` | Embedded Linux / Pi | SDL2 | C | +| `el_lvgl.c` | Microcontrollers | LVGL | C | + +--- + +## The Slot System + +Every widget — window, button, label, container, image — is stored in a static array: + +```c +#define EL__MAX_WIDGETS 4096 + +typedef struct { + ElWidgetKind kind; + /* platform-specific object reference (pointer, handle, ID...) */ + /* callback names */ + char* cb_click; + char* cb_change; +} ElWidget; + +static ElWidget _el_widgets[EL__MAX_WIDGETS]; +``` + +Rules: +- **Slot 0 is never valid.** Scan starts at index 1. This ensures 0 is never a valid handle. +- **Handle = slot index.** An `int64_t` value returned to El code is a direct index into `_el_widgets[]`. +- **-1 = invalid.** All create functions return -1 on failure (table full, platform API error). +- **Slot is FREE until allocated, FREE again after destroy.** Use an `ElWidgetKind` enum where `0 = EL_WIDGET_FREE` to track liveness. +- **4096 slots is the system-wide maximum.** This is intentional and sufficient for any realistic UI. Do not increase it without a compelling reason. + +### Slot allocation and release pattern + +```c +static int64_t el_widget_alloc(ElWidgetKind kind, /* platform object ref */) { + for (int i = 1; i < EL__MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + /* store platform object ref — retain/addref if needed */ + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + return (int64_t)i; + } + } + return -1; /* table full */ +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL__MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* release platform object ref */ + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} +``` + +**NULL handle guard:** `el_widget_get` must return `NULL` for any handle that is 0, negative, out of range, or points to a `FREE` slot. Every `__` function that takes a handle must null-check the result of `el_widget_get` before doing anything. Failing to do so causes crashes or corruption when El code passes an uninitialized handle. + +--- + +## The 33 Required Functions + +Every bridge must implement all 33 functions listed below. They are grouped by category. The signatures shown are from `el_native_target.h` and from `el_seed.c`'s wrapper layer; the bridge itself uses plain C types (the wrappers do the `el_val_t` ↔ C-type conversion). + +### Internal C signatures (what the bridge implements) + +These are what your `.c` / `.m` file defines. The `el_seed.c` wrappers call these. + +#### Lifecycle (2 functions) + +```c +void el__init(void); +``` +**Purpose:** Initialize the platform UI toolkit. Must be idempotent (safe to call more than once). Called once from `__native_init` before any widget creation. +**Edge cases:** On platforms where the toolkit must be initialized before a display connection is established (X11, Wayland), this is where that happens. On Android, this is a no-op because ElBridge.java calls init from Java. + +```c +void el__run_loop(void); +``` +**Purpose:** Start the platform event loop. On most platforms this **never returns**. Exceptions: Android (the loop is Java-managed — this must be a no-op) and headless test builds. +**Edge cases:** Must be called from the main thread. On iOS/UIKit, calls `UIApplicationMain` which never returns; El code must set `el_main_entry_fn` before calling this. + +#### Window (3 functions) + +```c +int64_t el__window_create(const char* title, int w, int h, int mw, int mh); +``` +**Purpose:** Create a top-level window. `w`/`h` = initial size in logical pixels. `mw`/`mh` = minimum size (0 = no minimum). +**Returns:** Slot handle, or -1 on failure. +**Edge cases:** NULL or empty `title` must be handled gracefully (use `""`). On mobile (iOS, Android), the concept of a "window" maps to the root view controller / activity root view — create that here. + +```c +void el__window_show(int64_t handle); +``` +**Purpose:** Make the window visible. On some platforms windows are hidden at creation; this makes them appear. +**Edge cases:** NULL handle → return silently. Calling on an already-visible window is a no-op. + +```c +void el__window_set_title(int64_t handle, const char* title); +``` +**Purpose:** Update the window's title bar text at runtime. +**Edge cases:** NULL handle or NULL title → no-op. + +#### Layout containers (4 functions) + +```c +int64_t el__vstack_create(int spacing); +int64_t el__hstack_create(int spacing); +``` +**Purpose:** Create a vertical/horizontal linear container. `spacing` = gap between children in logical pixels. +**Returns:** Slot handle, or -1. +**Edge cases:** spacing = 0 is valid and common. + +```c +int64_t el__zstack_create(void); +``` +**Purpose:** Create a z-axis layered container (children overlap, no stacking direction). Used for overlays. +**Returns:** Slot handle, or -1. + +```c +int64_t el__scroll_create(void); +``` +**Purpose:** Create a scrollable container. Scrolls vertically by default. Only the first child added is the scrollable content. +**Returns:** Slot handle, or -1. + +#### Leaf widgets (5 functions) + +```c +int64_t el__label_create(const char* text); +``` +**Purpose:** Create a non-editable text label. +**Edge cases:** NULL/empty text → label with empty string, not a crash. + +```c +int64_t el__button_create(const char* label); +``` +**Purpose:** Create a clickable button. The `label` is the button's visible text. +**Edge cases:** The button must wire up an action target at creation time so that click callbacks registered later via `el__widget_on_click` will fire. On platforms with target-action (AppKit, UIKit), allocate the delegate object here. + +```c +int64_t el__text_field_create(const char* placeholder); +``` +**Purpose:** Create a single-line text input. `placeholder` is the hint text shown when empty. +**Edge cases:** NULL placeholder → no hint text displayed. + +```c +int64_t el__text_area_create(const char* placeholder); +``` +**Purpose:** Create a multi-line text input (scrollable). +**Edge cases:** Same as text_field_create. On AppKit, this wraps NSTextView inside NSScrollView — the slot's `obj` points to the scroll view, not the text view. + +```c +int64_t el__image_create(const char* path_or_name); +``` +**Purpose:** Create an image widget. `path_or_name` can be a filesystem path or a platform resource name. Try path first, fall back to named resource. +**Edge cases:** Non-existent path → create an empty image widget (do not crash). NULL → same. + +#### Widget properties (12 functions) + +```c +void el__widget_set_text(int64_t handle, const char* text); +``` +**Purpose:** Update the text content of a label, button, text field, text area, or window title. Must dispatch on `kind` to use the correct API. +**Edge cases:** NULL handle → no-op. NULL text → treat as `""`. + +```c +const char* el__widget_get_text(int64_t handle); +``` +**Purpose:** Return the current text of a widget. Returns a `const char*` that the `el_seed.c` wrapper wraps into an `el_val_t` string. +**Edge cases:** NULL handle → return `""` (never NULL). The caller in `el_seed.c` handles the `strdup` lifetime issue — see the AppKit reference implementation notes. + +```c +void el__widget_set_color(int64_t h, float r, float g, float b, float a); +void el__widget_set_bg_color(int64_t h, float r, float g, float b, float a); +``` +**Purpose:** Set foreground (text) color and background color respectively. All channels are normalized floats [0.0, 1.0]. +**Edge cases:** For containers, `set_color` may be a no-op (no text); `set_bg_color` should set the layer/surface background. On platforms without alpha compositing, clamp alpha to 0 or 1. + +```c +void el__widget_set_font(int64_t h, const char* family, int size, int bold); +``` +**Purpose:** Set the font on a text-bearing widget. `family` = font family name or `"system"` for the platform default. `size` = point size. `bold` = 0 or 1. +**Edge cases:** If `family` is not found, fall back to the system font. Non-text widgets (containers, images) → no-op. + +```c +void el__widget_set_padding(int64_t h, int top, int right, int bottom, int left); +``` +**Purpose:** Set internal padding/insets for a container or text area. +**Edge cases:** On platforms where padding is per-view (not per-container), map to the nearest equivalent (margin, insets, text container inset). For leaf widgets other than text areas, this may be a partial no-op. + +```c +void el__widget_set_width(int64_t h, int width); +void el__widget_set_height(int64_t h, int height); +``` +**Purpose:** Apply a fixed-size constraint. `width`/`height` in logical pixels. +**Edge cases:** On platforms with Auto Layout or constraint systems, add a fixed-size constraint. Do not apply to windows (size is set at creation). Calling multiple times should override the previous constraint, not add another. + +```c +void el__widget_set_flex(int64_t h, int flex); +``` +**Purpose:** Set the flex/expansion factor. `flex > 0` → the widget expands to fill available space. `flex == 0` → hugs content size. +**Edge cases:** Maps to content-hugging priority (AppKit), `GtkWidget::hexpand`/`vexpand` (GTK4), or layout weight (Android). For windows → no-op. + +```c +void el__widget_set_corner_radius(int64_t h, int radius); +``` +**Purpose:** Apply rounded corners to the widget's visual layer. +**Edge cases:** Requires backing layer / GPU surface. On platforms without layer compositing (Win32 without DX), this may be a no-op or require manual painting. Radius in logical pixels. + +```c +void el__widget_set_disabled(int64_t h, int disabled); +``` +**Purpose:** Enable or disable user interaction. `disabled = 1` → greyed out, non-interactive. +**Edge cases:** Only meaningful for interactive widgets (button, text field). For containers/labels → no-op. + +```c +void el__widget_set_hidden(int64_t h, int hidden); +``` +**Purpose:** Show or hide the widget. `hidden = 1` → invisible but still in layout. +**Edge cases:** For windows, map to `orderOut`/`hide` or equivalent. For views, use `setHidden`/`gtk_widget_set_visible` or equivalent. + +#### Tree management (3 functions) + +```c +void el__widget_add_child(int64_t parent, int64_t child); +``` +**Purpose:** Attach a child widget to a parent container. Dispatch on parent kind: +- Window → add to root content view/container +- VStack/HStack → add as arranged/linear child +- ZStack → add as overlapping subview +- Scroll → set as document/content view (first child only) +- Other → add as plain subview + +**Edge cases:** NULL parent or child handle → no-op. Attempting to add a window as a child → no-op. Adding the same child twice is platform-defined behavior (tolerate it). + +```c +void el__widget_remove_child(int64_t parent, int64_t child); +``` +**Purpose:** Detach child from its parent. The child slot remains allocated; the widget is not destroyed. +**Edge cases:** NULL handles → no-op. Child not currently attached → no-op. + +```c +void el__widget_destroy(int64_t handle); +``` +**Purpose:** Destroy a widget: remove from superview/parent, release the platform object, release callback strings, mark slot as FREE. +**Edge cases:** For windows, close the window. Free any delegate/target objects stored in side tables. After destroy, the handle is invalid — El code must not use it again (this is the caller's responsibility, not enforced here). + +#### Event registration (3 functions) + +```c +void el__widget_on_click(int64_t h, const char* fn_name); +void el__widget_on_change(int64_t h, const char* fn_name); +void el__widget_on_submit(int64_t h, const char* fn_name); +``` +**Purpose:** Register an El callback function by symbol name. +- `on_click` → button press +- `on_change` → text field value change (keystroke-level) +- `on_submit` → text field Enter key (text field only; stored in `cb_click` slot in AppKit) + +The implementation stores `strdup(fn_name)` in the widget's `cb_click` or `cb_change` field. The platform event handler calls `el__invoke_cb` (see callback ABI section). +**Edge cases:** NULL or empty `fn_name` → clear the callback (`free` + set NULL). Calling on a non-interactive widget (label, image) → no-op or store silently (harmless). + +#### Manifest reader (1 function) + +```c +/* Note: __manifest_read is implemented in el_seed.c, not in the bridge. */ +/* Bridges do NOT need to implement this. */ +``` + +`__manifest_read` is handled entirely in the platform-independent section of `el_seed.c`. It reads a JSON/EL manifest file from the path in the `EL_MANIFEST` environment variable. Bridge authors do not need to implement this. + +--- + +## The Callback ABI + +When a platform event fires (button clicked, text changed), the bridge must call back into the El runtime. The mechanism: + +```c +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el__invoke_cb(const char* fn_name, int64_t handle, const char* data) { + if (!fn_name || !*fn_name) return; + void* sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} +``` + +The El callback signature (in El source): +``` +fn handler(handle: Int, data: String) -> Void +``` + +Which compiles to: +```c +void handler(int64_t handle, int64_t data) +``` + +Where `data` is a `const char*` cast to `int64_t` (the el `String` representation). For click events, `data` is `""`. For change/submit events, `data` is the current widget text. + +**On platforms without `dlsym`** (Windows, some embedded systems): use `GetProcAddress(GetModuleHandle(NULL), fn_name)` on Windows, or maintain a manual symbol registration table for embedded targets where dynamic linking is unavailable. + +**Thread safety for callbacks:** Callbacks fired from a background thread must be marshalled to the UI thread before calling into El code. El code may call `__widget_set_text` or other UI functions synchronously from within the callback — those must run on the UI thread. + +--- + +## Thread Safety Contract + +**All platform UI operations must execute on the main/UI thread.** This is a hard requirement on every platform (AppKit, UIKit, GTK4, Win32, Android View, SDL2 main thread rule). + +The reference pattern (AppKit): +```c +static void el_appkit_sync_main(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} +``` + +For other platforms: +- **GTK4:** `g_main_context_invoke` or `g_idle_add` + semaphore for synchronous dispatch +- **Win32:** `SendMessage(hwnd, WM_APP, ...)` or `PostMessage` + wait +- **Android:** `Activity.runOnUiThread` +- **SDL2:** All ops must be called from the thread that initialized SDL (the main thread) +- **LVGL:** `lv_lock()` / `lv_unlock()` for thread-safe access + +El program flow is: `main()` → build UI (on main thread) → `__native_run_loop()`. Because UI is built before the run loop starts, most widget creation calls are already on the main thread. The sync dispatch wrapper exists to handle callbacks that arrive on worker threads (e.g., network callbacks that update UI). + +--- + +## Integration Pattern + +### In `el_native_target.h` + +Add an `#ifdef EL_TARGET_` block declaring all 33 `__*` functions with their `el_val_t` signatures (identical to the existing blocks for MACOS, LINUX, etc.): + +```c +#ifdef EL_TARGET_ + +void __native_init(void); +void __native_run_loop(void); + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ */ +``` + +### In `el_seed.c` + +Add an `#ifdef EL_TARGET_` block containing: +1. `extern` declarations of all `el__*` functions (your bridge's C API) +2. Thin wrapper functions that marshal `el_val_t` ↔ C types and call through + +The wrapper pattern (copy from the `EL_TARGET_MACOS` block and substitute the platform name): + +```c +#ifdef EL_TARGET_ + +/* Forward declarations — implemented in el_.c */ +extern void el__init(void); +extern void el__run_loop(void); +extern int64_t el__window_create(const char* title, int w, int h, int mw, int mh); +/* ... all others ... */ + +/* Wrappers */ +void __native_init(void) { el__init(); } +void __native_run_loop(void) { el__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__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__window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el__window_set_title((int64_t)h, EL_CSTR(t)); } + +/* ... continue for all 33 functions ... */ + +#endif /* EL_TARGET_ */ +``` + +Key marshalling rules: +- `el_val_t` → `int64_t` handle: `(int64_t)h` +- `el_val_t` → `int`: `(int)(int64_t)value` +- `el_val_t` → `const char*`: `EL_CSTR(value)` +- `el_val_t` → `float`: `(float)el_to_float(value)` (for color channels) +- `int64_t` handle → `el_val_t`: `(el_val_t)handle` +- `const char*` → `el_val_t`: `EL_STR(str)` — but read the get_text note below + +**`__widget_get_text` note:** The bridge returns `const char*`. The `el_seed.c` wrapper wraps it with `EL_STR(s)`. The returned pointer must remain valid until the El program is done with it. The AppKit implementation returns a `strdup`'d string — the caller (seed wrapper) stores it without tracking it in the arena. This is a known lifetime edge; be consistent with the platform's existing pattern. + +--- + +## How el Strings Work + +Inside El compiled C code, strings are `el_val_t` values where the value is the `uintptr_t` cast of a `const char*`: + +```c +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +``` + +To construct a string result in `el_seed.c`: +```c +static char* s = strdup("hello"); +return EL_STR(s); +``` + +To read a string argument passed from El: +```c +const char* text = EL_CSTR(some_el_val_t_argument); +``` + +The `EL_STR` / `EL_CSTR` macros are defined in `el_seed.h` and are available in `el_seed.c`. Bridge files (`el_.c`) do not use these macros — they only deal in `const char*` at their API boundary. + +--- + +## Known Gotchas + +### Duplicate symbols with `el_runtime.c` + +`el_runtime.c` defines many of the same `__`-prefixed symbols as `el_seed.c`. When linking both (required for the native-hello example), the linker will reject duplicate definitions. The build system uses `nmedit` (macOS) to hide the `el_runtime.c` copies of symbols that `el_seed.c` already defines, keeping `el_seed.c` canonical. + +If you are writing a build script for a new platform and see duplicate symbol link errors involving `__println`, `__print`, `__str_len`, etc., apply the same nmedit / `objcopy --weaken-symbol` / `strip --strip-symbol` trick from the macOS build to your platform's object file handling. + +### The `nmedit` trick on macOS + +```bash +# Build a keep-list: symbols defined in el_seed.o but also in el_runtime.o +nm el_seed.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .seed_T.txt +nm el_runtime.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .rt_T.txt +# Keep only symbols unique to el_runtime.o +comm -23 .rt_T.txt .seed_T.txt > .rt_keep.txt +nmedit -s .rt_keep.txt el_runtime.o +``` + +On Linux, use `objcopy` with `--weaken-symbol` for each duplicate, or link `el_seed.o` before `el_runtime.o` and use `--allow-multiple-definition` if your use case permits it. + +### ObjC bridges must use MRC, not ARC + +Bridges that use Objective-C (AppKit, UIKit) **must** compile without ARC (`-fno-objc-arc`). The reason: the widget table stores `id` values in a plain C struct. ARC cannot insert retain/release through C struct boundaries, and will reject explicit `[obj retain]` / `[obj release]` calls in ARC mode. Use: + +```bash +clang -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m +``` + +### Widget table struct — don't put `id` fields in C structs under ARC + +If you ever add ARC-managed object fields to the `ElWidget` struct, you will get a compile error. Keep the struct C-only (pointers as `void*` if you must, cast when using) or compile as MRC. + +### The `el_seed.c` `__manifest_read` is platform-independent + +`__manifest_read` is already implemented in `el_seed.c` (in the section compiled unconditionally). You do not implement it in your bridge. You do need to declare it in the `el_native_target.h` block for your platform (matching the other platforms), but `el_seed.c` already has the implementation. + +--- + +## Completion Checklist + +Before declaring your bridge ready for integration: + +- [ ] All 33 `__*` functions implemented (including `__manifest_read` declaration, implementation is in seed) +- [ ] Slot table with 4096 entries, scan starting at index 1 +- [ ] `el_widget_get` returns NULL for handle 0, negative, out-of-range, and FREE slots +- [ ] All functions null-check `el_widget_get` result before use +- [ ] `el__init` is idempotent +- [ ] `el__run_loop` either never returns or is documented as a no-op (Android) +- [ ] Callback dispatch via `dlsym` (or platform equivalent) implemented +- [ ] All UI operations dispatched to the main/UI thread +- [ ] `el_native_target.h` updated with `#ifdef EL_TARGET_` block +- [ ] `el_seed.c` updated with `#ifdef EL_TARGET_` extern + wrapper block +- [ ] Bridge compiles cleanly with no warnings: `cc -DEL_TARGET_ -Wall -Wextra -c el_.c` +- [ ] Bridge links cleanly with `el_seed.o` and `el_runtime.o` (duplicate symbol check) +- [ ] `detect-platforms` script updated with detection logic for the new platform +- [ ] Basic smoke test: create window → add label → show window → run loop diff --git a/lang/el-compiler/runtime/detect-platforms b/lang/el-compiler/runtime/detect-platforms new file mode 100755 index 0000000..bceace2 --- /dev/null +++ b/lang/el-compiler/runtime/detect-platforms @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# detect-platforms — probe available platform bridge dependencies +# +# Reports which el-native platform bridges can be built on this machine, +# with install instructions for anything that is missing. +# +# Usage: ./detect-platforms +# ./build.sh platforms (from native-hello) + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +PASS="[+]" +FAIL="[ ]" + +_ok() { printf " ${PASS} %-22s %s\n" "$1" "$2"; } +_miss() { printf " ${FAIL} %-22s %s\n" "$1" "$2"; } + +# ── Header ──────────────────────────────────────────────────────────────────── + +echo "" +echo "==> el-native platform detection" +echo "" +echo " Checking build dependencies for each platform bridge..." +echo "" + +AVAILABLE=0 +MISSING=0 + +# ── macOS / AppKit ──────────────────────────────────────────────────────────── + +if [[ "$(uname)" == "Darwin" ]]; then + if xcrun --find clang &>/dev/null && xcrun --find xcodebuild &>/dev/null 2>/dev/null || \ + xcrun --find cc &>/dev/null; then + CLT_INFO="Xcode CLT $(xcode-select -p 2>/dev/null | sed 's|/Developer||' || echo '')" + _ok "macOS/AppKit" "-DEL_TARGET_MACOS ${CLT_INFO}" + AVAILABLE=$((AVAILABLE + 1)) + else + _miss "macOS/AppKit" "-DEL_TARGET_MACOS (Xcode CLT missing — xcode-select --install)" + MISSING=$((MISSING + 1)) + fi +else + _miss "macOS/AppKit" "-DEL_TARGET_MACOS (not on macOS)" +fi + +# ── iOS / UIKit ─────────────────────────────────────────────────────────────── + +if [[ "$(uname)" == "Darwin" ]]; then + if xcrun --sdk iphoneos --show-sdk-path &>/dev/null 2>&1; then + SDK_VER=$(xcrun --sdk iphoneos --show-sdk-version 2>/dev/null || echo "") + _ok "iOS/UIKit" "-DEL_TARGET_IOS SDK ${SDK_VER} (requires Xcode.app)" + AVAILABLE=$((AVAILABLE + 1)) + else + _miss "iOS/UIKit" "-DEL_TARGET_IOS (iOS SDK not found — install Xcode.app)" + MISSING=$((MISSING + 1)) + fi +else + _miss "iOS/UIKit" "-DEL_TARGET_IOS (not on macOS — requires Xcode)" +fi + +# ── Linux / GTK4 ────────────────────────────────────────────────────────────── + +if pkg-config --exists gtk4 2>/dev/null; then + GTK_VER=$(pkg-config --modversion gtk4 2>/dev/null) + _ok "Linux/GTK4" "-DEL_TARGET_LINUX gtk4 ${GTK_VER}" + AVAILABLE=$((AVAILABLE + 1)) +else + _miss "Linux/GTK4" "-DEL_TARGET_LINUX (gtk4 not found)" + echo " Install: apt install libgtk-4-dev" + echo " or: dnf install gtk4-devel" + echo " or: brew install gtk4" + MISSING=$((MISSING + 1)) +fi + +# ── SDL2 / Embedded Linux / Pi ──────────────────────────────────────────────── + +SDL2_OK=0 +if pkg-config --exists sdl2 2>/dev/null; then + SDL2_VER=$(pkg-config --modversion sdl2 2>/dev/null) + # Also check for SDL2_ttf (needed for text rendering) + if pkg-config --exists SDL2_ttf 2>/dev/null; then + TTF_VER=$(pkg-config --modversion SDL2_ttf 2>/dev/null) + _ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER}, SDL2_ttf ${TTF_VER}" + else + _ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (SDL2_ttf missing — needed for text)" + echo " Install: apt install libsdl2-ttf-dev" + fi + SDL2_OK=1 + AVAILABLE=$((AVAILABLE + 1)) +elif command -v sdl2-config &>/dev/null; then + SDL2_VER=$(sdl2-config --version 2>/dev/null || echo "") + _ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (via sdl2-config)" + SDL2_OK=1 + AVAILABLE=$((AVAILABLE + 1)) +fi + +if [[ $SDL2_OK -eq 0 ]]; then + _miss "SDL2/Embedded" "-DEL_TARGET_SDL2 (sdl2 not found)" + echo " Install: apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev" + echo " or: brew install sdl2 sdl2_ttf sdl2_image" + MISSING=$((MISSING + 1)) +fi + +# ── LVGL / Microcontrollers ─────────────────────────────────────────────────── + +LVGL_OK=0 +LVGL_WHERE="" + +if [ -f "${SCRIPT_DIR}/lvgl/lvgl.h" ]; then + LVGL_WHERE="./lvgl/lvgl.h" + LVGL_OK=1 +elif [ -f "${SCRIPT_DIR}/../lvgl/lvgl.h" ]; then + LVGL_WHERE="adjacent lvgl/" + LVGL_OK=1 +elif [ -f "/usr/include/lvgl/lvgl.h" ]; then + LVGL_WHERE="/usr/include/lvgl" + LVGL_OK=1 +elif [ -f "/usr/local/include/lvgl/lvgl.h" ]; then + LVGL_WHERE="/usr/local/include/lvgl" + LVGL_OK=1 +elif pkg-config --exists lvgl 2>/dev/null; then + LVGL_WHERE="pkg-config ($(pkg-config --modversion lvgl 2>/dev/null))" + LVGL_OK=1 +fi + +if [[ $LVGL_OK -eq 1 ]]; then + _ok "LVGL/MCU" "-DEL_TARGET_LVGL ${LVGL_WHERE}" + AVAILABLE=$((AVAILABLE + 1)) +else + _miss "LVGL/MCU" "-DEL_TARGET_LVGL (lvgl.h not found)" + echo " Install: git clone https://github.com/lvgl/lvgl" + echo " (place lvgl/ next to el-compiler/runtime/)" + MISSING=$((MISSING + 1)) +fi + +# ── Android / JNI ───────────────────────────────────────────────────────────── + +ANDROID_OK=0 +ANDROID_WHERE="" + +if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "${ANDROID_NDK_HOME}" ]; then + NDK_VER="" + if [ -f "${ANDROID_NDK_HOME}/source.properties" ]; then + NDK_VER=$(grep "Pkg.Revision" "${ANDROID_NDK_HOME}/source.properties" \ + 2>/dev/null | cut -d= -f2 | tr -d ' ' || echo "") + fi + ANDROID_WHERE="NDK ${NDK_VER:-(version unknown)} at \$ANDROID_NDK_HOME" + ANDROID_OK=1 +elif command -v ndk-build &>/dev/null; then + ANDROID_WHERE="ndk-build in PATH" + ANDROID_OK=1 +elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/ndk" ]; then + ANDROID_WHERE="NDK via \$ANDROID_HOME/ndk" + ANDROID_OK=1 +fi + +if [[ $ANDROID_OK -eq 1 ]]; then + _ok "Android/JNI" "-DEL_TARGET_ANDROID ${ANDROID_WHERE}" + AVAILABLE=$((AVAILABLE + 1)) +else + _miss "Android/JNI" "-DEL_TARGET_ANDROID (ANDROID_NDK_HOME not set)" + echo " Install: https://developer.android.com/studio/releases/ndk" + echo " Then: export ANDROID_NDK_HOME=/path/to/ndk" + MISSING=$((MISSING + 1)) +fi + +# ── Windows / Win32 (cross or native) ───────────────────────────────────────── + +WIN32_OK=0 +WIN32_WHERE="" + +if [[ "$(uname)" == MINGW* ]] || [[ "$(uname)" == CYGWIN* ]] || \ + [[ "$(uname)" == MSYS* ]]; then + WIN32_WHERE="native Windows ($(uname))" + WIN32_OK=1 +elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then + MINGW_VER=$(x86_64-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "") + WIN32_WHERE="mingw cross-compiler — ${MINGW_VER}" + WIN32_OK=1 +elif command -v i686-w64-mingw32-gcc &>/dev/null; then + WIN32_WHERE="mingw 32-bit cross-compiler" + WIN32_OK=1 +fi + +if [[ $WIN32_OK -eq 1 ]]; then + _ok "Windows/Win32" "-DEL_TARGET_WIN32 ${WIN32_WHERE}" + AVAILABLE=$((AVAILABLE + 1)) +else + _miss "Windows/Win32" "-DEL_TARGET_WIN32 (mingw cross-compiler not found)" + echo " Install: brew install mingw-w64" + echo " or: apt install gcc-mingw-w64" + MISSING=$((MISSING + 1)) +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── + +echo "" +echo " ${AVAILABLE} platform(s) available, ${MISSING} unavailable on this machine." +echo "" + +if [[ -x "${SCRIPT_DIR}/new-platform" ]]; then + echo " Scaffold a new bridge: ${SCRIPT_DIR}/new-platform " +fi +echo " Bridge contract: ${SCRIPT_DIR}/PLATFORM_BRIDGE_SPEC.md" +echo "" diff --git a/lang/el-compiler/runtime/el_android.c b/lang/el-compiler/runtime/el_android.c new file mode 100644 index 0000000..7e3e765 --- /dev/null +++ b/lang/el-compiler/runtime/el_android.c @@ -0,0 +1,949 @@ +/* + * 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); +} + +#endif /* EL_TARGET_ANDROID */ diff --git a/lang/el-compiler/runtime/el_appkit.m b/lang/el-compiler/runtime/el_appkit.m new file mode 100644 index 0000000..9eac7a6 --- /dev/null +++ b/lang/el-compiler/runtime/el_appkit.m @@ -0,0 +1,1018 @@ +/* + * el_appkit.m — AppKit backend for the el native widget system. + * + * This file implements the macOS AppKit widget layer that el_seed.c calls + * through to when EL_TARGET_MACOS is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_appkit_* C-callable functions declared here + * → NSView/NSWindow/NSStackView/NSButton/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 AppKit calls MUST run on the main thread. el_appkit_dispatch_main + * bounces work to the main thread synchronously if called from a background thread. + * el_appkit_run_loop MUST be called from the main thread; it never returns. + * + * Callback mechanism: when a widget fires an event (button click, 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. + * + * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): + * clang -std=gnu11 -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m -o el_appkit.o + * Then link el_appkit.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 Cocoa + */ + +#import +#include +#include +#include +#include +#include + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_APPKIT_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; /* NSWindow* or NSView* subclass, retained */ + char* cb_click; /* El function name for click/submit events */ + char* cb_change; /* El function name for value-change events */ + char* user_data; /* custom data string passed to click callbacks */ +} ElWidget; + +static ElWidget _el_widgets[EL_APPKIT_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* 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_APPKIT_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; + _el_widgets[i].user_data = 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_APPKIT_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; + free(w->user_data); w->user_data = NULL; +} + +/* ── Dispatch helpers ─────────────────────────────────────────────────────── */ + +/* Run a block on the main thread, waiting for completion. Safe to call from + * any thread including the main thread. */ +static void el_appkit_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_appkit_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 delegate for button clicks ─────────────────────────────────────── */ + +@interface ElButtonTarget : NSObject +@property int64_t widgetHandle; +@end + +@implementation ElButtonTarget +- (void)buttonClicked:(id)sender { + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + el_appkit_invoke_cb(w->cb_click, self.widgetHandle, w->user_data ? w->user_data : ""); + } +} +@end + +/* ── ObjC delegate for text field changes / enter key ───────────────────── */ + +@interface ElTextFieldDelegate : NSObject +@property int64_t widgetHandle; +@end + +@implementation ElTextFieldDelegate +- (void)controlTextDidChange:(NSNotification*)notif { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + NSTextField* tf = (NSTextField*)notif.object; + const char* val = [[tf stringValue] UTF8String]; + if (w->cb_change) { + el_appkit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); + } +} +- (void)controlTextDidEndEditing:(NSNotification*)notif { + /* Enter key (or tab) submits. The userInfo value is an NSNumber, not NSEvent. */ + NSNumber* movement = [[notif userInfo] objectForKey:@"NSTextMovement"]; + if (movement && [movement intValue] == NSTextMovementReturn) { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + NSTextField* tf = (NSTextField*)notif.object; + const char* val = [[tf stringValue] UTF8String]; + if (w->cb_click) { /* cb_click = on_submit for text fields */ + el_appkit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); + } + } +} +@end + +/* We hold strong references to our ObjC delegate objects in a side table so + * ARC/MRC don't collect them. Simple parallel array keyed by widget slot. */ +static id _el_delegates[EL_APPKIT_MAX_WIDGETS]; + +/* ── Flipped wrapper for NSScrollView document views ───────────────────────── + * NSScrollView's clip view uses the document view's coordinate system. When + * the document view is non-flipped (origin at bottom-left), content stacks + * from the bottom. This subclass overrides isFlipped so content starts at the + * visual top-left, matching expected list/feed behaviour. */ +@interface ElFlippedView : NSView +@end +@implementation ElFlippedView +- (BOOL)isFlipped { return YES; } +@end + +/* ── ElResizableContentView ─────────────────────────────────────────────────── + * Used as NSWindow's content view instead of a plain NSView. + * Overrides setFrameSize: to propagate the new size directly to the single + * root child without going through the AL constraint graph. + * + * Why: with TAMIC:NO on the content view and 4-edge AL constraints to the + * child, NSWindow calls [contentView fittingSize] to compute its minimum + * resize floor. fittingSize walks the constraint graph and returns the + * largest minimum it finds — locking horizontal resize to the initial width. + * By removing the AL constraints and propagating via setFrameSize: instead, + * NSWindow has no constraint-based minimum and respects setContentMinSize: only. */ +@interface ElResizableContentView : NSView +@end +@implementation ElResizableContentView +- (NSSize)fittingSize { return NSMakeSize(1.0, 1.0); } +- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric, NSViewNoIntrinsicMetric); } +@end + +/* ElWindow subclass — overrides minSize so the AL constraint graph cannot + * inflate the resize floor beyond the manifest-specified minimum. + * NSWindow normally sets minSize from the larger of the explicit setMinSize: + * call and the AL-computed minimum content size; the override replaces that + * with the explicit minimum only. */ +@interface ElWindow : NSWindow +@property (nonatomic) NSSize elMinFrameSize; +@property (nonatomic) NSSize elInitialContentSize; /* designed width/height from manifest */ +@end +@implementation ElWindow +- (NSSize)minSize { return _elMinFrameSize; } +@end + +/* ── NSApplication setup ──────────────────────────────────────────────────── */ + +@interface ElAppDelegate : NSObject +@end + +@implementation ElAppDelegate +- (void)applicationDidFinishLaunching:(NSNotification*)notif { + /* Nothing needed — windows are created programmatically before run loop. */ +} +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app { + return YES; +} +@end + +static ElAppDelegate* _el_app_delegate = nil; + +/* ── Public C API (called from el_seed.c) ─────────────────────────────────── */ + +/* + * el_appkit_init — initialize NSApplication. Must be called once on the main + * thread before any other el_appkit_* function. Idempotent. + */ +void el_appkit_init(void) { + static int done = 0; + if (done) return; + done = 1; + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + _el_app_delegate = [[ElAppDelegate alloc] init]; + [NSApp setDelegate:_el_app_delegate]; +} + +/* + * el_appkit_window_create — create an NSWindow and return its handle. + * title, width, height, min_width, min_height come from manifest.el app{} block. + */ +int64_t el_appkit_window_create(const char* title, int width, int height, + int min_width, int min_height) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSRect frame = NSMakeRect(0, 0, (CGFloat)width, (CGFloat)height); + NSWindowStyleMask style = + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable; + ElWindow* win = [[ElWindow alloc] + initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; + /* Store the manifest minimum as our explicit frame floor. */ + NSRect minContentRect = NSMakeRect(0, 0, (CGFloat)min_width, (CGFloat)min_height); + NSSize minFrame = [win frameRectForContentRect:minContentRect].size; + win.elMinFrameSize = minFrame; + [win setContentMinSize:NSMakeSize((CGFloat)min_width, (CGFloat)min_height)]; + win.elInitialContentSize = NSMakeSize((CGFloat)width, (CGFloat)height); + [win center]; + + ElResizableContentView* root = [[ElResizableContentView alloc] initWithFrame:frame]; + [win setContentView:root]; + + handle = el_widget_alloc(EL_WIDGET_WINDOW, win); + [win release]; + [root release]; + }); + return handle; +} + +/* + * el_appkit_window_show — make the window visible and bring it to front. + */ +void el_appkit_window_show(int64_t handle) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + NSWindow* win = (NSWindow*)w->obj; + /* AL may have shrunk the window during the build phase. + * Restore the designed content size before showing — the window + * is not yet visible so the user never sees the shrunken state. + * After this, AL runs within the correct frame and required + * 4-edge constraints keep the child filling on every resize. */ + if ([win isKindOfClass:[ElWindow class]]) { + ElWindow* elWin = (ElWindow*)win; + NSSize cs = elWin.elInitialContentSize; + if (cs.width > 0) { + [win setContentSize:cs]; + [win center]; + } + } + [win makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + }); +} + +/* + * el_appkit_window_set_title — update window title at runtime. + */ +void el_appkit_window_set_title(int64_t handle, const char* title) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + NSWindow* win = (NSWindow*)w->obj; + [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; + }); +} + +/* + * el_appkit_run_loop — start the NSApplication run loop. Never returns. + * Must be called from the main thread as the last step of main(). + */ +void el_appkit_run_loop(void) { + [NSApp run]; +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t make_stack(NSUserInterfaceLayoutOrientation orient, + CGFloat spacing) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSStackView* sv = [[NSStackView alloc] initWithFrame:NSZeroRect]; + [sv setOrientation:orient]; + /* Alignment must match the stack's cross-axis to avoid NSStackView + * silently flipping orientation. For a vertical stack, align children + * to the leading edge (left); for a horizontal stack, align to top. + * Using NSLayoutAttributeLeading on a horizontal stack flips it vertical. */ + NSLayoutAttribute align = (orient == NSUserInterfaceLayoutOrientationVertical) + ? NSLayoutAttributeLeading : NSLayoutAttributeTop; + [sv setAlignment:align]; + [sv setSpacing:spacing]; + [sv setEdgeInsets:NSEdgeInsetsZero]; + [sv setDistribution:NSStackViewDistributionFill]; + [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; + ElWidgetKind kind = (orient == NSUserInterfaceLayoutOrientationVertical) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + handle = el_widget_alloc(kind, sv); + [sv release]; + }); + return handle; +} + +int64_t el_appkit_vstack_create(int spacing) { + return make_stack(NSUserInterfaceLayoutOrientationVertical, (CGFloat)spacing); +} + +int64_t el_appkit_hstack_create(int spacing) { + return make_stack(NSUserInterfaceLayoutOrientationHorizontal, (CGFloat)spacing); +} + +int64_t el_appkit_zstack_create(void) { + /* ZStack = NSView with manual child positioning (no stack manager). */ + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSView* v = [[NSView alloc] initWithFrame:NSZeroRect]; + [v setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); + [v release]; + }); + return handle; +} + +int64_t el_appkit_scroll_create(void) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSScrollView* sv = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + [sv setHasVerticalScroller:YES]; + [sv setHasHorizontalScroller:NO]; + [sv setAutohidesScrollers:YES]; + [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); + [sv release]; + }); + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_appkit_label_create(const char* text) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSTextField* lbl = [NSTextField labelWithString: + [NSString stringWithUTF8String:text ? text : ""]]; + [lbl setEditable:NO]; + [lbl setBordered:NO]; + [lbl setDrawsBackground:NO]; + [lbl setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); + }); + return handle; +} + +int64_t el_appkit_button_create(const char* label) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSButton* btn = [[NSButton alloc] initWithFrame:NSZeroRect]; + [btn setButtonType:NSButtonTypeMomentaryLight]; + [btn setBezelStyle:NSBezelStyleShadowlessSquare]; + [btn setTitle:[NSString stringWithUTF8String:label ? label : ""]]; + [btn setAlignment:NSTextAlignmentLeft]; + [btn setTranslatesAutoresizingMaskIntoConstraints:NO]; + + /* Create action target. */ + ElButtonTarget* target = [[ElButtonTarget alloc] init]; + /* handle assigned below after alloc — set after */ + [btn setTarget:target]; + [btn setAction:@selector(buttonClicked:)]; + + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); + target.widgetHandle = h; + _el_delegates[h] = target; /* retain */ + handle = h; + [btn release]; + }); + return handle; +} + +int64_t el_appkit_text_field_create(const char* placeholder) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSTextField* tf = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [tf setTranslatesAutoresizingMaskIntoConstraints:NO]; + if (placeholder && *placeholder) { + [tf setPlaceholderString:[NSString stringWithUTF8String:placeholder]]; + } + + ElTextFieldDelegate* del = [[ElTextFieldDelegate alloc] init]; + [tf setDelegate:del]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); + del.widgetHandle = h; + _el_delegates[h] = del; + handle = h; + [tf release]; + }); + return handle; +} + +int64_t el_appkit_text_area_create(const char* placeholder) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSScrollView* scroll = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + [scroll setHasVerticalScroller:YES]; + [scroll setAutohidesScrollers:YES]; + [scroll setTranslatesAutoresizingMaskIntoConstraints:NO]; + + NSTextView* tv = [[NSTextView alloc] initWithFrame:NSZeroRect]; + [tv setMinSize:NSMakeSize(0, 60)]; + [tv setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [tv setVerticallyResizable:YES]; + [tv setHorizontallyResizable:NO]; + [tv setAutoresizingMask:NSViewWidthSizable]; + if (placeholder && *placeholder) { + /* NSTextView has no native placeholder; we set a hint via + * a custom accessibility description as a minimal approach. */ + [tv setAccessibilityPlaceholderValue: + [NSString stringWithUTF8String:placeholder]]; + } + [[tv textContainer] setWidthTracksTextView:YES]; + [scroll setDocumentView:tv]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, scroll); + handle = h; + [scroll release]; + [tv release]; + }); + return handle; +} + +int64_t el_appkit_image_create(const char* path_or_name) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSImage* img = nil; + if (path_or_name && *path_or_name) { + /* Try as filesystem path first, then as named system image. */ + img = [[NSImage alloc] initWithContentsOfFile: + [NSString stringWithUTF8String:path_or_name]]; + if (!img) { + img = [NSImage imageNamed:[NSString stringWithUTF8String:path_or_name]]; + if (img) [img retain]; + } + } + NSImageView* iv = [[NSImageView alloc] initWithFrame:NSZeroRect]; + [iv setTranslatesAutoresizingMaskIntoConstraints:NO]; + if (img) { + [iv setImage:img]; + [img release]; + } + handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); + [iv release]; + }); + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_appkit_widget_set_text(int64_t handle, const char* text) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = [NSString stringWithUTF8String:text ? text : ""]; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setStringValue:s]; + break; + case EL_WIDGET_BUTTON: + [(NSButton*)w->obj setTitle:s]; + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setString:s]; + break; + } + case EL_WIDGET_WINDOW: + [(NSWindow*)w->obj setTitle:s]; + break; + default: break; + } + }); +} + +const char* el_appkit_widget_get_text(int64_t handle) { + __block const char* result = ""; + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = nil; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + s = [(NSTextField*)w->obj stringValue]; + break; + case EL_WIDGET_BUTTON: + s = [(NSButton*)w->obj title]; + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + s = [tv string]; + break; + } + default: break; + } + if (s) result = strdup([s UTF8String]); + }); + return result; +} + +void el_appkit_widget_set_color(int64_t handle, float r, float g, float b, float a) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r + green:(CGFloat)g + blue:(CGFloat)b + alpha:(CGFloat)a]; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setTextColor:c]; + break; + case EL_WIDGET_BUTTON: { + /* Update foreground color on attributed title, preserving font. */ + NSButton* btn = (NSButton*)w->obj; + NSAttributedString* existing = [btn attributedTitle]; + NSMutableAttributedString* as; + if (existing && [existing length] > 0) { + as = [[NSMutableAttributedString alloc] initWithAttributedString:existing]; + } else { + NSString* t = [btn title]; + if (!t) t = @""; + as = [[NSMutableAttributedString alloc] initWithString:t]; + } + [as addAttribute:NSForegroundColorAttributeName value:c + range:NSMakeRange(0, [as length])]; + [btn setAttributedTitle:as]; + [as release]; + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: { + /* Set background via wantsLayer + layer background. */ + NSView* v = (NSView*)w->obj; + [v setWantsLayer:YES]; + [v layer].backgroundColor = [c CGColor]; + break; + } + default: break; + } + }); +} + +void el_appkit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r + green:(CGFloat)g + blue:(CGFloat)b + alpha:(CGFloat)a]; + if (w->kind == EL_WIDGET_WINDOW) { + NSWindow* win = (NSWindow*)w->obj; + [win setBackgroundColor:c]; + /* Also paint the contentView layer so it shows through NSStackView. */ + NSView* cv = [win contentView]; + [cv setWantsLayer:YES]; + [cv layer].backgroundColor = [c CGColor]; + return; + } + if (w->kind == EL_WIDGET_BUTTON) { + /* Disable native bezel so CALayer background is visible. */ + NSButton* btn = (NSButton*)w->obj; + [btn setBordered:NO]; + [btn setWantsLayer:YES]; + [btn layer].backgroundColor = [c CGColor]; + return; + } + NSView* v = (NSView*)w->obj; + if (w->kind == EL_WIDGET_SCROLL) { + /* NSScrollView needs setBackgroundColor, not just layer bg. */ + NSScrollView* sv = (NSScrollView*)v; + [sv setBackgroundColor:c]; + [sv setDrawsBackground:YES]; + } + [v setWantsLayer:YES]; + [v layer].backgroundColor = [c CGColor]; + }); +} + +void el_appkit_widget_set_font(int64_t handle, const char* family, int size, int bold) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSFont* font; + if (bold) { + font = [NSFont boldSystemFontOfSize:(CGFloat)size]; + } else if (family && *family && strcmp(family, "system") != 0) { + font = [NSFont fontWithName:[NSString stringWithUTF8String:family] + size:(CGFloat)size]; + if (!font) font = [NSFont systemFontOfSize:(CGFloat)size]; + } else { + font = [NSFont systemFontOfSize:(CGFloat)size]; + } + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setFont:font]; + break; + case EL_WIDGET_BUTTON: + /* NSButton: set font via attributed title, preserving existing foreground color. */ + { + NSButton* fbtn = (NSButton*)w->obj; + NSAttributedString* fexisting = [fbtn attributedTitle]; + NSMutableAttributedString* as; + if (fexisting && [fexisting length] > 0) { + as = [[NSMutableAttributedString alloc] initWithAttributedString:fexisting]; + } else { + NSString* ft = [fbtn title]; + if (!ft) ft = @""; + as = [[NSMutableAttributedString alloc] initWithString:ft]; + } + [as addAttribute:NSFontAttributeName value:font + range:NSMakeRange(0, [as length])]; + [fbtn setAttributedTitle:as]; + [as release]; + } + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setFont:font]; + break; + } + default: break; + } + }); +} + +void el_appkit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* Padding on stack views maps to edgeInsets. */ + if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { + NSStackView* sv = (NSStackView*)w->obj; + [sv setEdgeInsets:NSEdgeInsetsMake((CGFloat)top, (CGFloat)left, + (CGFloat)bottom, (CGFloat)right)]; + } + /* For individual widgets we set a custom layout margin via constraints. + * For now, NSTextView respects textContainerInset. */ + if (w->kind == EL_WIDGET_TEXTAREA) { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setTextContainerInset:NSMakeSize((CGFloat)(left + right) / 2, + (CGFloat)(top + bottom) / 2)]; + } + }); +} + +void el_appkit_widget_set_width(int64_t handle, int width) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + /* Add a fixed-width constraint. */ + NSLayoutConstraint* c = [NSLayoutConstraint + constraintWithItem:v + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:(CGFloat)width]; + c.priority = NSLayoutPriorityDefaultHigh; + [v addConstraint:c]; + }); +} + +void el_appkit_widget_set_height(int64_t handle, int height) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + NSLayoutConstraint* c = [NSLayoutConstraint + constraintWithItem:v + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:(CGFloat)height]; + c.priority = NSLayoutPriorityDefaultHigh; + [v addConstraint:c]; + }); +} + +void el_appkit_widget_set_flex(int64_t handle, int flex) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + if (flex > 0) { + /* flex:1 → low hugging (wants to expand) + low compression resistance + * (willing to be compressed). This makes NSStackViewDistributionFill + * choose this view to grow/shrink to fill available space. */ + [v setContentHuggingPriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentHuggingPriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationVertical]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationVertical]; + } else { + /* flex:0 → high hugging (stays at natural size) + high compression + * resistance (resists shrinking). */ + [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationVertical]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationVertical]; + } + }); +} + +void el_appkit_widget_set_corner_radius(int64_t handle, int radius) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + [v setWantsLayer:YES]; + [v layer].cornerRadius = (CGFloat)radius; + [v layer].masksToBounds = YES; + }); +} + +void el_appkit_widget_set_disabled(int64_t handle, int disabled) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind == EL_WIDGET_BUTTON) { + [(NSButton*)w->obj setEnabled:!disabled]; + } else if (w->kind == EL_WIDGET_TEXTFIELD) { + [(NSTextField*)w->obj setEnabled:!disabled]; + } + }); +} + +void el_appkit_widget_set_hidden(int64_t handle, int hidden) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v setHidden:(BOOL)hidden]; + } + }); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_appkit_widget_add_child — attach child widget to parent. + * + * - Window: child is added to contentView (NSStackView). + * - VStack/HStack: child is added as an arranged subview. + * - ZStack/generic view: child is added as a plain subview. + * - ScrollView: child becomes the documentView (only first child honoured). + */ +void el_appkit_widget_add_child(int64_t parent, int64_t child) { + el_appkit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw) return; + + NSView* childView = (NSView*)cw->obj; + if ([childView isKindOfClass:[NSWindow class]]) return; /* can't add window as child */ + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + NSWindow* win = (NSWindow*)pw->obj; + NSView* cv = [win contentView]; + /* Standard required 4-edge constraints so the child always + * fills the content view during user-initiated resize. + * AL may shrink the window during the build phase (before show), + * but el_appkit_window_show calls setContentSize: to restore + * the designed size before making the window visible. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [cv addSubview:childView]; + [NSLayoutConstraint activateConstraints:@[ + [childView.topAnchor constraintEqualToAnchor:cv.topAnchor], + [childView.bottomAnchor constraintEqualToAnchor:cv.bottomAnchor], + [childView.leadingAnchor constraintEqualToAnchor:cv.leadingAnchor], + [childView.trailingAnchor constraintEqualToAnchor:cv.trailingAnchor], + ]]; + break; + } + case EL_WIDGET_VSTACK: { + NSStackView* sv = (NSStackView*)pw->obj; + [sv addArrangedSubview:childView]; + /* Pin child leading+trailing to fill cross-axis (width) of the VStack. + * Priority 999 avoids intrinsic-size feedback through NSWindow. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + NSLayoutConstraint* vl = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeLeading + multiplier:1.0 constant:0.0]; + NSLayoutConstraint* vr = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeTrailing + multiplier:1.0 constant:0.0]; + vl.priority = 999; + vr.priority = 999; + [sv addConstraints:@[vl, vr]]; + break; + } + case EL_WIDGET_HSTACK: { + NSStackView* sv = (NSStackView*)pw->obj; + [sv addArrangedSubview:childView]; + /* Pin child top+bottom to fill cross-axis (height) of the HStack. + * Priority 499 (well below required 1000) lets the window frame + * win over child intrinsic size, avoiding NSWindow auto-resize. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + NSLayoutConstraint* ht = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeTop + multiplier:1.0 constant:0.0]; + NSLayoutConstraint* hb = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeBottom + multiplier:1.0 constant:0.0]; + ht.priority = 499; + hb.priority = 499; + [sv addConstraints:@[ht, hb]]; + break; + } + case EL_WIDGET_SCROLL: { + NSScrollView* sv = (NSScrollView*)pw->obj; + /* Wrap the child in a flipped view so content starts at the + * visual top-left. Without flipping, NSScrollView places the + * document origin at the bottom-left and lists appear inverted. */ + ElFlippedView* flipper = [[ElFlippedView alloc] initWithFrame:NSZeroRect]; + [flipper setTranslatesAutoresizingMaskIntoConstraints:NO]; + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [flipper addSubview:childView]; + /* Pin child to all 4 edges of the flipper. */ + NSArray* hc = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[c]|" + options:0 metrics:nil views:@{@"c": childView}]; + NSArray* vc = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[c]|" + options:0 metrics:nil views:@{@"c": childView}]; + [flipper addConstraints:hc]; + [flipper addConstraints:vc]; + [sv setDocumentView:flipper]; + /* Constrain flipper width to the scroll view so items fill the + * full panel width (vertical-only scrolling). */ + NSLayoutConstraint* wc = [NSLayoutConstraint + constraintWithItem:flipper attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeWidth + multiplier:1.0 constant:0.0]; + [sv addConstraint:wc]; + [flipper release]; + break; + } + case EL_WIDGET_ZSTACK: + default: { + NSView* pv = (NSView*)pw->obj; + [pv addSubview:childView]; + break; + } + } + }); +} + +/* + * el_appkit_widget_remove_child — remove a child widget from its parent. + */ +void el_appkit_widget_remove_child(int64_t parent, int64_t child) { + el_appkit_sync_main(^{ + ElWidget* cw = el_widget_get(child); + if (!cw) return; + NSView* v = (NSView*)cw->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v removeFromSuperview]; + } + }); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_appkit_widget_set_data(int64_t handle, const char* data_str) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->user_data); + w->user_data = data_str && *data_str ? strdup(data_str) : NULL; +} + +void el_appkit_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_appkit_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_appkit_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit = pressing Enter → stored in cb_click slot. */ + el_appkit_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_appkit_widget_destroy(int64_t handle) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v removeFromSuperview]; + } else { + [(NSWindow*)w->obj close]; + } + [_el_delegates[handle] release]; + _el_delegates[handle] = nil; + el_widget_free(handle); + }); +} diff --git a/lang/el-compiler/runtime/el_gtk4.c b/lang/el-compiler/runtime/el_gtk4.c new file mode 100644 index 0000000..d427786 --- /dev/null +++ b/lang/el-compiler/runtime/el_gtk4.c @@ -0,0 +1,1327 @@ +/* + * el_gtk4.c — GTK4 backend for the el native widget system. + * + * This file implements the GTK4 widget layer that el_seed.c calls through to + * when EL_TARGET_LINUX is defined. It runs on both Linux and macOS (Homebrew). + * + * 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). + * + * macOS lifecycle: + * On macOS the GTK4 Quartz backend sets the NSApplication main menu during + * g_application_run → startup signal, which requires the main thread. The el + * program's compiled main() calls native_init(), then window/widget creation, + * then native_run_loop() — so g_application_run() IS called from the main + * thread in el_gtk4_run_loop(). + * + * The constraint is that GTK widget creation requires an initialized display, + * which only exists after g_application_run starts. To handle this, all + * el_gtk4_* calls made before el_gtk4_run_loop() are DEFERRED into a queue. + * The activate handler replays the queue, then the app runs normally. + * + * Threading (post-activation): + * 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; + +/* + * el_widget_alloc — allocate a slot for a fully-constructed widget. + * Takes a strong ref. Returns slot index or -1 if full. + */ +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; +} + +/* + * el_widget_prealloc — reserve a slot without a GTK widget (pre-activation). + * The slot kind is set; widget remains NULL until el_widget_install() fills it. + * Returns the reserved slot index, or -1 if full. + */ +static int64_t el_widget_prealloc(ElWidgetKind kind) { + 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 = NULL; + _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; +} + +/* + * el_widget_install — install a GTK widget into a pre-allocated slot. + * Takes a strong ref on w. + */ +static void el_widget_install(int64_t slot, GtkWidget* w) { + if (slot <= 0 || slot >= EL_GTK4_MAX_WIDGETS) return; + if (_el_widgets[slot].widget) g_object_unref(_el_widgets[slot].widget); + _el_widgets[slot].widget = (GtkWidget*)g_object_ref(w); +} + +/* 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; +} + +/* ── Deferred operation queue ─────────────────────────────────────────────── */ +/* + * Before el_gtk4_run_loop() starts g_application_run(), the GTK display is not + * yet initialized. All el_gtk4_* calls are recorded as deferred ops and + * replayed from the activate handler after the display is ready. + * + * Each deferred op is { fn, data } where fn is a _main-style function and data + * is a heap-allocated copy of the arguments. The _main functions are called + * exactly as they would be post-activation. + * + * Creation functions (window_create, label_create, etc.) pre-allocate a slot + * via el_widget_prealloc() before recording the deferred op. The slot index is + * returned immediately to the el program. The _main function receives the + * pre-allocated slot as part of its arg struct and calls el_widget_install() + * rather than el_widget_alloc(). + */ + +#define EL_DEFERRED_MAX 8192 + +typedef struct { + void (*fn)(void*); + void* data; +} ElDeferredOp; + +static ElDeferredOp _el_deferred[EL_DEFERRED_MAX]; +static int _el_deferred_count = 0; + +/* Queue a deferred op. data must be heap-allocated (will be free()'d after replay). */ +static void el_defer(void (*fn)(void*), void* data) { + if (_el_deferred_count >= EL_DEFERRED_MAX) { + fprintf(stderr, "el_gtk4: deferred op queue full\n"); + return; + } + _el_deferred[_el_deferred_count].fn = fn; + _el_deferred[_el_deferred_count].data = data; + _el_deferred_count++; +} + +/* Replay all deferred ops in order. Called from the activate handler. */ +static void el_deferred_replay(void) { + for (int i = 0; i < _el_deferred_count; i++) { + _el_deferred[i].fn(_el_deferred[i].data); + free(_el_deferred[i].data); + } + _el_deferred_count = 0; +} + +/* ── GtkApplication (global) ─────────────────────────────────────────────── */ + +static GtkApplication* _el_app = NULL; +static int _el_app_running = 0; /* 1 after activate fires */ + +/* ── 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, ...). + * + * NOTE: only called post-activation. Pre-activation calls go through el_defer(). + */ + +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) { + /* 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); +} + +/* ── 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 : "")); +} + +/* ── 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); +} + +/* + * _el_on_close_request — prevent spurious window-close events on macOS. + * + * On macOS, the GTK4 Quartz backend sends windowShouldClose: messages to + * GtkApplicationWindows when the app is launched from a terminal or when + * NSApplication sends automatic hide/close notifications. These arrive as + * close-request signals and would cause the app to exit prematurely. + * + * Returning TRUE from close-request tells GTK to NOT destroy the window. + * This is correct default behavior for el apps — windows stay open until + * the el program explicitly destroys them or the process exits. + * + * If the el program needs user-closeable windows, it can connect its own + * close-request handler (registered after this one) that returns FALSE to + * allow the close. + */ +static gboolean _el_on_close_request(GtkWindow* window, gpointer user_data) { + (void)window; (void)user_data; + /* Return TRUE: prevent the default close/destroy action. */ + return TRUE; +} + +/* ── GtkApplication activate callback ───────────────────────────────────── */ + +static void _el_app_activate(GtkApplication* app, gpointer user_data) { + /* + * Replay all deferred widget operations. These are the window/widget + * creation calls made during the el boot sequence before native_run_loop(). + * The display is now initialized, so GTK APIs are safe. + * + * g_application_hold() prevents the GApplication from auto-quitting when + * there are temporarily no open windows (e.g., during the replay phase + * before gtk_window_present is called on the first window). + */ + (void)user_data; + /* + * Hold the GApplication during replay to prevent auto-quit if there are + * temporarily no windows between the start of replay and gtk_window_present. + */ + g_application_hold(G_APPLICATION(app)); + _el_app_running = 1; + el_deferred_replay(); + g_application_release(G_APPLICATION(app)); +} + +/* ── 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() on the main + * thread. The el boot sequence runs on the main thread, so this call is always + * from the main thread. The deferred queue mechanism handles the fact that all + * widget creation occurred before this call. + */ +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 slot; /* pre-allocated slot; _main installs widget here */ +} _ElWindowCreateArgs; + +static void _el_window_create_main(void* vp) { + _ElWindowCreateArgs* a = (_ElWindowCreateArgs*)vp; + GtkWidget* win = gtk_application_window_new(_el_app); + 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); + + if (a->slot >= 0) { + el_widget_install(a->slot, win); + g_object_unref(win); /* el_widget_install took a ref */ + } + /* Connect close-request handler to prevent macOS GTK4 Quartz auto-close. */ + g_signal_connect(win, "close-request", + G_CALLBACK(_el_on_close_request), NULL); +} + +int64_t el_gtk4_window_create(const char* title, int width, int height, + int min_width, int min_height) { + int64_t slot = el_widget_prealloc(EL_WIDGET_WINDOW); + if (!_el_app_running) { + _ElWindowCreateArgs* a = malloc(sizeof(_ElWindowCreateArgs)); + a->title = title ? strdup(title) : NULL; + a->width = width; + a->height = height; + a->min_width = min_width; + a->min_height = min_height; + a->slot = slot; + el_defer(_el_window_create_main, a); + } else { + _ElWindowCreateArgs a = { title, width, height, min_width, min_height, slot }; + el_gtk4_sync_main(_el_window_create_main, &a); + } + return slot; +} + +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 || !w->widget) return; + gtk_window_present(GTK_WINDOW(w->widget)); +} + +void el_gtk4_window_show(int64_t handle) { + if (!_el_app_running) { + _ElHandleArgs* a = malloc(sizeof(_ElHandleArgs)); + a->handle = handle; + el_defer(_el_window_show_main, a); + } else { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_window_show_main, &a); + } +} + +typedef struct { int64_t handle; 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 || !w->widget) 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) { + if (!_el_app_running) { + _ElSetTitleArgs* a = malloc(sizeof(_ElSetTitleArgs)); + a->handle = handle; + a->title = title ? strdup(title) : NULL; + el_defer(_el_window_set_title_main, a); + } else { + _ElSetTitleArgs a = { handle, (char*)title }; + el_gtk4_sync_main(_el_window_set_title_main, &a); + } +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +typedef struct { + GtkOrientation orient; + int spacing; + int64_t slot; +} _ElStackCreateArgs; + +static void _el_stack_create_main(void* vp) { + _ElStackCreateArgs* a = (_ElStackCreateArgs*)vp; + GtkWidget* box = gtk_box_new(a->orient, a->spacing); + if (a->slot >= 0) { + el_widget_install(a->slot, box); + g_object_unref(box); + } +} + +int64_t el_gtk4_vstack_create(int spacing) { + int64_t slot = el_widget_prealloc(EL_WIDGET_VSTACK); + if (!_el_app_running) { + _ElStackCreateArgs* a = malloc(sizeof(_ElStackCreateArgs)); + a->orient = GTK_ORIENTATION_VERTICAL; + a->spacing = spacing; + a->slot = slot; + el_defer(_el_stack_create_main, a); + } else { + _ElStackCreateArgs a = { GTK_ORIENTATION_VERTICAL, spacing, slot }; + el_gtk4_sync_main(_el_stack_create_main, &a); + } + return slot; +} + +int64_t el_gtk4_hstack_create(int spacing) { + int64_t slot = el_widget_prealloc(EL_WIDGET_HSTACK); + if (!_el_app_running) { + _ElStackCreateArgs* a = malloc(sizeof(_ElStackCreateArgs)); + a->orient = GTK_ORIENTATION_HORIZONTAL; + a->spacing = spacing; + a->slot = slot; + el_defer(_el_stack_create_main, a); + } else { + _ElStackCreateArgs a = { GTK_ORIENTATION_HORIZONTAL, spacing, slot }; + el_gtk4_sync_main(_el_stack_create_main, &a); + } + return slot; +} + +typedef struct { int64_t slot; } _ElSlotArgs; + +static void _el_zstack_create_main(void* vp) { + _ElSlotArgs* a = (_ElSlotArgs*)vp; + GtkWidget* overlay = gtk_overlay_new(); + if (a->slot >= 0) { el_widget_install(a->slot, overlay); g_object_unref(overlay); } +} + +int64_t el_gtk4_zstack_create(void) { + int64_t slot = el_widget_prealloc(EL_WIDGET_ZSTACK); + if (!_el_app_running) { + _ElSlotArgs* a = malloc(sizeof(_ElSlotArgs)); a->slot = slot; + el_defer(_el_zstack_create_main, a); + } else { + _ElSlotArgs a = { slot }; + el_gtk4_sync_main(_el_zstack_create_main, &a); + } + return slot; +} + +static void _el_scroll_create_main(void* vp) { + _ElSlotArgs* a = (_ElSlotArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + if (a->slot >= 0) { el_widget_install(a->slot, sw); g_object_unref(sw); } +} + +int64_t el_gtk4_scroll_create(void) { + int64_t slot = el_widget_prealloc(EL_WIDGET_SCROLL); + if (!_el_app_running) { + _ElSlotArgs* a = malloc(sizeof(_ElSlotArgs)); a->slot = slot; + el_defer(_el_scroll_create_main, a); + } else { + _ElSlotArgs a = { slot }; + el_gtk4_sync_main(_el_scroll_create_main, &a); + } + return slot; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +typedef struct { char* text; int64_t slot; } _ElTextSlotArgs; + +static void _el_label_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)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); + if (a->slot >= 0) { el_widget_install(a->slot, lbl); g_object_unref(lbl); } +} + +int64_t el_gtk4_label_create(const char* text) { + int64_t slot = el_widget_prealloc(EL_WIDGET_LABEL); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = text ? strdup(text) : NULL; a->slot = slot; + el_defer(_el_label_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)text, slot }; + el_gtk4_sync_main(_el_label_create_main, &a); + } + return slot; +} + +static void _el_button_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* btn = gtk_button_new_with_label(a->text ? a->text : ""); + if (a->slot >= 0) { + el_widget_install(a->slot, btn); + g_object_unref(btn); + g_signal_connect(btn, "clicked", + G_CALLBACK(_el_on_button_clicked), + (gpointer)(intptr_t)a->slot); + } +} + +int64_t el_gtk4_button_create(const char* label) { + int64_t slot = el_widget_prealloc(EL_WIDGET_BUTTON); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = label ? strdup(label) : NULL; a->slot = slot; + el_defer(_el_button_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)label, slot }; + el_gtk4_sync_main(_el_button_create_main, &a); + } + return slot; +} + +static void _el_text_field_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* entry = gtk_entry_new(); + if (a->text && *a->text) { + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), a->text); + } + if (a->slot >= 0) { + el_widget_install(a->slot, entry); + g_object_unref(entry); + gpointer slot_ptr = (gpointer)(intptr_t)a->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); + } +} + +int64_t el_gtk4_text_field_create(const char* placeholder) { + int64_t slot = el_widget_prealloc(EL_WIDGET_TEXTFIELD); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = placeholder ? strdup(placeholder) : NULL; a->slot = slot; + el_defer(_el_text_field_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)placeholder, slot }; + el_gtk4_sync_main(_el_text_field_create_main, &a); + } + return slot; +} + +static void _el_text_area_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)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); + + if (a->slot >= 0) { + el_widget_install(a->slot, sw); + g_object_unref(sw); + 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)a->slot); + } +} + +int64_t el_gtk4_text_area_create(const char* placeholder) { + int64_t slot = el_widget_prealloc(EL_WIDGET_TEXTAREA); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = placeholder ? strdup(placeholder) : NULL; a->slot = slot; + el_defer(_el_text_area_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)placeholder, slot }; + el_gtk4_sync_main(_el_text_area_create_main, &a); + } + return slot; +} + +static void _el_image_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)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(); + } + if (a->slot >= 0) { el_widget_install(a->slot, img); g_object_unref(img); } +} + +int64_t el_gtk4_image_create(const char* path_or_name) { + int64_t slot = el_widget_prealloc(EL_WIDGET_IMAGE); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = path_or_name ? strdup(path_or_name) : NULL; a->slot = slot; + el_defer(_el_image_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)path_or_name, slot }; + el_gtk4_sync_main(_el_image_create_main, &a); + } + return slot; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +typedef struct { int64_t handle; 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 || !w->widget) 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) { + if (!_el_app_running) { + _ElWidgetTextArgs* a = malloc(sizeof(_ElWidgetTextArgs)); + a->handle = handle; a->text = text ? strdup(text) : NULL; + el_defer(_el_widget_set_text_main, a); + } else { + _ElWidgetTextArgs a = { handle, (char*)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 || !w->widget) { 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 || !w->widget) 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) { + if (!_el_app_running) { + _ElColorArgs* args = malloc(sizeof(_ElColorArgs)); + *args = ((_ElColorArgs){ handle, r, g, b, a }); + el_defer(_el_widget_set_color_main, args); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElColorArgs* args = malloc(sizeof(_ElColorArgs)); + *args = ((_ElColorArgs){ handle, r, g, b, a }); + el_defer(_el_widget_set_bg_color_main, args); + } else { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_bg_color_main, &args); + } +} + +typedef struct { + int64_t handle; + 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 || !w->widget) 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) { + if (!_el_app_running) { + _ElFontArgs* a = malloc(sizeof(_ElFontArgs)); + a->handle = handle; a->family = family ? strdup(family) : NULL; + a->size = size; a->bold = bold; + el_defer(_el_widget_set_font_main, a); + } else { + _ElFontArgs a = { handle, (char*)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 || !w->widget) 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) { + if (!_el_app_running) { + _ElPaddingArgs* a = malloc(sizeof(_ElPaddingArgs)); + *a = ((_ElPaddingArgs){ handle, top, right, bottom, left }); + el_defer(_el_widget_set_padding_main, a); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = width; + el_defer(_el_widget_set_width_main, a); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = height; + el_defer(_el_widget_set_height_main, a); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = flex; + el_defer(_el_widget_set_flex_main, a); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = radius; + el_defer(_el_widget_set_corner_radius_main, a); + } else { + _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 || !w->widget) return; + gtk_widget_set_sensitive(w->widget, !a->value); +} + +void el_gtk4_widget_set_disabled(int64_t handle, int disabled) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = disabled; + el_defer(_el_widget_set_disabled_main, a); + } else { + _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->widget || 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) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = hidden; + el_defer(_el_widget_set_hidden_main, a); + } else { + _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 || !pw->widget || !cw->widget) 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) { + if (!_el_app_running) { + _ElParentChildArgs* a = malloc(sizeof(_ElParentChildArgs)); + a->parent = parent; a->child = child; + el_defer(_el_widget_add_child_main, a); + } else { + _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 || !pw->widget || !cw->widget) 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) { + if (!_el_app_running) { + _ElParentChildArgs* a = malloc(sizeof(_ElParentChildArgs)); + a->parent = parent; a->child = child; + el_defer(_el_widget_remove_child_main, a); + } else { + _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. + * + * These are safe to call pre-activation because they only touch the slot table, + * not GTK objects. We still defer them to ensure ordering with widget creation. + */ + +typedef struct { int64_t handle; char* fn_name; } _ElCbArgs; + +static void _el_widget_on_click_main(void* vp) { + _ElCbArgs* a = (_ElCbArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (a->fn_name && *a->fn_name) ? strdup(a->fn_name) : NULL; +} + +void el_gtk4_widget_on_click(int64_t handle, const char* fn_name) { + if (!_el_app_running) { + _ElCbArgs* a = malloc(sizeof(_ElCbArgs)); + a->handle = handle; a->fn_name = fn_name ? strdup(fn_name) : NULL; + el_defer(_el_widget_on_click_main, a); + } else { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + } +} + +static void _el_widget_on_change_main(void* vp) { + _ElCbArgs* a = (_ElCbArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (a->fn_name && *a->fn_name) ? strdup(a->fn_name) : NULL; +} + +void el_gtk4_widget_on_change(int64_t handle, const char* fn_name) { + if (!_el_app_running) { + _ElCbArgs* a = malloc(sizeof(_ElCbArgs)); + a->handle = handle; a->fn_name = fn_name ? strdup(fn_name) : NULL; + el_defer(_el_widget_on_change_main, a); + } else { + 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 || !w->widget) 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) { + if (!_el_app_running) { + _ElHandleArgs* a = malloc(sizeof(_ElHandleArgs)); + a->handle = handle; + el_defer(_el_widget_destroy_main, a); + } else { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_widget_destroy_main, &a); + } +} + +#endif /* EL_TARGET_LINUX */ diff --git a/lang/el-compiler/runtime/el_lvgl.c b/lang/el-compiler/runtime/el_lvgl.c new file mode 100644 index 0000000..4c3a318 --- /dev/null +++ b/lang/el-compiler/runtime/el_lvgl.c @@ -0,0 +1,844 @@ +/* + * el_lvgl.c — LVGL v9 backend for the el native widget system. + * + * This file implements the microcontroller/embedded widget layer that el_seed.c + * calls through to when EL_TARGET_LVGL is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_lvgl_* C functions defined here + * → lv_obj_t widgets via LVGL v9 + * + * Target platforms: ESP32, STM32, industrial panels, any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles: every widget is assigned an int64_t slot index into + * g_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is reserved; -1 = invalid handle. + * + * Window model: on embedded there is one screen. __window_create configures + * lv_scr_act() as the root container. __window_show is a no-op (the screen + * is always visible). __native_run_loop calls lv_task_handler() in a tight + * loop — on RTOS this runs inside a dedicated task; on bare metal it IS the + * main loop. The host application is responsible for initialising the display + * driver and calling lv_tick_inc() before calling __native_run_loop. + * + * Callback dispatch: + * When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing): + * dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime. + * When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32): + * The caller must provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * which maps function names to function pointers via a compile-time table. + * + * Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font + * maps the requested point size to the nearest available Montserrat variant. + * Bold is approximated by stepping up two sizes (no separate bold face in the + * default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select(). + * + * Compile (hosted test build): + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a / lvgl source tree. + * + * Compile (bare-metal, no dynamic linker): + * arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \ + * -I./lvgl el_lvgl.c -c -o el_lvgl.o + */ + +#ifdef EL_TARGET_LVGL + +#include "lvgl/lvgl.h" +#include +#include +#include +#include + +#ifndef EL_LVGL_NO_DLSYM +#include +#endif + +#include "el_runtime.h" + +/* ── Callback dispatch macro ─────────────────────────────────────────────── */ + +#ifdef EL_LVGL_NO_DLSYM +/* + * Bare-metal path. The application provides this function: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * It maps string names → function pointers, typically via a switch on a hash + * or a sorted table of {name, fn_ptr} pairs generated by elc. + */ +extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); +#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b)) +#else +/* + * Hosted path. dlsym resolves the El symbol at call time. + * We use a compound-statement expression (GCC/Clang extension) to avoid + * executing dlsym more than once per call. + */ +#define EL_LVGL_CALL(fn_name, a, b) \ + ({ \ + typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \ + _el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \ + _fn ? _fn((a), (b)) : (el_val_t)0; \ + }) +#endif + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_LVGL_MAX_WIDGETS 4096 + +/* + * Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay + * consistent across all targets. + */ +typedef enum { + EL_LVGL_FREE = 0, + EL_LVGL_WINDOW = 1, + EL_LVGL_VSTACK = 2, + EL_LVGL_HSTACK = 3, + EL_LVGL_ZSTACK = 4, + EL_LVGL_SCROLL = 5, + EL_LVGL_LABEL = 6, + EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */ + EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */ + EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */ + EL_LVGL_IMAGE = 10, + EL_LVGL_DIVIDER = 11, + EL_LVGL_SPACER = 12, +} ElLvglKind; + +/* + * Per-slot state. Callback names are stored inline (256 bytes each) to avoid + * heap allocation on targets with no malloc or fragmented heaps. + */ +typedef struct { + ElLvglKind kind; + lv_obj_t *obj; /* primary LVGL object */ + lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */ + char cb_click[256]; + char cb_change[256]; + char cb_submit[256]; +} ElLvglWidget; + +static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS]; + +/* ── Slot helpers ─────────────────────────────────────────────────────────── */ + +static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) { + for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) { + if (g_widgets[i].kind == EL_LVGL_FREE) { + g_widgets[i].kind = kind; + g_widgets[i].obj = obj; + g_widgets[i].btn_label = NULL; + g_widgets[i].cb_click[0] = '\0'; + g_widgets[i].cb_change[0] = '\0'; + g_widgets[i].cb_submit[0] = '\0'; + return (int64_t)i; + } + } + return -1; /* table full */ +} + +static ElLvglWidget *lvgl_slot_get(int64_t handle) { + if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL; + if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL; + return &g_widgets[handle]; +} + +static void lvgl_slot_free(int64_t handle) { + if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return; + ElLvglWidget *w = &g_widgets[handle]; + w->kind = EL_LVGL_FREE; + w->obj = NULL; + w->btn_label = NULL; + w->cb_click[0] = '\0'; + w->cb_change[0] = '\0'; + w->cb_submit[0] = '\0'; +} + +/* ── Font selection ───────────────────────────────────────────────────────── */ +/* + * LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h): + * 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 + * + * We map the requested size to the nearest available size. Bold is + * approximated by stepping up two sizes (no separate bold face in the default + * font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely. + */ +#ifndef EL_LVGL_CUSTOM_FONT + +static const lv_font_t *font_select(int size, int bold) { + /* Step up two sizes for bold approximation. */ + if (bold) size += 4; + + /* Clamp to available range. */ + if (size < 8) size = 8; + if (size > 48) size = 48; + + /* Round to nearest even size >= 8. */ + if (size % 2 != 0) size++; + + switch (size) { +#if LV_FONT_MONTSERRAT_8 + case 8: return &lv_font_montserrat_8; +#endif +#if LV_FONT_MONTSERRAT_10 + case 10: return &lv_font_montserrat_10; +#endif +#if LV_FONT_MONTSERRAT_12 + case 12: return &lv_font_montserrat_12; +#endif +#if LV_FONT_MONTSERRAT_14 + case 14: return &lv_font_montserrat_14; +#endif +#if LV_FONT_MONTSERRAT_16 + case 16: return &lv_font_montserrat_16; +#endif +#if LV_FONT_MONTSERRAT_18 + case 18: return &lv_font_montserrat_18; +#endif +#if LV_FONT_MONTSERRAT_20 + case 20: return &lv_font_montserrat_20; +#endif +#if LV_FONT_MONTSERRAT_22 + case 22: return &lv_font_montserrat_22; +#endif +#if LV_FONT_MONTSERRAT_24 + case 24: return &lv_font_montserrat_24; +#endif +#if LV_FONT_MONTSERRAT_26 + case 26: return &lv_font_montserrat_26; +#endif +#if LV_FONT_MONTSERRAT_28 + case 28: return &lv_font_montserrat_28; +#endif +#if LV_FONT_MONTSERRAT_30 + case 30: return &lv_font_montserrat_30; +#endif +#if LV_FONT_MONTSERRAT_32 + case 32: return &lv_font_montserrat_32; +#endif +#if LV_FONT_MONTSERRAT_34 + case 34: return &lv_font_montserrat_34; +#endif +#if LV_FONT_MONTSERRAT_36 + case 36: return &lv_font_montserrat_36; +#endif +#if LV_FONT_MONTSERRAT_38 + case 38: return &lv_font_montserrat_38; +#endif +#if LV_FONT_MONTSERRAT_40 + case 40: return &lv_font_montserrat_40; +#endif +#if LV_FONT_MONTSERRAT_42 + case 42: return &lv_font_montserrat_42; +#endif +#if LV_FONT_MONTSERRAT_44 + case 44: return &lv_font_montserrat_44; +#endif +#if LV_FONT_MONTSERRAT_46 + case 46: return &lv_font_montserrat_46; +#endif +#if LV_FONT_MONTSERRAT_48 + case 48: return &lv_font_montserrat_48; +#endif + default: + /* + * Requested size is not compiled in. Fall back to the default + * theme font, which is guaranteed to be present. + */ + return LV_FONT_DEFAULT; + } +} + +#endif /* EL_LVGL_CUSTOM_FONT */ + +/* ── Event callback ───────────────────────────────────────────────────────── */ + +/* + * Single LVGL event callback used for all widget events. The user_data is + * the slot index cast to (void*) via intptr_t — avoids heap allocation. + * + * Three event codes are handled: + * LV_EVENT_CLICKED → cb_click (buttons, any tappable widget) + * LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.) + * LV_EVENT_READY → cb_submit (Enter pressed in textarea one-line mode) + */ +static void el_lvgl_event_cb(lv_event_t *e) { + lv_event_code_t code = lv_event_get_code(e); + intptr_t slot = (intptr_t)lv_event_get_user_data(e); + ElLvglWidget *w = lvgl_slot_get((int64_t)slot); + if (!w) return; + + if (code == LV_EVENT_CLICKED && w->cb_click[0]) { + EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0); + } + + if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) { + /* + * Retrieve current text for textarea/textfield widgets so the handler + * receives the updated value as its second argument. + */ + const char *txt = ""; + lv_obj_t *target = lv_event_get_target(e); + if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) { + txt = lv_textarea_get_text(target); + if (!txt) txt = ""; + } + EL_LVGL_CALL(w->cb_change, (el_val_t)slot, + (el_val_t)(uintptr_t)txt); + } + + if (code == LV_EVENT_READY && w->cb_submit[0]) { + /* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */ + EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0); + } +} + +/* ── Initialisation ───────────────────────────────────────────────────────── */ + +/* + * el_lvgl_init — call lv_init(). The host must have already initialised the + * display driver and input driver before this, or immediately after. Idempotent. + */ +void el_lvgl_init(void) { + static int done = 0; + if (done) return; + done = 1; + lv_init(); +} + +/* + * el_lvgl_run_loop — drive lv_task_handler() indefinitely. + * + * On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task. + * On bare metal: call this as the last statement of main(). + * + * The 5 ms delay between handler calls matches the LVGL documentation + * recommendation for a ~200 Hz refresh budget. + * + * On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS + * targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc. + */ +void el_lvgl_run_loop(void) { + for (;;) { + lv_task_handler(); +#if defined(EL_LVGL_RTOS_DELAY) + EL_LVGL_RTOS_DELAY(5); +#elif defined(__linux__) || defined(__APPLE__) + { + /* Hosted test build — usleep available. */ +#include + usleep(5000); + } +#endif + /* Bare-metal without a delay macro: the HAL tick increment loop + * is the caller's responsibility. No sleep needed if lv_tick_inc() + * is driven from a hardware timer ISR. */ + } +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +/* + * el_lvgl_window_create — configure lv_scr_act() as a vertical flex container + * and return a slot handle wrapping it. The title is stored for informational + * purposes (e.g., a status bar widget the host might create). Width/height + * are ignored on embedded targets because the screen size is fixed by the + * display driver; they are accepted for API compatibility with other backends. + */ +int64_t el_lvgl_window_create(const char *title, int width, int height, + int min_width, int min_height) { + (void)width; (void)height; (void)min_width; (void)min_height; + + lv_obj_t *scr = lv_scr_act(); + + /* Configure the active screen as a vertical flex container so that + * widgets added via __widget_add_child stack naturally. */ + lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100)); + + /* Store the window title in a user-data string on the screen object + * so host code can retrieve it if it wants to render a title bar. */ + if (title && *title) { + /* lv_obj_set_user_data stores a void* — we cast the string pointer. + * The string must outlive the screen object; for literals this is + * always true. For dynamic titles use el_lvgl_window_set_title. */ + lv_obj_set_user_data(scr, (void *)(uintptr_t)title); + } + + /* Allocate a slot for the screen object. */ + int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr); + return h; +} + +/* + * el_lvgl_window_show — no-op on embedded. The screen is always visible. + */ +void el_lvgl_window_show(int64_t handle) { + (void)handle; + /* On multi-screen setups, load the screen: */ + /* ElLvglWidget *w = lvgl_slot_get(handle); + * if (w) lv_scr_load(w->obj); */ +} + +/* + * el_lvgl_window_set_title — update the user_data pointer on the screen object. + * On embedded, "title" is typically only used by a custom host title bar. + */ +void el_lvgl_window_set_title(int64_t handle, const char *title) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w || w->kind != EL_LVGL_WINDOW) return; + lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : "")); +} + +/* ── Layout containers ────────────────────────────────────────────────────── */ + +/* + * el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing. + */ +int64_t el_lvgl_vstack_create(int spacing) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + /* Remove default LVGL border and background so containers are transparent + * by default, matching the AppKit/GTK4 backends. */ + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_VSTACK, obj); +} + +/* + * el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing. + */ +int64_t el_lvgl_hstack_create(int spacing) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_HSTACK, obj); +} + +/* + * el_lvgl_zstack_create — plain container, children positioned absolutely. + * No flex flow is set; callers use lv_obj_set_pos() on children directly, + * or rely on their natural 0,0 origin. + */ +int64_t el_lvgl_zstack_create(void) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj); +} + +/* + * el_lvgl_scroll_create — vertically scrollable container. + */ +int64_t el_lvgl_scroll_create(void) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_scroll_dir(obj, LV_DIR_VER); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_SCROLL, obj); +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +/* + * el_lvgl_label_create — static text label. + */ +int64_t el_lvgl_label_create(const char *text) { + lv_obj_t *obj = lv_label_create(lv_scr_act()); + lv_label_set_text(obj, text ? text : ""); + return lvgl_slot_alloc(EL_LVGL_LABEL, obj); +} + +/* + * el_lvgl_button_create — pressable button with a child label. + * + * LVGL buttons are containers; text is placed in an inner lv_label child. + * We store the child label pointer in btn_label so set_text / get_text can + * reach it without searching the object tree at runtime. + */ +int64_t el_lvgl_button_create(const char *label) { + lv_obj_t *btn = lv_btn_create(lv_scr_act()); + lv_obj_t *lbl = lv_label_create(btn); + lv_label_set_text(lbl, label ? label : ""); + lv_obj_center(lbl); + + int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn); + if (h >= 0) { + g_widgets[h].btn_label = lbl; + /* Register click callback immediately so button responds when a + * callback name is registered later via __widget_on_click. */ + lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_text_field_create — single-line text input. + */ +int64_t el_lvgl_text_field_create(const char *placeholder) { + lv_obj_t *obj = lv_textarea_create(lv_scr_act()); + lv_textarea_set_one_line(obj, true); + if (placeholder && *placeholder) { + lv_textarea_set_placeholder_text(obj, placeholder); + } + int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj); + if (h >= 0) { + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)h); + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_text_area_create — multi-line text input. + */ +int64_t el_lvgl_text_area_create(const char *placeholder) { + lv_obj_t *obj = lv_textarea_create(lv_scr_act()); + lv_textarea_set_one_line(obj, false); + if (placeholder && *placeholder) { + lv_textarea_set_placeholder_text(obj, placeholder); + } + int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj); + if (h >= 0) { + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_image_create — image widget. + * + * On hosted Linux (SDL backend), path is a filesystem path. + * On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin". + * LVGL image decoders are registered separately by the host application. + */ +int64_t el_lvgl_image_create(const char *path_or_name) { + lv_obj_t *obj = lv_img_create(lv_scr_act()); + if (path_or_name && *path_or_name) { + lv_img_set_src(obj, path_or_name); + } + return lvgl_slot_alloc(EL_LVGL_IMAGE, obj); +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +/* + * el_lvgl_widget_set_text — update visible text. + * + * Dispatch per kind: + * LABEL → lv_label_set_text + * BUTTON → lv_label_set_text on inner btn_label child + * TEXTFIELD / TEXTAREA → lv_textarea_set_text + * WINDOW → lv_obj_set_user_data (stores title string) + */ +void el_lvgl_widget_set_text(int64_t handle, const char *text) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + const char *t = text ? text : ""; + switch (w->kind) { + case EL_LVGL_LABEL: + lv_label_set_text(w->obj, t); + break; + case EL_LVGL_BUTTON: + if (w->btn_label) lv_label_set_text(w->btn_label, t); + break; + case EL_LVGL_TEXTFIELD: + case EL_LVGL_TEXTAREA: + lv_textarea_set_text(w->obj, t); + break; + case EL_LVGL_WINDOW: + lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t); + break; + default: + break; + } +} + +/* + * el_lvgl_widget_get_text — retrieve visible text. + * + * Returns a pointer into LVGL's internal storage — valid until the next LVGL + * operation that modifies the widget. Callers that need to hold the value + * across LVGL calls must strdup() it. + */ +const char *el_lvgl_widget_get_text(int64_t handle) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return ""; + switch (w->kind) { + case EL_LVGL_LABEL: + return lv_label_get_text(w->obj); + case EL_LVGL_BUTTON: + return w->btn_label ? lv_label_get_text(w->btn_label) : ""; + case EL_LVGL_TEXTFIELD: + case EL_LVGL_TEXTAREA: + return lv_textarea_get_text(w->obj); + default: + return ""; + } +} + +/* + * el_lvgl_widget_set_color — foreground (text) colour. + * + * r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h). + * LVGL lv_color_make takes uint8_t 0–255 components. + */ +void el_lvgl_widget_set_color(int64_t handle, + float r, float g, float b, float a) { + (void)a; /* LVGL text colour has no per-glyph alpha channel */ + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_color_t c = lv_color_make( + (uint8_t)(r * 255.0f + 0.5f), + (uint8_t)(g * 255.0f + 0.5f), + (uint8_t)(b * 255.0f + 0.5f)); + lv_obj_set_style_text_color(w->obj, c, 0); + if (w->kind == EL_LVGL_BUTTON && w->btn_label) { + lv_obj_set_style_text_color(w->btn_label, c, 0); + } +} + +/* + * el_lvgl_widget_set_bg_color — background fill colour + opacity. + */ +void el_lvgl_widget_set_bg_color(int64_t handle, + float r, float g, float b, float a) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_color_t c = lv_color_make( + (uint8_t)(r * 255.0f + 0.5f), + (uint8_t)(g * 255.0f + 0.5f), + (uint8_t)(b * 255.0f + 0.5f)); + lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f); + lv_obj_set_style_bg_color(w->obj, c, 0); + lv_obj_set_style_bg_opa(w->obj, opa, 0); +} + +/* + * el_lvgl_widget_set_font — apply font to text-bearing widget. + * + * The `family` parameter is accepted for API compatibility but LVGL uses + * compiled-in fonts only. Only the size and bold flag have effect unless + * EL_LVGL_CUSTOM_FONT is defined by the host. + */ +void el_lvgl_widget_set_font(int64_t handle, + const char *family, int size, int bold) { + (void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */ + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + const lv_font_t *font = font_select(size, bold); + if (!font) return; + lv_obj_set_style_text_font(w->obj, font, 0); + if (w->kind == EL_LVGL_BUTTON && w->btn_label) { + lv_obj_set_style_text_font(w->btn_label, font, 0); + } +} + +/* + * el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left). + */ +void el_lvgl_widget_set_padding(int64_t handle, + int top, int right, int bottom, int left) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0); + lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0); + lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0); + lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0); +} + +/* + * el_lvgl_widget_set_width — set explicit pixel width. + */ +void el_lvgl_widget_set_width(int64_t handle, int width) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_width(w->obj, (lv_coord_t)width); +} + +/* + * el_lvgl_widget_set_height — set explicit pixel height. + */ +void el_lvgl_widget_set_height(int64_t handle, int height) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_height(w->obj, (lv_coord_t)height); +} + +/* + * el_lvgl_widget_set_flex — set flex grow factor. + * + * flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill + * remaining space proportional to its grow factor. + * flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size. + */ +void el_lvgl_widget_set_flex(int64_t handle, int flex) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0)); +} + +/* + * el_lvgl_widget_set_corner_radius — set border radius. + */ +void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0); +} + +/* + * el_lvgl_widget_set_disabled — enable/disable interactive state. + * + * LV_STATE_DISABLED greys out the widget and prevents input events. + */ +void el_lvgl_widget_set_disabled(int64_t handle, int disabled) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + if (disabled) { + lv_obj_add_state(w->obj, LV_STATE_DISABLED); + } else { + lv_obj_clear_state(w->obj, LV_STATE_DISABLED); + } +} + +/* + * el_lvgl_widget_set_hidden — show/hide widget. + * + * LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow. + */ +void el_lvgl_widget_set_hidden(int64_t handle, int hidden) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + if (hidden) { + lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN); + } +} + +/* ── Tree operations ──────────────────────────────────────────────────────── */ + +/* + * el_lvgl_widget_add_child — attach child widget to parent. + * + * lv_obj_set_parent() reparents the child object inside the LVGL tree. + * For WINDOW parents we use the screen object itself as the parent, since + * lv_scr_act() IS the root container. + */ +void el_lvgl_widget_add_child(int64_t parent, int64_t child) { + ElLvglWidget *pw = lvgl_slot_get(parent); + ElLvglWidget *cw = lvgl_slot_get(child); + if (!pw || !cw) return; + lv_obj_set_parent(cw->obj, pw->obj); +} + +/* + * el_lvgl_widget_remove_child — detach child from its current parent. + * + * LVGL has no explicit "remove from parent without deleting" operation. + * We reparent the child back to the active screen (making it a root-level + * floating widget) and then hide it. The widget still occupies a slot and + * can be re-attached or destroyed later. + */ +void el_lvgl_widget_remove_child(int64_t parent, int64_t child) { + (void)parent; + ElLvglWidget *cw = lvgl_slot_get(child); + if (!cw) return; + /* Move to screen root and hide. */ + lv_obj_set_parent(cw->obj, lv_scr_act()); + lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN); +} + +/* + * el_lvgl_widget_destroy — delete widget and its children from the LVGL tree, + * then free the slot. + * + * lv_obj_del() recursively deletes the object and all children. After this + * call the handle is invalid and must not be used. + */ +void el_lvgl_widget_destroy(int64_t handle) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_del(w->obj); + lvgl_slot_free(handle); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +/* + * Event registration stores the El function name in the widget slot. The + * actual lv_obj_add_event_cb() call is made here (or was made in the factory + * for buttons/textfields where we know the relevant event codes upfront). + * + * For widgets that did not register their event callback in the factory (e.g. + * labels receiving a click handler), we add the LVGL event binding now. + */ + +void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_click, fn_name ? fn_name : "", 255); + w->cb_click[255] = '\0'; + /* + * Buttons already have LV_EVENT_CLICKED registered in the factory. + * For other widget kinds (labels, containers used as tap targets), add + * the click flag and register the callback. + */ + if (w->kind != EL_LVGL_BUTTON) { + lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED, + (void *)(intptr_t)handle); + } +} + +void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_change, fn_name ? fn_name : "", 255); + w->cb_change[255] = '\0'; + /* + * Textfield/textarea factories already register VALUE_CHANGED. + * For other kinds (e.g. a custom toggle), add the binding now. + */ + if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) { + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)handle); + } +} + +void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_submit, fn_name ? fn_name : "", 255); + w->cb_submit[255] = '\0'; + /* + * LV_EVENT_READY fires when Enter is pressed in a one-line textarea. + * Textfield factories already register READY. For other kinds, add it. + */ + if (w->kind != EL_LVGL_TEXTFIELD) { + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY, + (void *)(intptr_t)handle); + } +} + +#endif /* EL_TARGET_LVGL */ diff --git a/lang/el-compiler/runtime/el_native_target.h b/lang/el-compiler/runtime/el_native_target.h new file mode 100644 index 0000000..7daabb7 --- /dev/null +++ b/lang/el-compiler/runtime/el_native_target.h @@ -0,0 +1,574 @@ +/* + * 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); +void __widget_set_data(el_val_t handle, el_val_t data_str); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/lang/el-compiler/runtime/el_runtime_win32.c b/lang/el-compiler/runtime/el_runtime_win32.c new file mode 100644 index 0000000..4588839 --- /dev/null +++ b/lang/el-compiler/runtime/el_runtime_win32.c @@ -0,0 +1,1146 @@ +/* + * el_runtime_win32.c — Minimal Win32-compatible el runtime stub. + * + * Provides only the symbols that native-hello (and the el-native vessel) call + * from el_runtime.h. Replaces the full el_runtime.c + el_seed.c for + * cross-compiled Win32 targets where POSIX headers (pthread, curl, dlfcn, + * sys/socket, etc.) are unavailable. + * + * Symbols provided: + * Allocators: el_strdup, el_strbuf, el_wrap_str (static helpers) + * I/O: println, print + * String: el_str_concat, str_eq, str_starts_with, str_len, str_concat, + * int_to_str, str_to_int, native_str_to_int, str_slice, str_trim, + * str_contains, str_ends_with, str_to_upper, str_to_lower, + * str_index_of, str_char_at, str_char_code, str_replace, + * str_pad_left, str_pad_right, str_upper, str_lower + * Math: el_abs, el_max, el_min, int_to_float, float_to_int + * List: el_list_empty, el_list_new, el_list_len, el_list_get, + * el_list_append, el_list_clone + * Map: el_map_new, el_map_get, el_get_field, el_map_set + * Retain/RC: el_retain, el_release + * Arena: el_arena_push, el_arena_pop + * JSON: json_get_string, json_get_int, json_get_float, json_get_bool, + * json_get_raw, json_find_key (static), json_skip_value (static) + * Process: exit_program, env, el_runtime_init_args + * Stubs: All remaining el_runtime.h symbols as no-op / return-0 stubs + * so the linker is satisfied even if the app doesn't call them. + * + * el_request_start / el_request_end are provided as empty no-ops (no HTTP + * server, no per-request arena needed for a native GUI app). + * + * Compile: + * x86_64-w64-mingw32-gcc -std=c11 -DEL_TARGET_WIN32 -I \ + * -c el_runtime_win32.c -o el_runtime_win32.o + */ + +#include "el_runtime.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ── Arena (single-threaded; no pthread needed) ─────────────────────────── */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static ElArena _arena = {NULL, 0, 0}; +static int _arena_active = 0; + +static void el_arena_track(char* p) { + if (!_arena_active || !p) return; + if (_arena.count >= _arena.cap) { + size_t nc = _arena.cap == 0 ? EL_ARENA_INITIAL : _arena.cap * 2; + char** g = realloc(_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _arena.ptrs = g; + _arena.cap = nc; + } + _arena.ptrs[_arena.count++] = p; +} + +void el_request_start(void) { _arena.count = 0; _arena_active = 1; } +void el_request_end(void) { + _arena_active = 0; + for (size_t i = 0; i < _arena.count; i++) free(_arena.ptrs[i]); + _arena.count = 0; +} + +#define EL_ARENA_SCOPE_DEPTH 32 +static size_t _arena_scope[EL_ARENA_SCOPE_DEPTH]; +static int _arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_arena_active) _arena_active = 1; + if (_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) + _arena_scope[_arena_scope_depth++] = _arena.count; + return (el_val_t)(int64_t)_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _arena.count) save = 0; + for (size_t i = save; i < _arena.count; i++) { + if (_arena.ptrs[i]) { free(_arena.ptrs[i]); _arena.ptrs[i] = NULL; } + } + _arena.count = save; + if (_arena_scope_depth > 0) _arena_scope_depth--; + if (save == 0) _arena_active = 0; + return 0; +} + +/* ── Allocator helpers ───────────────────────────────────────────────────── */ + +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_win32: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +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); + puts(str ? str : ""); + 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)); +} + +/* ── 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), 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* p = EL_CSTR(prefv); + if (!s || !p) return 0; + return (el_val_t)(strncmp(s, p, strlen(p)) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s || !suf) return 0; + size_t ls = strlen(s), lsuf = strlen(suf); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suf) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + return s ? (el_val_t)strlen(s) : 0; +} + +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); +} + +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), lf = strlen(from), lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + /* Count occurrences */ + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t new_len = ls + count * (lt > lf ? lt - lf : 0) - count * (lf > lt ? lf - lt : 0); + char* out = el_strbuf(new_len); + const char* src = s; + char* dst = out; + while ((p = strstr(src, from)) != NULL) { + size_t pre = (size_t)(p - src); + memcpy(dst, src, pre); dst += pre; + memcpy(dst, to, lt); dst += lt; + src = p + lf; + } + strcpy(dst, src); + 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_upper(el_val_t sv) { return str_to_upper(sv); } +el_val_t str_lower(el_val_t sv) { return str_to_lower(sv); } + +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); +} + +el_val_t str_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) return -1; + const char* p = strstr(s, sub); + return p ? (el_val_t)(p - s) : -1; +} + +el_val_t str_char_at(el_val_t sv, el_val_t i) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t idx = (int64_t)i; + int64_t len = (int64_t)strlen(s); + if (idx < 0 || idx >= len) return 0; + char buf[2] = { s[idx], '\0' }; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t sv, el_val_t i) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t idx = (int64_t)i; + int64_t len = (int64_t)strlen(s); + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)s[idx]; +} + +el_val_t str_pad_left(el_val_t sv, el_val_t widthv, el_val_t padv) { + const char* s = EL_CSTR(sv); + const char* pad = EL_CSTR(padv); + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t width = (int64_t)widthv; + int64_t slen = (int64_t)strlen(s); + if (slen >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - slen; + char* out = el_strbuf((size_t)width); + size_t plen = strlen(pad); + for (int64_t i = 0; i < need; i++) out[i] = pad[i % plen]; + memcpy(out + need, s, (size_t)slen); + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_right(el_val_t sv, el_val_t widthv, el_val_t padv) { + const char* s = EL_CSTR(sv); + const char* pad = EL_CSTR(padv); + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t width = (int64_t)widthv; + int64_t slen = (int64_t)strlen(s); + if (slen >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - slen; + char* out = el_strbuf((size_t)width); + memcpy(out, s, (size_t)slen); + size_t plen = strlen(pad); + for (int64_t i = 0; i < need; i++) out[slen + i] = pad[i % plen]; + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_format(el_val_t fmtv, el_val_t datav) { + /* Minimal stub — just return the format string unchanged. */ + return fmtv; +} + +el_val_t str_split(el_val_t sv, el_val_t sepv) { + /* Return empty list stub. */ + return el_list_empty(); +} + +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 lsub = strlen(sub); + int64_t cnt = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { cnt++; p += lsub; } + return (el_val_t)cnt; +} + +/* ── 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; } + +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 float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} +el_val_t str_to_float(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return el_from_float(strtod(s, NULL)); +} +el_val_t format_float(el_val_t f, el_val_t decimals) { + char buf[128]; + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; if (d > 30) d = 30; + 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) { return format_float(f, decimals); } +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(log10(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.14159265358979323846); } + +/* ── Refcounted heap objects ─────────────────────────────────────────────── */ + +#define EL_MAGIC_LIST 0xE15710A1u +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { uint32_t magic; uint32_t refcount; } ElHeader; + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +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; + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; + if (p & 0x7) return 0; + 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); h->magic = 0; 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); h->magic = 0; free(m); + } + } +} + +/* ── List ────────────────────────────────────────────────────────────────── */ + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime_win32: OOM\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_win32: OOM\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 lv) { + ElList* l = (ElList*)(uintptr_t)lv; + return l ? l->length : 0; +} + +el_val_t el_list_get(el_val_t lv, el_val_t idx) { + ElList* l = (ElList*)(uintptr_t)lv; + if (!l || idx < 0 || idx >= l->length) return 0; + return l->elems[idx]; +} + +el_val_t el_list_append(el_val_t lv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)lv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t nc = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* g = realloc(old->elems, (size_t)nc * sizeof(el_val_t)); + if (!g) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + old->elems = g; + old->capacity = nc; + } + old->elems[old->length++] = elem; + return lv; + } + int64_t nc = old->length + 1; + if (nc < 4) nc = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = nc; + fresh->elems = malloc((size_t)nc * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime_win32: OOM\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 lv) { + ElList* old = (ElList*)(uintptr_t)lv; + if (!old) return el_list_empty(); + ElList* fresh = list_alloc(old->capacity > 0 ? old->capacity : 4); + if (old->length > 0) + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + fresh->length = old->length; + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime_win32: OOM\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_win32: OOM\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); +} + +el_val_t el_map_get(el_val_t mv, el_val_t kv) { + ElMap* m = (ElMap*)(uintptr_t)mv; + const char* key = EL_CSTR(kv); + 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 mv, el_val_t kv) { return el_map_get(mv, kv); } + +el_val_t el_map_set(el_val_t mv, el_val_t kv, el_val_t value) { + ElMap* m = (ElMap*)(uintptr_t)mv; + if (!m) { + ElMap* fresh = map_alloc(4); + fresh->keys[0] = kv; fresh->values[0] = value; fresh->count = 1; + return EL_STR(fresh); + } + const char* key = EL_CSTR(kv); + 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 mv; } + } + } + if (m->count >= m->capacity) { + int64_t nc = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)nc * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)nc * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + m->keys = gk; m->values = gv; m->capacity = nc; + } + m->keys[m->count] = kv; m->values[m->count] = value; m->count++; + return mv; +} + +/* ── JSON helpers ────────────────────────────────────────────────────────── */ + +typedef struct { + const char* p; + const char* end; + int err; +} JsonParser; + +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 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_win32: OOM\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': + 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_win32: OOM\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 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, close = (open == '{') ? '}' : ']'; + int depth = 0, in_str = 0, 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) return p; continue; } + p++; + } + return p; + } + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +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, in_str = 0, 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 == '"') { + p++; in_str = 0; continue; + } + p++; continue; + } + if (c == '"') { + 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 (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; +} + +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("")); + JsonParser jp = { p, json + (json ? strlen(json) : 0), 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 || *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 || *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; + return (el_val_t)(strncmp(p, "true", 4) == 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); + 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_get(el_val_t json, el_val_t key) { + return json_get_raw(json, key); +} + +/* Simple JSON number / scalar parser helpers for json_parse / json_stringify */ + +el_val_t json_parse(el_val_t s) { + /* Minimal: return the string verbatim (caller accesses via json_get_*). */ + return s; +} + +el_val_t json_stringify(el_val_t v) { + /* Basic: if it already looks like a string, return it. */ + const char* s = EL_CSTR(v); + return s ? v : EL_STR("{}"); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + /* Stub: returns the original JSON unchanged. */ + return json_str; +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s || *s != '[') return 0; + int64_t cnt = 0; + int depth = 0, in_str = 0, e = 0; + const char* p = s; + 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 == '[' || c == '{') { if (depth++ == 0 && c == '[') { p++; continue; } } + else if (c == ']' || c == '}') { if (--depth == 0) break; } + else if (c == ',' && depth == 1) cnt++; + p++; + } + /* If we saw any content, count = separators + 1 */ + if (depth == 0) { /* empty or done */ } + /* Simpler: count commas at depth 1 + 1 if non-empty */ + /* Re-scan for simplicity */ + cnt = 0; + depth = 0; in_str = 0; e = 0; + p = s; + int has_content = 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 == '[' || c == '{') depth++; + else if (c == ']' || c == '}') { + depth--; + if (depth == 0) break; + } + if (depth == 1) { + if (c != '[' && c != ' ' && c != '\t' && c != '\n' && c != '\r') has_content = 1; + if (c == ',') cnt++; + } + p++; + } + return has_content ? cnt + 1 : 0; +} + +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + if (!s || *s != '[') return 0; + int64_t target = (int64_t)index; + int64_t cur = 0; + int depth = 0, in_str = 0, e = 0; + const char* p = s; + 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; } + if (c == '[' || c == '{') depth++; + else if (c == ']' || c == '}') { if (--depth == 0) break; } + if (depth == 1 && (p == s + 1 || c == ',')) { + if (c == ',') { cur++; p++; while (*p == ' ' || *p == '\t' || *p == '\n') p++; } + else { p++; while (*p == ' ' || *p == '\t' || *p == '\n') p++; } + if (cur == target) { + 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); + } + continue; + } + p++; + } + return 0; +} + +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); + if (!raw) return el_wrap_str(el_strdup("")); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return raw; + JsonParser jp = { s, s + strlen(s), 0 }; + char* parsed = jp_parse_string_raw(&jp); + return el_wrap_str(parsed); +} + +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); + char* out = el_strbuf(n * 6 + 4); + char* p = out; + *p++ = '"'; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': *p++ = '\\'; *p++ = '"'; break; + case '\\': *p++ = '\\'; *p++ = '\\'; break; + case '\n': *p++ = '\\'; *p++ = 'n'; break; + case '\r': *p++ = '\\'; *p++ = 'r'; break; + case '\t': *p++ = '\\'; *p++ = 't'; break; + default: + if (c < 0x20) { + p += sprintf(p, "\\u%04x", c); + } else { + *p++ = (char)c; + } + } + } + *p++ = '"'; *p = '\0'; + return el_wrap_str(out); +} + +el_val_t json_build_object(el_val_t kvs) { return kvs; } +el_val_t json_build_array(el_val_t items) { return items; } + +/* ── Environment / process ───────────────────────────────────────────────── */ + +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 : "")); +} + +el_val_t exit_program(el_val_t code) { + exit((int)(int64_t)code); + return 0; +} + +/* Args store — populated by el_runtime_init_args */ +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; +} + +/* ── HTML helpers (stubs) ────────────────────────────────────────────────── */ + +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json) { return input_html; } +el_val_t html_raw(el_val_t s) { return s; } +el_val_t html_escape(el_val_t s) { return s; } +el_val_t url_encode(el_val_t s) { return s; } +el_val_t url_decode(el_val_t s) { return s; } + +/* ── Filesystem stubs ────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return el_wrap_str(el_strdup("")); + FILE* f = fopen(p, "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("")); } + char* buf = malloc((size_t)sz + 1); + if (!buf) { fclose(f); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + el_arena_track(buf); + return EL_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; + fputs(c, f); + fclose(f); + return 1; +} + +el_val_t fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fclose(f); + return 1; +} + +el_val_t fs_list(el_val_t path) { return el_wrap_str(el_strdup("")); } +el_val_t fs_list_json(el_val_t path) { return el_wrap_str(el_strdup("[]")); } +el_val_t fs_mkdir(el_val_t path) { return 0; } +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return 0; } + +/* ── HTTP stubs (no network in Win32 GUI app) ────────────────────────────── */ + +el_val_t http_get(el_val_t url) { return el_wrap_str(el_strdup("{\"error\":\"no http\"}")); } +el_val_t http_post(el_val_t url, el_val_t body) { return http_get(url); } +el_val_t http_post_json(el_val_t url, el_val_t body) { return http_get(url); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { return http_get(url); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { return http_get(url); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { return http_get(url); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { return http_get(url); } +el_val_t http_delete(el_val_t url) { return http_get(url); } +el_val_t http_serve(el_val_t port, el_val_t handler) { return 0; } +el_val_t http_set_handler(el_val_t name) { return 0; } +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { return 0; } +el_val_t http_set_handler_v2(el_val_t name) { return 0; } +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return body; } +void el_seed_set_http_conn_fd(int fd) { (void)fd; } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t out) { return 0; } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t out) { return 0; } + +/* ── Time stubs ──────────────────────────────────────────────────────────── */ + +#include + +el_val_t time_now(void) { return (el_val_t)time(NULL); } +el_val_t time_now_utc(void) { return time_now(); } +el_val_t now_ns(void) { return (el_val_t)(time(NULL) * (int64_t)1000000000LL); } +el_val_t el_now_instant(void) { return now_ns(); } +el_val_t now(void) { return el_now_instant(); } +el_val_t unix_seconds(el_val_t n) { return n * 1000000000LL; } +el_val_t unix_millis(el_val_t n) { return n * 1000000LL; } +el_val_t unix_timestamp(void) { return (el_val_t)time(NULL); } +el_val_t sleep_secs(el_val_t secs) { return 0; } +el_val_t sleep_ms(el_val_t ms) { return 0; } +el_val_t el_sleep_duration(el_val_t dur) { return 0; } +el_val_t time_format(el_val_t ts, el_val_t fmt) { return el_wrap_str(el_strdup("")); } +el_val_t time_to_parts(el_val_t ts) { return el_wrap_str(el_strdup("{}")); } +el_val_t time_from_parts(el_val_t s, el_val_t ns, el_val_t tz) { return s; } +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { return ts; } +el_val_t time_diff(el_val_t a, el_val_t b, el_val_t unit) { return a - b; } +el_val_t instant_from_iso8601(el_val_t s) { return 0; } +el_val_t el_duration_from_nanos(el_val_t ns) { return ns; } +el_val_t duration_seconds(el_val_t n) { return n * 1000000000LL; } +el_val_t duration_millis(el_val_t n) { return n * 1000000LL; } +el_val_t duration_nanos(el_val_t n) { return n; } +el_val_t el_instant_add_dur(el_val_t i, el_val_t d) { return i + d; } +el_val_t el_instant_sub_dur(el_val_t i, el_val_t d) { return i - d; } +el_val_t el_instant_diff(el_val_t a, el_val_t b) { return a - b; } +el_val_t el_duration_add(el_val_t a, el_val_t b) { return a + b; } +el_val_t el_duration_sub(el_val_t a, el_val_t b) { return a - b; } +el_val_t el_duration_scale(el_val_t d, el_val_t s) { return d * s; } +el_val_t el_duration_div(el_val_t d, el_val_t s) { return s ? d / s : 0; } +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return a <= b; } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return a > b; } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return a >= b; } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return a != b; } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return a <= b; } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return a > b; } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return a >= b; } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return a != b; } +el_val_t instant_to_unix_seconds(el_val_t i) { return i / 1000000000LL; } +el_val_t instant_to_unix_millis(el_val_t i) { return i / 1000000LL; } +el_val_t instant_to_iso8601(el_val_t i) { return el_wrap_str(el_strdup("")); } +el_val_t duration_to_seconds(el_val_t d) { return d / 1000000000LL; } +el_val_t duration_to_millis(el_val_t d) { return d / 1000000LL; } +el_val_t duration_to_nanos(el_val_t d) { return d; } + +/* ── Calendar stubs ──────────────────────────────────────────────────────── */ + +el_val_t zone(el_val_t id) { return id; } +el_val_t zone_utc(void) { return EL_STR("UTC"); } +el_val_t zone_local(void) { return EL_STR("local"); } +el_val_t zone_offset(el_val_t h, el_val_t m) { return 0; } +el_val_t earth_calendar(el_val_t z) { return 0; } +el_val_t earth_calendar_default(void) { return 0; } +el_val_t mars_calendar(void) { return 0; } +el_val_t cycle_calendar(el_val_t p) { return 0; } +el_val_t no_cycle_calendar(void) { return 0; } +el_val_t relative_calendar(el_val_t e) { return 0; } +el_val_t now_in(el_val_t cal) { return 0; } +el_val_t in_calendar(el_val_t inst, el_val_t cal) { return 0; } +el_val_t cal_format(el_val_t ct, el_val_t pattern) { return el_wrap_str(el_strdup("")); } +el_val_t cal_to_instant(el_val_t ct) { return 0; } +el_val_t cal_cycle_phase(el_val_t ct) { return 0; } +el_val_t cal_in(el_val_t ct, el_val_t cal) { return 0; } +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { return 0; } +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { return 0; } +el_val_t local_datetime(el_val_t date, el_val_t time) { return 0; } +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal) { return 0; } +el_val_t local_date_year(el_val_t ld) { return 0; } +el_val_t local_date_month(el_val_t ld) { return 0; } +el_val_t local_date_day(el_val_t ld) { return 0; } +el_val_t local_time_hour(el_val_t lt) { return 0; } +el_val_t local_time_minute(el_val_t lt) { return 0; } +el_val_t local_time_second(el_val_t lt) { return 0; } +el_val_t local_time_nanos(el_val_t lt) { return 0; } +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur) { return ld; } +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur) { return lt; } +el_val_t el_local_date_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_local_date_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t rhythm_cycle_start(void) { return 0; } +el_val_t rhythm_cycle_phase(el_val_t p) { return 0; } +el_val_t rhythm_duration(el_val_t d) { return 0; } +el_val_t rhythm_session_start(void) { return 0; } +el_val_t rhythm_event(el_val_t name) { return 0; } +el_val_t rhythm_and(el_val_t a, el_val_t b) { return 0; } +el_val_t rhythm_or(el_val_t a, el_val_t b) { return 0; } +el_val_t rhythm_weekday(el_val_t day) { return 0; } +el_val_t rhythm_weekly_at(el_val_t day, el_val_t h, el_val_t m) { return 0; } +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal) { return 0; } +el_val_t rhythm_matches(el_val_t r, el_val_t ct) { return 0; } + +/* ── UUID stubs ──────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void) { return el_wrap_str(el_strdup("00000000-0000-0000-0000-000000000000")); } +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── State K/V stubs ─────────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value) { return value; } +el_val_t state_get(el_val_t key) { return 0; } +el_val_t state_del(el_val_t key) { return 0; } +el_val_t state_keys(void) { return el_wrap_str(el_strdup("[]")); } +el_val_t state_has(el_val_t key) { return 0; } +el_val_t state_get_or(el_val_t key, el_val_t def) { return def; } + +/* ── TTL cache stubs ─────────────────────────────────────────────────────── */ + +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { return value; } +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { return 0; } +el_val_t ttl_cache_age(el_val_t key) { return -1; } diff --git a/lang/el-compiler/runtime/el_sdl2.c b/lang/el-compiler/runtime/el_sdl2.c new file mode 100644 index 0000000..6f9a70d --- /dev/null +++ b/lang/el-compiler/runtime/el_sdl2.c @@ -0,0 +1,1360 @@ +/* + * el_sdl2.c — SDL2 backend for the el native widget system. + * + * This file implements the SDL2 widget layer that el_seed.c calls through to + * when EL_TARGET_SDL2 is defined. It is the embedded/Pi counterpart to + * el_appkit.m (macOS) and el_gtk4.c (Linux desktop). + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_sdl2_* C functions declared here + * → SDL2 pixel drawing (SDL_RenderFillRect, TTF_RenderUTF8_Blended) + * + * SDL2 has no widget toolkit — it only draws pixels. This bridge implements + * widgets as drawn primitives: rectangles, text, borders. Each "widget" is a + * logical node in a retained layout tree. On any state change, the whole scene + * re-renders via el_sdl2_layout + el_sdl2_render_widget. + * + * Widget handles: every widget is an int64_t slot index into g_widgets[]. + * Slot 0 is reserved; valid handles are 1..EL_SDL2_MAX_WIDGETS-1. -1 = invalid. + * + * Threading: SDL2 requires that all rendering happen on the thread that called + * SDL_Init. __native_run_loop runs on the main thread and never returns. + * Callback functions fired by the event loop are called synchronously from the + * same thread — no locking required. + * + * Font loading: DejaVu or Liberation Sans searched in standard Linux paths. + * Falls back to a NULL font (text omitted) if none found. + * + * Target: Raspberry Pi Zero 2W, 512 MB RAM, no desktop environment, raw + * framebuffer via SDL2's KMS/DRM or fbdev driver. + * + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Widget type constants ────────────────────────────────────────────────── */ + +#define EL_WIDGET_NONE 0 +#define EL_WIDGET_WINDOW 1 +#define EL_WIDGET_VSTACK 2 +#define EL_WIDGET_HSTACK 3 +#define EL_WIDGET_ZSTACK 4 +#define EL_WIDGET_SCROLL 5 +#define EL_WIDGET_LABEL 6 +#define EL_WIDGET_BUTTON 7 +#define EL_WIDGET_TEXT_FIELD 8 +#define EL_WIDGET_TEXT_AREA 9 +#define EL_WIDGET_IMAGE 10 +#define EL_WIDGET_DIVIDER 11 +#define EL_WIDGET_SPACER 12 + +/* Layout direction stored in layout_type */ +#define EL_LAYOUT_NONE 0 +#define EL_LAYOUT_VSTACK 1 +#define EL_LAYOUT_HSTACK 2 +#define EL_LAYOUT_ZSTACK 3 + +#define EL_SDL2_MAX_WIDGETS 4096 + +/* ── Widget struct ────────────────────────────────────────────────────────── */ + +typedef struct { + int type; /* EL_WIDGET_* */ + SDL_Rect bounds; /* computed layout position */ + char text[512]; /* label text, button label, placeholder, img path */ + char cb_click[256]; /* El callback fn name — button click / text submit */ + char cb_change[256]; /* El callback fn name — text change */ + char cb_submit[256]; /* El callback fn name — text submit (stored here, mirrored to cb_click) */ + /* foreground colour */ + uint8_t fg_r, fg_g, fg_b, fg_a; + /* background colour */ + uint8_t bg_r, bg_g, bg_b, bg_a; + int font_size; + char font_family[128]; + int font_bold; + int padding_t, padding_r, padding_b, padding_l; + int fixed_width, fixed_height; /* 0 = auto */ + int flex; + int corner_radius; + int disabled; + int hidden; + /* tree */ + int children[64]; + int child_count; + int parent; /* -1 = root */ + /* layout when this node IS a container */ + int layout_type; + int layout_spacing; + /* text field / text area state */ + char input_buf[1024]; + int input_focused; + int cursor_pos; + /* hover state for button highlight */ + int hovered; + /* cached texture for image (freed on destroy) */ + SDL_Texture *img_texture; +} ElWidget; + +/* ── Globals ──────────────────────────────────────────────────────────────── */ + +static ElWidget g_widgets[EL_SDL2_MAX_WIDGETS]; +static int g_slot_used[EL_SDL2_MAX_WIDGETS]; +static SDL_Window *g_sdl_window = NULL; +static SDL_Renderer *g_renderer = NULL; +static TTF_Font *g_font_default = NULL; +static int g_root_widget = -1; +static int g_focused_widget = -1; +static int g_dirty = 1; /* 1 = needs redraw */ +static int g_win_w = 800; +static int g_win_h = 600; + +/* ── Slot management ──────────────────────────────────────────────────────── */ + +static int64_t el_sdl2_alloc_slot(int type) { + for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) { + if (!g_slot_used[i]) { + g_slot_used[i] = 1; + ElWidget *w = &g_widgets[i]; + memset(w, 0, sizeof(*w)); + w->type = type; + w->parent = -1; + w->layout_type = EL_LAYOUT_NONE; + /* default colours: white fg, transparent bg */ + w->fg_r = 255; w->fg_g = 255; w->fg_b = 255; w->fg_a = 255; + w->bg_r = 0; w->bg_g = 0; w->bg_b = 0; w->bg_a = 0; + w->font_size = 14; + return (int64_t)i; + } + } + return -1; /* full */ +} + +static ElWidget *el_sdl2_get(int64_t handle) { + if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return NULL; + if (!g_slot_used[handle]) return NULL; + return &g_widgets[handle]; +} + +static void el_sdl2_free_slot(int64_t handle) { + if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return; + ElWidget *w = &g_widgets[handle]; + if (w->img_texture) { + SDL_DestroyTexture(w->img_texture); + w->img_texture = NULL; + } + g_slot_used[handle] = 0; +} + +/* ── Font loading ─────────────────────────────────────────────────────────── */ + +static TTF_Font *el_sdl2_load_font(int size) { + static const char *paths[] = { + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf", + "/System/Library/Fonts/Helvetica.ttc", /* macOS dev */ + NULL + }; + for (int i = 0; paths[i]; i++) { + TTF_Font *f = TTF_OpenFont(paths[i], size); + if (f) return f; + } + return NULL; +} + +static TTF_Font *el_sdl2_get_font(int size) { + if (size <= 0) size = 14; + /* Simple approach: reuse the default font if size matches, else open again. + * For Pi Zero with one app, this is sufficient. */ + if (g_font_default && size == 14) return g_font_default; + return el_sdl2_load_font(size); +} + +/* ── Callback dispatch ────────────────────────────────────────────────────── */ + +static void el_sdl2_fire_cb(const char *fn_name, int64_t handle, const char *data) { + if (!fn_name || !fn_name[0]) return; + typedef el_val_t (*el_cb_fn)(el_val_t, el_val_t); + el_cb_fn fn = (el_cb_fn)(uintptr_t)dlsym(RTLD_DEFAULT, fn_name); + if (fn) { + el_val_t d = data ? (el_val_t)(uintptr_t)data : (el_val_t)0; + fn((el_val_t)handle, d); + g_dirty = 1; + } +} + +/* ── Rounded rectangle helpers ────────────────────────────────────────────── */ + +/* Draw a filled rounded rectangle approximation using SDL_RenderFillRect. + * The approach: draw the centre cross and fill the 4 corner arcs by drawing + * small squares step-by-step. Sufficient for Pi Zero where SDL_gfx may not + * be available. */ +static void el_sdl2_fill_rounded_rect(SDL_Renderer *r, const SDL_Rect *rect, + int radius, + uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) { + if (radius <= 0 || rect->w <= 0 || rect->h <= 0) { + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + SDL_RenderFillRect(r, rect); + return; + } + if (radius > rect->w / 2) radius = rect->w / 2; + if (radius > rect->h / 2) radius = rect->h / 2; + + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + + /* Horizontal band (full width, inner height minus corners) */ + SDL_Rect horiz = {rect->x, rect->y + radius, rect->w, rect->h - 2 * radius}; + SDL_RenderFillRect(r, &horiz); + + /* Top band */ + SDL_Rect top = {rect->x + radius, rect->y, rect->w - 2 * radius, radius}; + SDL_RenderFillRect(r, &top); + + /* Bottom band */ + SDL_Rect bot = {rect->x + radius, rect->y + rect->h - radius, rect->w - 2 * radius, radius}; + SDL_RenderFillRect(r, &bot); + + /* Draw quarter circles at each corner using row-by-row filling. + * For each row dy in [0, radius), compute the chord width from circle eq. */ + for (int dy = 0; dy < radius; dy++) { + /* x-extent of circle arc at this dy from centre */ + double dx = radius - 1 - (int)(0.5 + (double)radius * + (1.0 - ((double)(radius - 1 - dy) * (double)(radius - 1 - dy)) / + ((double)radius * (double)radius))); + int fill_w = radius - (int)dx; + if (fill_w < 1) fill_w = 1; + + /* top-left corner */ + SDL_Rect tl = {rect->x + radius - fill_w, rect->y + dy, fill_w, 1}; + SDL_RenderFillRect(r, &tl); + /* top-right corner */ + SDL_Rect tr = {rect->x + rect->w - radius, rect->y + dy, fill_w, 1}; + SDL_RenderFillRect(r, &tr); + /* bottom-left corner */ + SDL_Rect bl = {rect->x + radius - fill_w, rect->y + rect->h - 1 - dy, fill_w, 1}; + SDL_RenderFillRect(r, &bl); + /* bottom-right corner */ + SDL_Rect br = {rect->x + rect->w - radius, rect->y + rect->h - 1 - dy, fill_w, 1}; + SDL_RenderFillRect(r, &br); + } +} + +/* Draw a 1px border around a (possibly rounded) rect */ +static void el_sdl2_draw_border(SDL_Renderer *r, const SDL_Rect *rect, + uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) { + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + SDL_RenderDrawRect(r, rect); +} + +/* ── Layout engine ────────────────────────────────────────────────────────── */ + +/* Measure the natural size of a leaf widget. + * Returns 0 if size could not be determined. */ +static void el_sdl2_measure(int slot, int *out_w, int *out_h) { + ElWidget *w = el_sdl2_get(slot); + if (!w) { *out_w = 0; *out_h = 0; return; } + + int fs = w->font_size > 0 ? w->font_size : 14; + TTF_Font *font = el_sdl2_get_font(fs); + + switch (w->type) { + case EL_WIDGET_LABEL: { + if (font && w->text[0]) { + int tw = 0, th = 0; + TTF_SizeUTF8(font, w->text, &tw, &th); + *out_w = tw + w->padding_l + w->padding_r; + *out_h = th + w->padding_t + w->padding_b; + } else { + *out_w = 8 + w->padding_l + w->padding_r; + *out_h = fs + 4 + w->padding_t + w->padding_b; + } + break; + } + case EL_WIDGET_BUTTON: { + if (font && w->text[0]) { + int tw = 0, th = 0; + TTF_SizeUTF8(font, w->text, &tw, &th); + *out_w = tw + 32 + w->padding_l + w->padding_r; + *out_h = th + 20 + w->padding_t + w->padding_b; + } else { + *out_w = 80 + w->padding_l + w->padding_r; + *out_h = fs + 20 + w->padding_t + w->padding_b; + } + break; + } + case EL_WIDGET_TEXT_FIELD: + case EL_WIDGET_TEXT_AREA: { + *out_w = 200; /* default — will stretch with flex */ + *out_h = fs + 12 + w->padding_t + w->padding_b; + if (w->type == EL_WIDGET_TEXT_AREA) *out_h = (fs + 4) * 4 + w->padding_t + w->padding_b; + break; + } + case EL_WIDGET_IMAGE: { + *out_w = 64; + *out_h = 64; + break; + } + case EL_WIDGET_SPACER: { + *out_w = 0; + *out_h = 0; + break; + } + case EL_WIDGET_DIVIDER: { + *out_w = 2; + *out_h = 2; + break; + } + default: { + *out_w = 0; + *out_h = 0; + break; + } + } + + /* Fixed dimensions override natural size */ + if (w->fixed_width > 0) *out_w = w->fixed_width; + if (w->fixed_height > 0) *out_h = w->fixed_height; +} + +/* Forward declaration */ +static void el_sdl2_layout(int slot, SDL_Rect available); + +static void el_sdl2_layout_vstack(ElWidget *w, SDL_Rect inner) { + int spacing = w->layout_spacing; + int n = w->child_count; + if (n == 0) return; + + /* First pass: measure non-flex children and sum flex weights */ + int total_fixed_h = 0; + int total_flex = 0; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw || cw->hidden) continue; + if (cw->flex > 0) { + total_flex += cw->flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + total_fixed_h += mh; + } + } + /* Count spacing gaps */ + int gaps = (n > 1) ? (n - 1) * spacing : 0; + int flex_pool = inner.h - total_fixed_h - gaps; + if (flex_pool < 0) flex_pool = 0; + + /* Second pass: assign rects */ + int y = inner.y; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw) continue; + if (cw->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + cw->bounds = z; + continue; + } + int ch; + if (cw->flex > 0 && total_flex > 0) { + ch = (flex_pool * cw->flex) / total_flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + ch = mh; + } + SDL_Rect child_rect = {inner.x, y, inner.w, ch}; + el_sdl2_layout(ci, child_rect); + y += ch + spacing; + } +} + +static void el_sdl2_layout_hstack(ElWidget *w, SDL_Rect inner) { + int spacing = w->layout_spacing; + int n = w->child_count; + if (n == 0) return; + + int total_fixed_w = 0; + int total_flex = 0; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw || cw->hidden) continue; + if (cw->flex > 0) { + total_flex += cw->flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + total_fixed_w += mw; + } + } + int gaps = (n > 1) ? (n - 1) * spacing : 0; + int flex_pool = inner.w - total_fixed_w - gaps; + if (flex_pool < 0) flex_pool = 0; + + int x = inner.x; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw) continue; + if (cw->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + cw->bounds = z; + continue; + } + int cw_size; + if (cw->flex > 0 && total_flex > 0) { + cw_size = (flex_pool * cw->flex) / total_flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + cw_size = mw; + } + SDL_Rect child_rect = {x, inner.y, cw_size, inner.h}; + el_sdl2_layout(ci, child_rect); + x += cw_size + spacing; + } +} + +static void el_sdl2_layout(int slot, SDL_Rect available) { + ElWidget *w = el_sdl2_get(slot); + if (!w) return; + + if (w->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + w->bounds = z; + return; + } + + /* Apply fixed dimensions if set */ + if (w->fixed_width > 0) available.w = w->fixed_width; + if (w->fixed_height > 0) available.h = w->fixed_height; + + w->bounds = available; + + /* For containers: compute inner rect after padding, then layout children */ + SDL_Rect inner = { + available.x + w->padding_l, + available.y + w->padding_t, + available.w - w->padding_l - w->padding_r, + available.h - w->padding_t - w->padding_b + }; + if (inner.w < 0) inner.w = 0; + if (inner.h < 0) inner.h = 0; + + switch (w->layout_type) { + case EL_LAYOUT_VSTACK: + el_sdl2_layout_vstack(w, inner); + break; + case EL_LAYOUT_HSTACK: + el_sdl2_layout_hstack(w, inner); + break; + case EL_LAYOUT_ZSTACK: + /* Each child gets the full inner rect */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_layout(w->children[i], inner); + } + break; + default: + /* Leaf: layout_type NONE means no children. + * WINDOW/SCROLL/etc. that has a content child: give it full inner. */ + if (w->type == EL_WIDGET_WINDOW || w->type == EL_WIDGET_SCROLL) { + for (int i = 0; i < w->child_count; i++) { + el_sdl2_layout(w->children[i], inner); + } + } + break; + } +} + +/* ── Render pass ──────────────────────────────────────────────────────────── */ + +static void el_sdl2_render_text_in_rect(TTF_Font *font, const char *text, + SDL_Color col, SDL_Rect dst) { + if (!font || !text || !text[0]) return; + SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col); + if (!surf) return; + SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + if (!tex) return; + + int tw, th; + SDL_QueryTexture(tex, NULL, NULL, &tw, &th); + + /* Clip to dst rect */ + SDL_Rect src_rect = {0, 0, tw, th}; + SDL_Rect dst_rect = dst; + /* Centre vertically; left-align horizontally */ + dst_rect.y = dst.y + (dst.h - th) / 2; + dst_rect.h = th; + dst_rect.w = tw; + if (dst_rect.w > dst.w) { + dst_rect.w = dst.w; + src_rect.w = dst.w; + } + + SDL_RenderSetClipRect(g_renderer, &dst); + SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect); + SDL_RenderSetClipRect(g_renderer, NULL); + SDL_DestroyTexture(tex); +} + +static void el_sdl2_render_text_centered(TTF_Font *font, const char *text, + SDL_Color col, SDL_Rect dst) { + if (!font || !text || !text[0]) return; + SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col); + if (!surf) return; + SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + if (!tex) return; + + int tw, th; + SDL_QueryTexture(tex, NULL, NULL, &tw, &th); + + SDL_Rect src_rect = {0, 0, tw, th}; + if (src_rect.w > dst.w) src_rect.w = dst.w; + + SDL_Rect dst_rect; + dst_rect.x = dst.x + (dst.w - src_rect.w) / 2; + dst_rect.y = dst.y + (dst.h - th) / 2; + dst_rect.w = src_rect.w; + dst_rect.h = th; + + SDL_RenderSetClipRect(g_renderer, &dst); + SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect); + SDL_RenderSetClipRect(g_renderer, NULL); + SDL_DestroyTexture(tex); +} + +/* Forward declaration */ +static void el_sdl2_render_widget(int slot); + +static void el_sdl2_render_widget(int slot) { + ElWidget *w = el_sdl2_get(slot); + if (!w || w->hidden) return; + + SDL_Rect b = w->bounds; + if (b.w <= 0 || b.h <= 0) return; + + int fs = w->font_size > 0 ? w->font_size : 14; + TTF_Font *font = el_sdl2_get_font(fs); + SDL_Color fg = {w->fg_r, w->fg_g, w->fg_b, w->fg_a}; + + /* ── Draw background ── */ + if (w->bg_a > 0) { + el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius, + w->bg_r, w->bg_g, w->bg_b, w->bg_a); + } + + /* ── Widget-specific drawing ── */ + switch (w->type) { + case EL_WIDGET_WINDOW: { + /* Window just provides a background; children render themselves. */ + /* Fill entire window with bg (default dark) */ + SDL_Rect win_rect = {0, 0, g_win_w, g_win_h}; + uint8_t br = w->bg_a > 0 ? w->bg_r : 30; + uint8_t bg_g = w->bg_a > 0 ? w->bg_g : 30; + uint8_t bb = w->bg_a > 0 ? w->bg_b : 30; + SDL_SetRenderDrawColor(g_renderer, br, bg_g, bb, 255); + SDL_RenderFillRect(g_renderer, &win_rect); + break; + } + + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: + case EL_WIDGET_SCROLL: + /* Containers: bg already drawn above. Children rendered below. */ + break; + + case EL_WIDGET_LABEL: { + /* Text content */ + SDL_Rect inner = { + b.x + w->padding_l, + b.y + w->padding_t, + b.w - w->padding_l - w->padding_r, + b.h - w->padding_t - w->padding_b + }; + el_sdl2_render_text_in_rect(font, w->text, fg, inner); + break; + } + + case EL_WIDGET_BUTTON: { + /* Button body */ + uint8_t btn_r, btn_g, btn_b; + if (w->disabled) { + btn_r = 80; btn_g = 80; btn_b = 80; + } else if (w->hovered) { + btn_r = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_r + 40) : 100); + btn_g = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_g + 40) : 100); + btn_b = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_b + 40) : 140); + } else { + btn_r = w->bg_a > 0 ? w->bg_r : 60; + btn_g = w->bg_a > 0 ? w->bg_g : 60; + btn_b = w->bg_a > 0 ? w->bg_b : 100; + } + el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius > 0 ? w->corner_radius : 4, + btn_r, btn_g, btn_b, 255); + + /* Border */ + SDL_SetRenderDrawColor(g_renderer, 140, 140, 200, 255); + SDL_RenderDrawRect(g_renderer, &b); + + /* Centered label */ + SDL_Color label_col = {w->fg_r, w->fg_g, w->fg_b, w->disabled ? 128u : 255u}; + SDL_Rect inner = { + b.x + w->padding_l + 4, + b.y + w->padding_t, + b.w - w->padding_l - w->padding_r - 8, + b.h - w->padding_t - w->padding_b + }; + el_sdl2_render_text_centered(font, w->text, label_col, inner); + break; + } + + case EL_WIDGET_TEXT_FIELD: + case EL_WIDGET_TEXT_AREA: { + /* Background */ + uint8_t field_bg_r = 40, field_bg_g = 40, field_bg_b = 50; + if (w->bg_a > 0) { field_bg_r = w->bg_r; field_bg_g = w->bg_g; field_bg_b = w->bg_b; } + el_sdl2_fill_rounded_rect(g_renderer, &b, 3, + field_bg_r, field_bg_g, field_bg_b, 255); + + /* Border — highlighted when focused */ + if (w->input_focused) { + SDL_SetRenderDrawColor(g_renderer, 80, 140, 220, 255); + } else { + SDL_SetRenderDrawColor(g_renderer, 100, 100, 120, 255); + } + SDL_RenderDrawRect(g_renderer, &b); + + SDL_Rect inner = { + b.x + 6 + w->padding_l, + b.y + w->padding_t, + b.w - 12 - w->padding_l - w->padding_r, + b.h - w->padding_t - w->padding_b + }; + + const char *display = w->input_buf[0] ? w->input_buf : w->text; /* text = placeholder */ + SDL_Color text_col; + if (!w->input_buf[0] && w->text[0]) { + /* placeholder style: dimmed */ + text_col = (SDL_Color){120, 120, 140, 255}; + } else { + text_col = fg; + } + + el_sdl2_render_text_in_rect(font, display, text_col, inner); + + /* Draw cursor if focused */ + if (w->input_focused && font) { + /* Measure text up to cursor position */ + char before_cursor[1024]; + int cplen = w->cursor_pos; + if (cplen < 0) cplen = 0; + int buflen = (int)strlen(w->input_buf); + if (cplen > buflen) cplen = buflen; + memcpy(before_cursor, w->input_buf, cplen); + before_cursor[cplen] = '\0'; + int cx = 0, cy_unused = 0; + if (before_cursor[0]) { + TTF_SizeUTF8(font, before_cursor, &cx, &cy_unused); + } + int cursor_x = inner.x + cx; + int cursor_y1 = b.y + 3; + int cursor_y2 = b.y + b.h - 3; + SDL_SetRenderDrawColor(g_renderer, 200, 200, 255, 255); + SDL_RenderDrawLine(g_renderer, cursor_x, cursor_y1, cursor_x, cursor_y2); + } + break; + } + + case EL_WIDGET_IMAGE: { + /* Load or reuse cached texture */ + if (!w->img_texture && w->text[0]) { + SDL_Surface *surf = IMG_Load(w->text); + if (!surf) { + /* Try as raw SDL surface */ + surf = SDL_LoadBMP(w->text); + } + if (surf) { + w->img_texture = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + } + } + if (w->img_texture) { + SDL_RenderCopy(g_renderer, w->img_texture, NULL, &b); + } else { + /* Placeholder: grey box with X */ + SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255); + SDL_RenderFillRect(g_renderer, &b); + SDL_SetRenderDrawColor(g_renderer, 140, 140, 140, 255); + SDL_RenderDrawLine(g_renderer, b.x, b.y, b.x + b.w, b.y + b.h); + SDL_RenderDrawLine(g_renderer, b.x + b.w, b.y, b.x, b.y + b.h); + } + break; + } + + case EL_WIDGET_DIVIDER: { + SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255); + SDL_RenderFillRect(g_renderer, &b); + break; + } + + case EL_WIDGET_SPACER: + default: + break; + } + + /* ── Recurse into children ── */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_render_widget(w->children[i]); + } +} + +static void el_sdl2_render(void) { + /* Clear with dark background */ + SDL_SetRenderDrawColor(g_renderer, 30, 30, 30, 255); + SDL_RenderClear(g_renderer); + + if (g_root_widget >= 0) { + el_sdl2_render_widget(g_root_widget); + } +} + +/* ── Event handling ───────────────────────────────────────────────────────── */ + +/* Hit-test: find the deepest interactive widget at (x, y). + * Returns slot index or -1. */ +static int el_sdl2_hit_test(int slot, int x, int y) { + ElWidget *w = el_sdl2_get(slot); + if (!w || w->hidden) return -1; + + SDL_Rect b = w->bounds; + if (x < b.x || x >= b.x + b.w) return -1; + if (y < b.y || y >= b.y + b.h) return -1; + + /* Check children first (front-to-back; last child = topmost) */ + for (int i = w->child_count - 1; i >= 0; i--) { + int r = el_sdl2_hit_test(w->children[i], x, y); + if (r >= 0) return r; + } + + /* This widget itself is a candidate if interactive */ + if (w->type == EL_WIDGET_BUTTON || + w->type == EL_WIDGET_TEXT_FIELD || + w->type == EL_WIDGET_TEXT_AREA) { + return slot; + } + + return -1; +} + +/* Clear hover on all buttons */ +static void el_sdl2_clear_hover(int slot) { + ElWidget *w = el_sdl2_get(slot); + if (!w) return; + if (w->type == EL_WIDGET_BUTTON) w->hovered = 0; + for (int i = 0; i < w->child_count; i++) { + el_sdl2_clear_hover(w->children[i]); + } +} + +static void el_sdl2_handle_event(SDL_Event *e) { + switch (e->type) { + case SDL_QUIT: + exit(0); + break; + + case SDL_WINDOWEVENT: + if (e->window.event == SDL_WINDOWEVENT_RESIZED || + e->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + g_win_w = e->window.data1; + g_win_h = e->window.data2; + /* Re-layout root */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; + } + break; + + case SDL_MOUSEBUTTONDOWN: { + if (e->button.button != SDL_BUTTON_LEFT) break; + int mx = e->button.x; + int my = e->button.y; + + /* Unfocus previous text field */ + if (g_focused_widget >= 0) { + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (fw) fw->input_focused = 0; + } + g_focused_widget = -1; + + int hit = (g_root_widget >= 0) ? el_sdl2_hit_test(g_root_widget, mx, my) : -1; + if (hit < 0) break; + + ElWidget *hw = el_sdl2_get(hit); + if (!hw) break; + + if (hw->type == EL_WIDGET_BUTTON && !hw->disabled) { + el_sdl2_fire_cb(hw->cb_click, (int64_t)hit, ""); + } else if (hw->type == EL_WIDGET_TEXT_FIELD || + hw->type == EL_WIDGET_TEXT_AREA) { + hw->input_focused = 1; + g_focused_widget = hit; + SDL_StartTextInput(); + } + g_dirty = 1; + break; + } + + case SDL_MOUSEMOTION: { + if (g_root_widget < 0) break; + int mx = e->motion.x; + int my = e->motion.y; + int prev_hover = -1; + /* Find any currently hovered button */ + for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) { + if (g_slot_used[i] && g_widgets[i].type == EL_WIDGET_BUTTON && + g_widgets[i].hovered) { + prev_hover = i; + break; + } + } + /* Clear all hover */ + el_sdl2_clear_hover(g_root_widget); + /* Set hover on current button under cursor */ + int hit = el_sdl2_hit_test(g_root_widget, mx, my); + if (hit >= 0) { + ElWidget *hw = el_sdl2_get(hit); + if (hw && hw->type == EL_WIDGET_BUTTON && !hw->disabled) { + hw->hovered = 1; + } + } + if (hit != prev_hover) g_dirty = 1; + break; + } + + case SDL_KEYDOWN: { + if (g_focused_widget < 0) break; + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (!fw) break; + + SDL_Keycode key = e->key.keysym.sym; + + if (key == SDLK_BACKSPACE) { + int len = (int)strlen(fw->input_buf); + if (len > 0 && fw->cursor_pos > 0) { + /* Remove character before cursor */ + int cp = fw->cursor_pos; + memmove(fw->input_buf + cp - 1, fw->input_buf + cp, + (size_t)(len - cp + 1)); + fw->cursor_pos--; + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + } + g_dirty = 1; + } else if (key == SDLK_DELETE) { + int len = (int)strlen(fw->input_buf); + if (fw->cursor_pos < len) { + memmove(fw->input_buf + fw->cursor_pos, + fw->input_buf + fw->cursor_pos + 1, + (size_t)(len - fw->cursor_pos)); + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } + } else if (key == SDLK_LEFT) { + if (fw->cursor_pos > 0) { fw->cursor_pos--; g_dirty = 1; } + } else if (key == SDLK_RIGHT) { + int len = (int)strlen(fw->input_buf); + if (fw->cursor_pos < len) { fw->cursor_pos++; g_dirty = 1; } + } else if (key == SDLK_HOME) { + fw->cursor_pos = 0; g_dirty = 1; + } else if (key == SDLK_END) { + fw->cursor_pos = (int)strlen(fw->input_buf); g_dirty = 1; + } else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) { + if (fw->type == EL_WIDGET_TEXT_FIELD) { + /* Submit */ + el_sdl2_fire_cb(fw->cb_submit[0] ? fw->cb_submit : fw->cb_click, + (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } else { + /* Text area: insert newline via SDL_TEXTINPUT */ + } + } else if (key == SDLK_TAB) { + /* Blur current field */ + fw->input_focused = 0; + SDL_StopTextInput(); + g_focused_widget = -1; + g_dirty = 1; + } + break; + } + + case SDL_TEXTINPUT: { + if (g_focused_widget < 0) break; + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (!fw) break; + + const char *inp = e->text.text; + int inp_len = (int)strlen(inp); + int buf_len = (int)strlen(fw->input_buf); + int max_buf = (int)sizeof(fw->input_buf) - 1; + + if (buf_len + inp_len <= max_buf) { + /* Insert at cursor position */ + int cp = fw->cursor_pos; + memmove(fw->input_buf + cp + inp_len, fw->input_buf + cp, + (size_t)(buf_len - cp + 1)); + memcpy(fw->input_buf + cp, inp, (size_t)inp_len); + fw->cursor_pos += inp_len; + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } + break; + } + + default: + break; + } +} + +/* ── Public API — called from el_seed.c wrappers ──────────────────────────── */ + +void el_sdl2_init(void) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { + fprintf(stderr, "el_sdl2: SDL_Init failed: %s\n", SDL_GetError()); + exit(1); + } + if (TTF_Init() != 0) { + fprintf(stderr, "el_sdl2: TTF_Init failed: %s\n", TTF_GetError()); + exit(1); + } + if (!(IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG) & (IMG_INIT_PNG | IMG_INIT_JPG))) { + fprintf(stderr, "el_sdl2: IMG_Init warning: %s\n", IMG_GetError()); + /* Non-fatal: images won't load, but text UI still works */ + } + + /* Load default font */ + g_font_default = el_sdl2_load_font(14); + if (!g_font_default) { + fprintf(stderr, "el_sdl2: warning: no system font found; text will not render\n"); + } +} + +void el_sdl2_run_loop(void) { + SDL_Event event; + while (1) { + while (SDL_PollEvent(&event)) { + el_sdl2_handle_event(&event); + } + if (g_dirty) { + el_sdl2_render(); + SDL_RenderPresent(g_renderer); + g_dirty = 0; + } + SDL_Delay(16); /* ~60 fps cap */ + } +} + +int64_t el_sdl2_window_create(const char *title, int width, int height, + int min_width, int min_height) { + g_win_w = width > 0 ? width : 800; + g_win_h = height > 0 ? height : 600; + + Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; + + /* On Pi with no desktop: use fullscreen desktop if w/h not specified */ + g_sdl_window = SDL_CreateWindow( + title ? title : "", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + g_win_w, g_win_h, flags); + + if (!g_sdl_window) { + fprintf(stderr, "el_sdl2: SDL_CreateWindow failed: %s\n", SDL_GetError()); + return -1; + } + + if (min_width > 0 || min_height > 0) { + SDL_SetWindowMinimumSize(g_sdl_window, min_width, min_height); + } + + g_renderer = SDL_CreateRenderer(g_sdl_window, -1, + SDL_RENDERER_ACCELERATED | + SDL_RENDERER_PRESENTVSYNC); + if (!g_renderer) { + /* Fall back to software renderer (required on Pi Zero fbdev) */ + g_renderer = SDL_CreateRenderer(g_sdl_window, -1, SDL_RENDERER_SOFTWARE); + } + if (!g_renderer) { + fprintf(stderr, "el_sdl2: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return -1; + } + + SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND); + + int64_t handle = el_sdl2_alloc_slot(EL_WIDGET_WINDOW); + if (handle < 0) return -1; + + ElWidget *w = el_sdl2_get(handle); + snprintf(w->text, sizeof(w->text), "%s", title ? title : ""); + w->layout_type = EL_LAYOUT_VSTACK; + w->layout_spacing = 0; + w->bounds.x = 0; w->bounds.y = 0; + w->bounds.w = g_win_w; w->bounds.h = g_win_h; + + g_root_widget = (int)handle; + g_dirty = 1; + return handle; +} + +void el_sdl2_window_show(int64_t handle) { + if (g_sdl_window) { + SDL_ShowWindow(g_sdl_window); + } + /* Trigger initial layout */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_window_set_title(int64_t handle, const char *title) { + if (g_sdl_window) { + SDL_SetWindowTitle(g_sdl_window, title ? title : ""); + } + ElWidget *w = el_sdl2_get(handle); + if (w) snprintf(w->text, sizeof(w->text), "%s", title ? title : ""); + g_dirty = 1; +} + +int64_t el_sdl2_vstack_create(int spacing) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_VSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_VSTACK; + w->layout_spacing = spacing; + return h; +} + +int64_t el_sdl2_hstack_create(int spacing) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_HSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_HSTACK; + w->layout_spacing = spacing; + return h; +} + +int64_t el_sdl2_zstack_create(void) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_ZSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_ZSTACK; + return h; +} + +int64_t el_sdl2_scroll_create(void) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_SCROLL); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + /* Scroll: single content child; layout passes full inner rect */ + w->layout_type = EL_LAYOUT_NONE; + return h; +} + +int64_t el_sdl2_label_create(const char *text) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_LABEL); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", text ? text : ""); + return h; +} + +int64_t el_sdl2_button_create(const char *label) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_BUTTON); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", label ? label : ""); + w->padding_t = 4; w->padding_b = 4; + w->padding_l = 8; w->padding_r = 8; + return h; +} + +int64_t el_sdl2_text_field_create(const char *placeholder) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_FIELD); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + /* text field: text[] holds the placeholder */ + snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : ""); + w->padding_t = 2; w->padding_b = 2; + w->padding_l = 4; w->padding_r = 4; + return h; +} + +int64_t el_sdl2_text_area_create(const char *placeholder) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_AREA); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : ""); + w->padding_t = 4; w->padding_b = 4; + w->padding_l = 6; w->padding_r = 6; + return h; +} + +int64_t el_sdl2_image_create(const char *path_or_name) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_IMAGE); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", path_or_name ? path_or_name : ""); + return h; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_sdl2_widget_set_text(int64_t handle, const char *text) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) { + /* Setting text on a field = set input buffer content */ + snprintf(w->input_buf, sizeof(w->input_buf), "%s", text ? text : ""); + w->cursor_pos = (int)strlen(w->input_buf); + } else { + snprintf(w->text, sizeof(w->text), "%s", text ? text : ""); + /* Invalidate image texture if path changed */ + if (w->type == EL_WIDGET_IMAGE && w->img_texture) { + SDL_DestroyTexture(w->img_texture); + w->img_texture = NULL; + } + } + g_dirty = 1; +} + +const char *el_sdl2_widget_get_text(int64_t handle) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return ""; + if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) { + return w->input_buf; + } + return w->text; +} + +void el_sdl2_widget_set_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fg_r = (uint8_t)(r * 255.0f); + w->fg_g = (uint8_t)(g * 255.0f); + w->fg_b = (uint8_t)(b * 255.0f); + w->fg_a = (uint8_t)(a * 255.0f); + g_dirty = 1; +} + +void el_sdl2_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->bg_r = (uint8_t)(r * 255.0f); + w->bg_g = (uint8_t)(g * 255.0f); + w->bg_b = (uint8_t)(b * 255.0f); + w->bg_a = (uint8_t)(a * 255.0f); + g_dirty = 1; +} + +void el_sdl2_widget_set_font(int64_t handle, const char *family, int size, int bold) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + if (family) snprintf(w->font_family, sizeof(w->font_family), "%s", family); + if (size > 0) w->font_size = size; + w->font_bold = bold; + g_dirty = 1; +} + +void el_sdl2_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->padding_t = top; + w->padding_r = right; + w->padding_b = bottom; + w->padding_l = left; + /* Re-layout from root */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_width(int64_t handle, int width) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fixed_width = width; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_height(int64_t handle, int height) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fixed_height = height; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_flex(int64_t handle, int flex) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->flex = flex; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_corner_radius(int64_t handle, int radius) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->corner_radius = radius; + g_dirty = 1; +} + +void el_sdl2_widget_set_disabled(int64_t handle, int disabled) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->disabled = disabled; + g_dirty = 1; +} + +void el_sdl2_widget_set_hidden(int64_t handle, int hidden) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->hidden = hidden; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +void el_sdl2_widget_add_child(int64_t parent, int64_t child) { + ElWidget *pw = el_sdl2_get(parent); + ElWidget *cw = el_sdl2_get(child); + if (!pw || !cw) return; + if (pw->child_count >= 64) return; /* max children per node */ + + /* Prevent duplicates */ + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == (int)child) return; + } + + pw->children[pw->child_count++] = (int)child; + cw->parent = (int)parent; + + /* Re-layout */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_remove_child(int64_t parent, int64_t child) { + ElWidget *pw = el_sdl2_get(parent); + ElWidget *cw = el_sdl2_get(child); + if (!pw) return; + + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == (int)child) { + memmove(&pw->children[i], &pw->children[i + 1], + (size_t)(pw->child_count - i - 1) * sizeof(int)); + pw->child_count--; + if (cw) cw->parent = -1; + break; + } + } + + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_sdl2_widget_on_click(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : ""); +} + +void el_sdl2_widget_on_change(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_change, sizeof(w->cb_change), "%s", fn_name ? fn_name : ""); +} + +void el_sdl2_widget_on_submit(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_submit, sizeof(w->cb_submit), "%s", fn_name ? fn_name : ""); + /* Mirror into cb_click so RETURN key fires via cb_click fallback too */ + snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : ""); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_sdl2_widget_destroy(int64_t handle) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + + /* Remove from parent */ + if (w->parent >= 0) { + el_sdl2_widget_remove_child((int64_t)w->parent, handle); + } + + /* Recursively destroy children */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_widget_destroy((int64_t)w->children[i]); + } + + if (g_focused_widget == (int)handle) { + g_focused_widget = -1; + SDL_StopTextInput(); + } + if (g_root_widget == (int)handle) { + g_root_widget = -1; + } + + el_sdl2_free_slot(handle); + g_dirty = 1; +} + +#endif /* EL_TARGET_SDL2 */ diff --git a/lang/el-compiler/runtime/el_uikit.m b/lang/el-compiler/runtime/el_uikit.m new file mode 100644 index 0000000..01a97a1 --- /dev/null +++ b/lang/el-compiler/runtime/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/lang/el-compiler/runtime/el_win32.c b/lang/el-compiler/runtime/el_win32.c new file mode 100644 index 0000000..b99ea71 --- /dev/null +++ b/lang/el-compiler/runtime/el_win32.c @@ -0,0 +1,1363 @@ +/* + * el_win32.c — Win32 backend for the el native widget system. + * + * This file implements the Windows Win32 widget layer that el_seed.c calls + * through to when EL_TARGET_WIN32 is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_win32_* C functions declared here + * → HWND/Win32 API / GDI + * + * 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; handle -1 = invalid). + * + * Threading: Win32 UI must run on the main thread. el is single-threaded for + * native UI, so all calls are direct. The message loop runs on the main thread + * and never returns. + * + * Callback mechanism: when a widget fires an event (button click, text change), + * the bridge looks up the registered callback function name, then calls + * GetProcAddress(GetModuleHandle(NULL), fn_name)(widget_handle, event_data) + * This resolves the El function symbol at runtime, matching el_seed.c's + * dlsym(RTLD_DEFAULT, fn_name) pattern on POSIX. + * + * Layout: VStack and HStack containers maintain a child HWND list and reflow + * children on WM_SIZE using SetWindowPos. ZStack uses absolute positioning. + * + * Subclassing: SetWindowSubclass (comctl32) is used to intercept WM_COMMAND + * and WM_NOTIFY on container windows so button clicks and edit changes route + * back to el callbacks. + * + * String encoding: el strings are UTF-8 (const char*). All Win32 text APIs + * use wide strings (WCHAR*). Conversion via MultiByteToWideChar / + * WideCharToMultiByte throughout. + * + * Compile: + * cl /DEL_TARGET_WIN32 el_win32.c /c /Fo:el_win32.obj + * (link with: comctl32.lib user32.lib gdi32.lib) + * or with clang-cl: + * clang-cl /DEL_TARGET_WIN32 -c el_win32.c -o el_win32.obj + */ + +#ifdef EL_TARGET_WIN32 + +#define WIN32_LEAN_AND_MEAN +#ifndef UNICODE +#define UNICODE +#endif +#ifndef _UNICODE +#define _UNICODE +#endif + +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Compile-time linkage ─────────────────────────────────────────────────── */ +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "gdi32.lib") + +/* ── Custom window messages ───────────────────────────────────────────────── */ +/* WM_USER+1: posted to a container to trigger a layout reflow pass. */ +#define WM_EL_REFLOW (WM_USER + 1) + +/* ── Widget type enum ─────────────────────────────────────────────────────── */ + +#define EL_WIN32_MAX_WIDGETS 4096 +#define EL_WIN32_MAX_CHILDREN 256 + +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; + +/* Per-slot data. */ +typedef struct { + ElWidgetKind kind; + HWND hwnd; /* primary window handle; NULL if free */ + char* cb_click; /* El function name for click / submit events */ + char* cb_change; /* El function name for value-change events */ + + /* Layout data (used by VSTACK / HSTACK / ZSTACK / WINDOW containers) */ + int spacing; /* pixels between children */ + int pad_top; + int pad_right; + int pad_bottom; + int pad_left; + int flex; /* flex weight; 0 = fixed size */ + + /* Fixed size overrides (0 = not set) */ + int fixed_w; + int fixed_h; + + /* Child list for layout containers */ + int64_t children[EL_WIN32_MAX_CHILDREN]; + int child_count; + + /* Color state (stored for WM_CTLCOLOR* replies) */ + COLORREF fg_color; + COLORREF bg_color; + HBRUSH bg_brush; + int has_fg; + int has_bg; + + /* Font */ + HFONT font; /* NULL = default system font */ + + /* Image: HBITMAP loaded for SS_BITMAP static */ + HBITMAP hbmp; +} ElWidget; + +static ElWidget _el_widgets[EL_WIN32_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* ── Window class names ───────────────────────────────────────────────────── */ +static const WCHAR* EL_WNDCLASS_VSTACK = L"ElVStack"; +static const WCHAR* EL_WNDCLASS_HSTACK = L"ElHStack"; +static const WCHAR* EL_WNDCLASS_ZSTACK = L"ElZStack"; +static const WCHAR* EL_WNDCLASS_SCROLL = L"ElScroll"; +static const WCHAR* EL_WNDCLASS_WINDOW = L"ElWindow"; + +/* ── Forward declarations ─────────────────────────────────────────────────── */ +static void el_win32_reflow(int64_t container_handle); +static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); +static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + +/* ── String helpers ───────────────────────────────────────────────────────── */ + +/* UTF-8 → WCHAR*. Caller must free(). Returns NULL on failure. */ +static WCHAR* el_utf8_to_wide(const char* s) { + if (!s) s = ""; + int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); + if (n <= 0) return _wcsdup(L""); + WCHAR* buf = (WCHAR*)malloc((size_t)n * sizeof(WCHAR)); + if (!buf) return _wcsdup(L""); + MultiByteToWideChar(CP_UTF8, 0, s, -1, buf, n); + return buf; +} + +/* WCHAR* → UTF-8 char*. Caller must free(). Returns NULL on failure. */ +static char* el_wide_to_utf8(const WCHAR* ws) { + if (!ws) return strdup(""); + int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + if (n <= 0) return strdup(""); + char* buf = (char*)malloc((size_t)n); + if (!buf) return strdup(""); + WideCharToMultiByte(CP_UTF8, 0, ws, -1, buf, n, NULL, NULL); + return buf; +} + +/* ── Slot management ──────────────────────────────────────────────────────── */ + +static int64_t el_widget_alloc(ElWidgetKind kind, HWND hwnd) { + for (int i = 1; i < EL_WIN32_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + ElWidget* w = &_el_widgets[i]; + memset(w, 0, sizeof(ElWidget)); + w->kind = kind; + w->hwnd = hwnd; + 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_WIN32_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free_slot(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; + if (w->bg_brush) { DeleteObject(w->bg_brush); w->bg_brush = NULL; } + if (w->font) { DeleteObject(w->font); w->font = NULL; } + if (w->hbmp) { DeleteObject(w->hbmp); w->hbmp = NULL; } + w->hwnd = NULL; + w->kind = EL_WIDGET_FREE; +} + +/* Map HWND → widget slot (O(n) linear scan; table is small). */ +static int64_t el_hwnd_to_slot(HWND hwnd) { + for (int i = 1; i <= _el_widget_count; i++) { + if (_el_widgets[i].kind != EL_WIDGET_FREE && _el_widgets[i].hwnd == hwnd) + return (int64_t)i; + } + return -1; +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. El functions have the signature: + * fn handler(handle: Int, data: String) -> Void + * compiled to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_win32_invoke_cb(const char* fn_name, int64_t handle, const char* data) { + if (!fn_name || !*fn_name) return; + FARPROC proc = GetProcAddress(GetModuleHandleW(NULL), fn_name); + if (!proc) return; + ElCb2 fn = (ElCb2)(void*)proc; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── Layout reflow ────────────────────────────────────────────────────────── */ +/* + * Reflow the direct children of a container widget. + * + * For VSTACK: stack children top-to-bottom, respecting padding and spacing. + * Children with flex > 0 divide the leftover height proportionally. + * Children with fixed_h > 0 use that height; otherwise QueryWidget. + * + * For HSTACK: same logic, left-to-right. + * + * For WINDOW: its content area is the client rect; children stacked vertically + * (WINDOW acts like an outer VSTACK for direct child attachment purposes). + * + * For ZSTACK: children fill the entire client rect (absolute; no reflow). + * + * For SCROLL: the single child fills the client rect width; height is its + * natural height (scroll bar handles overflow). + */ +static void el_win32_reflow(int64_t handle) { + ElWidget* pw = el_widget_get(handle); + if (!pw || !pw->hwnd) return; + + RECT cr; + GetClientRect(pw->hwnd, &cr); + int total_w = cr.right - cr.left; + int total_h = cr.bottom - cr.top; + + int pt = pw->pad_top; + int pr = pw->pad_right; + int pb = pw->pad_bottom; + int pl = pw->pad_left; + int sp = pw->spacing; + + int n = pw->child_count; + if (n == 0) return; + + if (pw->kind == EL_WIDGET_ZSTACK) { + /* All children fill the client rect. */ + int cw = total_w - pl - pr; + int ch = total_h - pt - pb; + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + } + return; + } + + if (pw->kind == EL_WIDGET_SCROLL) { + /* Single child fills width; natural height. */ + if (n < 1) return; + ElWidget* cw_ = el_widget_get(pw->children[0]); + if (!cw_ || !cw_->hwnd) return; + int cw = total_w - pl - pr; + if (cw < 0) cw = 0; + /* Measure natural height. */ + RECT cr2; + GetWindowRect(cw_->hwnd, &cr2); + int ch = cr2.bottom - cr2.top; + if (cw_->fixed_h > 0) ch = cw_->fixed_h; + SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + /* Update scroll range. */ + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE; + si.nMin = 0; + si.nMax = ch + pt + pb; + si.nPage = (UINT)total_h; + SetScrollInfo(pw->hwnd, SB_VERT, &si, TRUE); + return; + } + + /* VSTACK, HSTACK, WINDOW */ + int is_vert = (pw->kind == EL_WIDGET_VSTACK || + pw->kind == EL_WIDGET_WINDOW); + + /* First pass: determine fixed sizes and total flex weight. */ + int total_flex = 0; + int fixed_used = 0; /* pixels consumed by fixed children + spacing */ + + /* Per-child resolved size in the main axis. */ + int* sizes = (int*)malloc((size_t)n * sizeof(int)); + if (!sizes) return; + + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + sizes[i] = 0; + if (!cw_ || !cw_->hwnd) continue; + int fixed = is_vert ? cw_->fixed_h : cw_->fixed_w; + if (fixed > 0) { + sizes[i] = fixed; + fixed_used += fixed; + } else if (cw_->flex > 0) { + total_flex += cw_->flex; + } else { + /* No flex, no fixed: measure current window size as default. */ + RECT wr; + GetWindowRect(cw_->hwnd, &wr); + int nat = is_vert ? (wr.bottom - wr.top) : (wr.right - wr.left); + if (nat < 1) nat = is_vert ? 24 : 80; /* reasonable fallback */ + sizes[i] = nat; + fixed_used += nat; + } + if (i < n - 1) fixed_used += sp; /* spacing between children */ + } + + /* Available space for flex children. */ + int avail = (is_vert ? (total_h - pt - pb) : (total_w - pl - pr)) - fixed_used; + if (avail < 0) avail = 0; + + /* Second pass: assign flex sizes. */ + if (total_flex > 0) { + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + if (cw_->flex > 0 && sizes[i] == 0) { + sizes[i] = (avail * cw_->flex) / total_flex; + } + } + } + + /* Third pass: position children. */ + int cursor = is_vert ? pt : pl; + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + + int x, y, cw, ch; + if (is_vert) { + x = pl; + y = cursor; + cw = (cw_->fixed_w > 0) ? cw_->fixed_w : (total_w - pl - pr); + ch = sizes[i]; + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + cursor += ch + sp; + } else { + x = cursor; + y = pt; + cw = sizes[i]; + ch = (cw_->fixed_h > 0) ? cw_->fixed_h : (total_h - pt - pb); + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + cursor += cw + sp; + } + + SetWindowPos(cw_->hwnd, NULL, x, y, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + + /* Recursively reflow child containers. */ + if (cw_->kind == EL_WIDGET_VSTACK || + cw_->kind == EL_WIDGET_HSTACK || + cw_->kind == EL_WIDGET_ZSTACK || + cw_->kind == EL_WIDGET_SCROLL || + cw_->kind == EL_WIDGET_WINDOW) { + el_win32_reflow(pw->children[i]); + } + } + + free(sizes); +} + +/* ── Container WndProc ────────────────────────────────────────────────────── */ +/* + * Used for VSTACK, HSTACK, ZSTACK, SCROLL containers. + * Handles WM_SIZE (reflow), WM_CTLCOLOR* (color theming), WM_COMMAND (events), + * WM_VSCROLL (scroll container). + */ +static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg, + WPARAM wp, LPARAM lp) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + + switch (msg) { + case WM_SIZE: + if (w) el_win32_reflow(slot); + return 0; + + case WM_EL_REFLOW: + if (w) el_win32_reflow(slot); + return 0; + + case WM_VSCROLL: { + if (!w || w->kind != EL_WIDGET_SCROLL) break; + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_ALL; + GetScrollInfo(hwnd, SB_VERT, &si); + int pos = si.nPos; + switch (LOWORD(wp)) { + case SB_LINEUP: pos -= 20; break; + case SB_LINEDOWN: pos += 20; break; + case SB_PAGEUP: pos -= si.nPage; break; + case SB_PAGEDOWN: pos += si.nPage; break; + case SB_THUMBTRACK: pos = si.nTrackPos; break; + case SB_TOP: pos = si.nMin; break; + case SB_BOTTOM: pos = si.nMax; break; + } + if (pos < si.nMin) pos = si.nMin; + if (pos > si.nMax - (int)si.nPage) pos = si.nMax - (int)si.nPage; + si.fMask = SIF_POS; + si.nPos = pos; + SetScrollInfo(hwnd, SB_VERT, &si, TRUE); + /* Scroll child. */ + if (w->child_count > 0) { + ElWidget* cw_ = el_widget_get(w->children[0]); + if (cw_ && cw_->hwnd) { + RECT cr; GetClientRect(hwnd, &cr); + SetWindowPos(cw_->hwnd, NULL, 0, -pos, + cr.right - cr.left, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + } + } + return 0; + } + + case WM_COMMAND: { + /* Route click / change notifications to el callbacks. */ + HWND ctrl = (HWND)lp; + if (!ctrl) break; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (!cw) break; + + WORD notif = HIWORD(wp); + if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) { + el_win32_invoke_cb(cw->cb_click, cs, ""); + } else if (cw->kind == EL_WIDGET_TEXTFIELD) { + if (notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + } else if (cw->kind == EL_WIDGET_TEXTAREA) { + if (notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + } + /* Propagate to parent so top-level window also sees it. */ + HWND parent = GetParent(hwnd); + if (parent) SendMessageW(parent, msg, wp, lp); + return 0; + } + + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORBTN: { + HWND ctrl = (HWND)lp; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (cw) { + HDC hdc = (HDC)wp; + if (cw->has_fg) SetTextColor(hdc, cw->fg_color); + if (cw->has_bg) { + SetBkColor(hdc, cw->bg_color); + if (cw->bg_brush) return (LRESULT)cw->bg_brush; + } + } + break; + } + + case WM_ERASEBKGND: { + if (w && w->has_bg && w->bg_brush) { + HDC hdc = (HDC)wp; + RECT rc; + GetClientRect(hwnd, &rc); + FillRect(hdc, &rc, w->bg_brush); + return 1; + } + break; + } + + case WM_DESTROY: + return 0; + } + + return DefWindowProcW(hwnd, msg, wp, lp); +} + +/* ── Top-level window WndProc ────────────────────────────────────────────── */ +static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg, + WPARAM wp, LPARAM lp) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + + switch (msg) { + case WM_SIZE: + if (w) el_win32_reflow(slot); + return 0; + + case WM_EL_REFLOW: + if (w) el_win32_reflow(slot); + return 0; + + case WM_COMMAND: { + /* Bubble COMMAND messages from child controls. */ + HWND ctrl = (HWND)lp; + if (!ctrl) break; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (!cw) { + /* Not a direct slot child — pass to DefWindowProc. */ + break; + } + WORD notif = HIWORD(wp); + if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) { + el_win32_invoke_cb(cw->cb_click, cs, ""); + } else if ((cw->kind == EL_WIDGET_TEXTFIELD || + cw->kind == EL_WIDGET_TEXTAREA) && notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + return 0; + } + + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORBTN: { + HWND ctrl = (HWND)lp; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (cw) { + HDC hdc = (HDC)wp; + if (cw->has_fg) SetTextColor(hdc, cw->fg_color); + if (cw->has_bg) { + SetBkColor(hdc, cw->bg_color); + if (cw->bg_brush) return (LRESULT)cw->bg_brush; + } + } + break; + } + + case WM_ERASEBKGND: { + if (w && w->has_bg && w->bg_brush) { + HDC hdc = (HDC)wp; + RECT rc; + GetClientRect(hwnd, &rc); + FillRect(hdc, &rc, w->bg_brush); + return 1; + } + break; + } + + case WM_GETMINMAXINFO: { + /* Stored in the window's GWLP_USERDATA as packed two ints: min_w | min_h. */ + LONG_PTR ud = GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (ud) { + int min_w = (int)(ud & 0xFFFF); + int min_h = (int)((ud >> 16) & 0xFFFF); + MINMAXINFO* mm = (MINMAXINFO*)lp; + if (min_w > 0) mm->ptMinTrackSize.x = min_w; + if (min_h > 0) mm->ptMinTrackSize.y = min_h; + } + return 0; + } + + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + + return DefWindowProcW(hwnd, msg, wp, lp); +} + +/* ── Subclass proc for EDIT controls (Enter key → on_submit) ────────────── */ +static LRESULT CALLBACK el_edit_subclass_proc(HWND hwnd, UINT msg, WPARAM wp, + LPARAM lp, UINT_PTR uid, + DWORD_PTR ref_data) { + (void)uid; (void)ref_data; + if (msg == WM_KEYDOWN && wp == VK_RETURN) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + if (w && w->cb_click) { + /* Gather current text as data. */ + int len = GetWindowTextLengthW(hwnd); + WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR)); + if (wbuf) { + GetWindowTextW(hwnd, wbuf, len + 1); + char* utf8 = el_wide_to_utf8(wbuf); + free(wbuf); + el_win32_invoke_cb(w->cb_click, slot, utf8); + free(utf8); + } + return 0; /* suppress default beep */ + } + } + return DefSubclassProc(hwnd, msg, wp, lp); +} + +/* ── Window-class registration ────────────────────────────────────────────── */ +static int _el_classes_registered = 0; + +static void el_win32_register_classes(void) { + if (_el_classes_registered) return; + _el_classes_registered = 1; + + /* Initialize common controls (needed for SetWindowSubclass). */ + INITCOMMONCONTROLSEX icc; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES; + InitCommonControlsEx(&icc); + + HINSTANCE hinst = GetModuleHandleW(NULL); + + /* Container class (VStack, HStack, ZStack, Scroll) */ + WNDCLASSW wc; + memset(&wc, 0, sizeof(wc)); + wc.lpfnWndProc = el_container_wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = EL_WNDCLASS_VSTACK; + RegisterClassW(&wc); + + wc.lpszClassName = EL_WNDCLASS_HSTACK; + RegisterClassW(&wc); + + wc.lpszClassName = EL_WNDCLASS_ZSTACK; + RegisterClassW(&wc); + + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpszClassName = EL_WNDCLASS_SCROLL; + RegisterClassW(&wc); + + /* Top-level window class */ + memset(&wc, 0, sizeof(wc)); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = el_toplevel_wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = EL_WNDCLASS_WINDOW; + RegisterClassW(&wc); +} + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_win32_init — one-time initialisation. Must be called before any other + * el_win32_* function. + */ +void el_win32_init(void) { + static int done = 0; + if (done) return; + done = 1; + el_win32_register_classes(); +} + +/* + * el_win32_run_loop — enter the Win32 message loop. Never returns. + * Must be called from the main thread after all windows are shown. + */ +void el_win32_run_loop(void) { + MSG msg; + while (GetMessageW(&msg, NULL, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +int64_t el_win32_window_create(const char* title, int width, int height, + int min_width, int min_height) { + el_win32_register_classes(); + HINSTANCE hinst = GetModuleHandleW(NULL); + WCHAR* wtitle = el_utf8_to_wide(title ? title : ""); + + DWORD style = WS_OVERLAPPEDWINDOW; + HWND hwnd = CreateWindowW( + EL_WNDCLASS_WINDOW, + wtitle, + style, + CW_USEDEFAULT, CW_USEDEFAULT, + width, height, + NULL, NULL, hinst, NULL); + + free(wtitle); + if (!hwnd) return -1; + + /* Pack min_width / min_height into GWLP_USERDATA (low 16 = min_w, next 16 = min_h). */ + LONG_PTR ud = ((LONG_PTR)(min_height & 0xFFFF) << 16) | (LONG_PTR)(min_width & 0xFFFF); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, ud); + + int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + w->spacing = 0; + + return handle; +} + +void el_win32_window_show(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return; + ShowWindow(w->hwnd, SW_SHOW); + UpdateWindow(w->hwnd); +} + +void el_win32_window_set_title(int64_t handle, const char* title) { + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return; + WCHAR* wt = el_utf8_to_wide(title ? title : ""); + SetWindowTextW(w->hwnd, wt); + free(wt); +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t el_win32_make_container(const WCHAR* class_name, ElWidgetKind kind, + int spacing) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + class_name, + L"", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, + 0, 0, 0, 0, + HWND_MESSAGE, /* temporary parent; reparented on add_child */ + NULL, + GetModuleHandleW(NULL), + NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(kind, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + w->spacing = spacing; + return handle; +} + +int64_t el_win32_vstack_create(int spacing) { + return el_win32_make_container(EL_WNDCLASS_VSTACK, EL_WIDGET_VSTACK, spacing); +} + +int64_t el_win32_hstack_create(int spacing) { + return el_win32_make_container(EL_WNDCLASS_HSTACK, EL_WIDGET_HSTACK, spacing); +} + +int64_t el_win32_zstack_create(void) { + return el_win32_make_container(EL_WNDCLASS_ZSTACK, EL_WIDGET_ZSTACK, 0); +} + +int64_t el_win32_scroll_create(void) { + HWND hwnd = CreateWindowW( + EL_WNDCLASS_SCROLL, + L"", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_VSCROLL, + 0, 0, 0, 0, + HWND_MESSAGE, + NULL, + GetModuleHandleW(NULL), + NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(EL_WIDGET_SCROLL, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_win32_label_create(const char* text) { + el_win32_register_classes(); + WCHAR* wt = el_utf8_to_wide(text ? text : ""); + HWND hwnd = CreateWindowW( + L"STATIC", + wt, + WS_CHILD | WS_VISIBLE | SS_LEFT, + 0, 0, 0, 24, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + free(wt); + if (!hwnd) return -1; + return el_widget_alloc(EL_WIDGET_LABEL, hwnd); +} + +int64_t el_win32_button_create(const char* label) { + el_win32_register_classes(); + WCHAR* wl = el_utf8_to_wide(label ? label : ""); + HWND hwnd = CreateWindowW( + L"BUTTON", + wl, + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 0, 0, 0, 32, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + free(wl); + if (!hwnd) return -1; + return el_widget_alloc(EL_WIDGET_BUTTON, hwnd); +} + +int64_t el_win32_text_field_create(const char* placeholder) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"EDIT", + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER | + ES_AUTOHSCROLL | ES_LEFT, + 0, 0, 0, 26, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + /* Set placeholder / cue banner (Windows Vista+). */ + if (placeholder && *placeholder) { + WCHAR* wp = el_utf8_to_wide(placeholder); + SendMessageW(hwnd, EM_SETCUEBANNER, TRUE, (LPARAM)wp); + free(wp); + } + + int64_t handle = el_widget_alloc(EL_WIDGET_TEXTFIELD, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + /* Subclass to intercept VK_RETURN for on_submit. */ + SetWindowSubclass(hwnd, el_edit_subclass_proc, (UINT_PTR)handle, 0); + return handle; +} + +int64_t el_win32_text_area_create(const char* placeholder) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"EDIT", + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER | + ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, + 0, 0, 0, 80, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + /* EDIT multiline does not support EM_SETCUEBANNER, so no placeholder. */ + (void)placeholder; + + int64_t handle = el_widget_alloc(EL_WIDGET_TEXTAREA, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + return handle; +} + +int64_t el_win32_image_create(const char* path) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"STATIC", + L"", + WS_CHILD | WS_VISIBLE | SS_BITMAP | SS_REALSIZECONTROL, + 0, 0, 0, 0, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(EL_WIDGET_IMAGE, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + + if (path && *path) { + WCHAR* wpath = el_utf8_to_wide(path); + HBITMAP hbmp = (HBITMAP)LoadImageW(NULL, wpath, IMAGE_BITMAP, + 0, 0, + LR_LOADFROMFILE | LR_DEFAULTSIZE); + free(wpath); + if (hbmp) { + w->hbmp = hbmp; + SendMessageW(hwnd, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hbmp); + } + } + + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_win32_widget_set_text(int64_t handle, const char* text) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + WCHAR* wt = el_utf8_to_wide(text ? text : ""); + SetWindowTextW(w->hwnd, wt); + free(wt); +} + +const char* el_win32_widget_get_text(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return strdup(""); + int len = GetWindowTextLengthW(w->hwnd); + if (len <= 0) return strdup(""); + WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR)); + if (!wbuf) return strdup(""); + GetWindowTextW(w->hwnd, wbuf, len + 1); + char* result = el_wide_to_utf8(wbuf); + free(wbuf); + return result; /* caller must free() */ +} + +void el_win32_widget_set_color(int64_t handle, float r, float g, float b, float a) { + (void)a; /* Win32 classic does not support per-control alpha */ + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + w->fg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f)); + w->has_fg = 1; + InvalidateRect(w->hwnd, NULL, TRUE); +} + +void el_win32_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + (void)a; + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + w->bg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f)); + w->has_bg = 1; + if (w->bg_brush) DeleteObject(w->bg_brush); + w->bg_brush = CreateSolidBrush(w->bg_color); + InvalidateRect(w->hwnd, NULL, TRUE); +} + +void el_win32_widget_set_font(int64_t handle, const char* family, int size, int bold) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + + LOGFONTW lf; + memset(&lf, 0, sizeof(lf)); + lf.lfHeight = -MulDiv(size > 0 ? size : 12, GetDeviceCaps(GetDC(NULL), LOGPIXELSY), 72); + lf.lfWeight = bold ? FW_BOLD : FW_NORMAL; + lf.lfCharSet = DEFAULT_CHARSET; + + if (family && *family && strcmp(family, "system") != 0) { + WCHAR* wf = el_utf8_to_wide(family); + wcsncpy(lf.lfFaceName, wf, LF_FACESIZE - 1); + free(wf); + } + /* family == NULL or "system" → lfFaceName stays zeroed → system font */ + + HFONT hfont = CreateFontIndirectW(&lf); + if (!hfont) return; + + if (w->font) DeleteObject(w->font); + w->font = hfont; + SendMessageW(w->hwnd, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0)); +} + +void el_win32_widget_set_padding(int64_t handle, int top, int right, + int bottom, int left) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->pad_top = top; + w->pad_right = right; + w->pad_bottom = bottom; + w->pad_left = left; + /* Trigger reflow if already parented. */ + if (w->hwnd) PostMessageW(w->hwnd, WM_EL_REFLOW, 0, 0); +} + +void el_win32_widget_set_width(int64_t handle, int width) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->fixed_w = width; + if (w->hwnd) { + RECT r; GetWindowRect(w->hwnd, &r); + int h = r.bottom - r.top; + SetWindowPos(w->hwnd, NULL, 0, 0, width, h, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +} + +void el_win32_widget_set_height(int64_t handle, int height) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->fixed_h = height; + if (w->hwnd) { + RECT r; GetWindowRect(w->hwnd, &r); + int cw = r.right - r.left; + SetWindowPos(w->hwnd, NULL, 0, 0, cw, height, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +} + +void el_win32_widget_set_flex(int64_t handle, int flex) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->flex = flex; + /* Let parent reflow absorb the change. */ +} + +void el_win32_widget_set_corner_radius(int64_t handle, int radius) { + /* + * Classic Win32 does not have native per-control corner radii. + * If radius > 0 we apply a rounded rectangular window region so the + * control at least clips to a rounded rect. This is approximate — it + * does not anti-alias and may look boxy on high-DPI displays. + */ + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd || radius <= 0) return; + RECT r; GetWindowRect(w->hwnd, &r); + int ww = r.right - r.left; + int wh = r.bottom - r.top; + if (ww <= 0 || wh <= 0) return; + HRGN rgn = CreateRoundRectRgn(0, 0, ww, wh, radius * 2, radius * 2); + SetWindowRgn(w->hwnd, rgn, TRUE); + /* Note: SetWindowRgn takes ownership of rgn; do NOT DeleteObject it. */ +} + +void el_win32_widget_set_disabled(int64_t handle, int disabled) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + EnableWindow(w->hwnd, disabled ? FALSE : TRUE); +} + +void el_win32_widget_set_hidden(int64_t handle, int hidden) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + ShowWindow(w->hwnd, hidden ? SW_HIDE : SW_SHOW); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_win32_widget_add_child — attach child widget to parent. + * + * - WINDOW / VSTACK / HSTACK / ZSTACK: SetParent child into container. + * - SCROLL: first child becomes the scrolled content. + * + * After reparenting, triggers a layout reflow of the parent. + */ +void el_win32_widget_add_child(int64_t parent, int64_t child) { + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw || !pw->hwnd || !cw->hwnd) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add window as child */ + if (pw->child_count >= EL_WIN32_MAX_CHILDREN) return; + + /* Reparent the child HWND into the container. */ + SetParent(cw->hwnd, pw->hwnd); + + /* Make sure the WS_CHILD style is set and WS_POPUP is cleared. */ + LONG_PTR style = GetWindowLongPtrW(cw->hwnd, GWL_STYLE); + style |= WS_CHILD; + style &= ~WS_POPUP; + SetWindowLongPtrW(cw->hwnd, GWL_STYLE, style); + ShowWindow(cw->hwnd, SW_SHOW); + + pw->children[pw->child_count++] = child; + + /* Reflow parent so the new child gets positioned. */ + el_win32_reflow(parent); +} + +void el_win32_widget_remove_child(int64_t parent, int64_t child) { + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!cw || !cw->hwnd) return; + + /* Detach HWND from parent. */ + SetParent(cw->hwnd, NULL); + + /* Remove from parent's child list. */ + if (pw) { + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == child) { + memmove(&pw->children[i], &pw->children[i + 1], + (size_t)(pw->child_count - i - 1) * sizeof(int64_t)); + pw->child_count--; + break; + } + } + el_win32_reflow(parent); + } +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_win32_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_win32_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_win32_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit (Enter key) is stored in cb_click, same as AppKit. */ + el_win32_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_win32_widget_destroy(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + DestroyWindow(w->hwnd); + el_widget_free_slot(handle); +} + +/* ── Manifest reader ──────────────────────────────────────────────────────── */ +/* + * Same parser as the macOS implementation in el_seed.c. + * Reads the app{} block from a manifest.el file: + * + * 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 failure. + * + * NOTE: el_seed.c already defines __manifest_read for both EL_TARGET_MACOS and + * EL_TARGET_WIN32 using the same parser body. We re-implement it here because + * el_seed.c's implementation is inside #ifdef EL_TARGET_MACOS; the Win32 + * version lives here, symmetric with the macOS one. + */ + +static void w32_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +static char* w32_manifest_read_string(const char** p) { + w32_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = (char*)malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +static int64_t w32_manifest_read_int(const char** p) { + w32_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; +} + +static const char* w32_manifest_find_app_block(const char* src) { + while (*src) { + 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 helpers — mirror of what el_seed.c provides. */ +#ifndef EL_CSTR +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#endif +#ifndef EL_STR +#define EL_STR(p) ((el_val_t)(uintptr_t)(p)) +#endif + +el_val_t el_win32_manifest_read(const char* path) { + if (!path || !*path) return EL_STR(strdup("{}")); + + FILE* f = fopen(path, "r"); + if (!f) return EL_STR(strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return EL_STR(strdup("{}")); } + char* src = (char*)malloc((size_t)sz + 1); + if (!src) { fclose(f); return EL_STR(strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = w32_manifest_find_app_block(src); + if (!app_start) { free(src); return EL_STR(strdup("{}")); } + + char* title = NULL; + int64_t width = 1200, height = 800; + int64_t min_w = 600, min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + 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; } + while (*cur == ' ' || *cur == '\t') cur++; + + if (key_len == 12 && strncmp(key_start, "window_title", 12) == 0) { free(title); title = w32_manifest_read_string(&cur); } + else if (key_len == 12 && strncmp(key_start, "window_width", 12) == 0) { width = w32_manifest_read_int(&cur); } + else if (key_len == 13 && strncmp(key_start, "window_height", 13) == 0) { height = w32_manifest_read_int(&cur); } + else if (key_len == 9 && strncmp(key_start, "min_width", 9) == 0) { min_w = w32_manifest_read_int(&cur); } + else if (key_len == 10 && strncmp(key_start, "min_height", 10) == 0) { min_h = w32_manifest_read_int(&cur); } + else { while (*cur && *cur != '\n' && *cur != '}') cur++; } + } + + free(src); + + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = (char*)malloc(buf_sz); + if (!buf) { free(title); return EL_STR(strdup("{}")); } + 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 EL_STR(buf); +} + +/* ── el_seed.c __* builtins for EL_TARGET_WIN32 ──────────────────────────── */ +/* + * These are the thin el_val_t wrappers that el_seed.c (with EL_TARGET_WIN32) + * will call. They mirror the EL_TARGET_MACOS section in el_seed.c exactly. + * + * el_seed.c includes this file's declarations via el_native_target.h and + * links el_win32.o. The __* symbol definitions below are compiled into + * el_win32.c itself (not el_seed.c) when EL_TARGET_WIN32 is defined, keeping + * el_seed.c platform-agnostic. + */ + +/* Helper: extract el string pointer. */ +static inline const char* _w32_cstr(el_val_t v) { + return (const char*)(uintptr_t)v; +} +static inline el_val_t _w32_str(const char* p) { + return (el_val_t)(uintptr_t)p; +} + +/* Bit-cast helper for float args (el_val_t carries IEEE 754 doubles). */ +static inline double _w32_to_float(el_val_t v) { + union { double d; int64_t i; } u; u.i = (int64_t)v; return u.d; +} + +void __native_init(void) { el_win32_init(); } +void __native_run_loop(void) { el_win32_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_win32_window_create( + _w32_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_win32_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_win32_window_set_title((int64_t)h, _w32_cstr(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_win32_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_win32_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_win32_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_win32_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_win32_label_create(_w32_cstr(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_win32_button_create(_w32_cstr(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_win32_text_field_create(_w32_cstr(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_win32_text_area_create(_w32_cstr(placeholder)); } +el_val_t __image_create(el_val_t path) { return (el_val_t)el_win32_image_create(_w32_cstr(path)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_win32_widget_set_text((int64_t)h, _w32_cstr(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_win32_widget_get_text((int64_t)h); + /* el_win32_widget_get_text returns a malloc'd string; caller treats as + * short-lived (same contract as el_appkit version). */ + return s ? _w32_str(s) : _w32_str(strdup("")); +} + +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_win32_widget_set_color((int64_t)h, + (float)_w32_to_float(r), (float)_w32_to_float(g), + (float)_w32_to_float(b), (float)_w32_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_win32_widget_set_bg_color((int64_t)h, + (float)_w32_to_float(r), (float)_w32_to_float(g), + (float)_w32_to_float(b), (float)_w32_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_win32_widget_set_font((int64_t)h, _w32_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_win32_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_win32_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_win32_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_win32_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_win32_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_win32_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_win32_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } + +void __widget_add_child(el_val_t p, el_val_t c) { el_win32_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_win32_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_win32_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_win32_widget_on_click((int64_t)h, _w32_cstr(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_win32_widget_on_change((int64_t)h, _w32_cstr(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_win32_widget_on_submit((int64_t)h, _w32_cstr(fn_name)); } + +el_val_t __manifest_read(el_val_t path) { + return el_win32_manifest_read(_w32_cstr(path)); +} + +#endif /* EL_TARGET_WIN32 */ diff --git a/lang/el-compiler/runtime/new-platform b/lang/el-compiler/runtime/new-platform new file mode 100755 index 0000000..86add28 --- /dev/null +++ b/lang/el-compiler/runtime/new-platform @@ -0,0 +1,711 @@ +#!/usr/bin/env bash +# new-platform — scaffold a new el-native platform bridge +# +# Usage: ./new-platform +# +# Example: +# ./new-platform myplatform +# +# Creates el_myplatform.c with all 33 required __* functions stubbed out, +# the ElWidget slot table, and the dlsym callback dispatcher. +# +# After running, follow the printed instructions to wire the bridge into +# el_native_target.h and el_seed.c. +# +# See PLATFORM_BRIDGE_SPEC.md for the full bridge contract. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Argument validation ─────────────────────────────────────────────────────── + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + echo "" >&2 + echo " lowercase identifier, e.g. myrtos, wayland, qt6" >&2 + echo "" >&2 + echo "This creates el_.c in the current directory." >&2 + exit 1 +fi + +PLATFORM_LOWER="${1,,}" # force lowercase +PLATFORM_UPPER="${PLATFORM_LOWER^^}" # force uppercase + +# Validate: letters, digits, underscores only +if [[ ! "${PLATFORM_LOWER}" =~ ^[a-z][a-z0-9_]*$ ]]; then + echo "Error: platform name must start with a letter and contain only a-z, 0-9, _" >&2 + exit 1 +fi + +OUTPUT_FILE="${SCRIPT_DIR}/el_${PLATFORM_LOWER}.c" + +if [[ -f "${OUTPUT_FILE}" ]]; then + echo "Error: ${OUTPUT_FILE} already exists." >&2 + echo " Remove it first if you want to regenerate it." >&2 + exit 1 +fi + +# ── Generate the bridge file ────────────────────────────────────────────────── + +cat > "${OUTPUT_FILE}" << BRIDGE_FILE +/* + * el_${PLATFORM_LOWER}.c — el-native platform bridge for ${PLATFORM_UPPER}. + * + * Generated by new-platform. Replace the TODO stubs with real implementations. + * See PLATFORM_BRIDGE_SPEC.md for the full contract. + * + * ── Slot system ────────────────────────────────────────────────────────────── + * Every widget (window, button, label, container, image) is stored in a static + * array of ElWidget structs. The el program holds an int64_t "handle" which is + * a direct index into that array. Rules: + * - Slot 0 is NEVER valid. Scans start at index 1. + * - Handle -1 means "invalid" / "create failed". + * - Maximum 4096 concurrent widgets. + * - el_widget_get() returns NULL for 0, negative, out-of-range, or FREE slots. + * + * ── Callback ABI ───────────────────────────────────────────────────────────── + * When a platform event fires (click, text change), call el_${PLATFORM_LOWER}_invoke_cb: + * + * el_${PLATFORM_LOWER}_invoke_cb(w->cb_click, slot_index, ""); + * + * The El callback signature: + * fn handler(handle: Int, data: String) -> Void + * compiles to: + * void handler(int64_t handle, int64_t data) + * where data is a const char* cast to int64_t (el String representation). + * + * ── Thread safety ──────────────────────────────────────────────────────────── + * ALL platform UI calls must run on the main/UI thread. If your platform + * delivers events on background threads (e.g., from a network callback that + * updates a label), marshal to the main thread before calling any widget op. + * + * ── Compile ────────────────────────────────────────────────────────────────── + * cc -DEL_TARGET_${PLATFORM_UPPER} \\ + * \$(pkg-config --cflags ) \\ + * -c el_${PLATFORM_LOWER}.c -o el_${PLATFORM_LOWER}.o + * + * ── Link ───────────────────────────────────────────────────────────────────── + * cc el_${PLATFORM_LOWER}.o el_seed.o el_runtime.o -o myapp \\ + * \$(pkg-config --libs ) -ldl -lpthread + */ + +#ifdef EL_TARGET_${PLATFORM_UPPER} + +/* ── TODO: add platform-specific includes here ────────────────────────────── + * Examples: + * #include // GTK4 + * #include // SDL2 + * #include "lvgl/lvgl.h" // LVGL + * #include // Win32 + */ + +#include +#include +#include +#include +#include /* dlsym — replace with GetProcAddress on Windows */ + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_${PLATFORM_UPPER}_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; + + /* TODO: add your platform's native widget reference here. + * Examples: + * GtkWidget* widget; // GTK4 + * SDL_Rect rect; // SDL2 (no object, just geometry) + * lv_obj_t* obj; // LVGL + * HWND hwnd; // Win32 + * void* native; // generic pointer + */ + void* native; /* platform widget reference — replace as needed */ + + /* Text content (cached for platforms that don't provide a get-text API) */ + char* text; + + /* Foreground / background color (RGBA, 0.0-1.0) */ + float fg_r, fg_g, fg_b, fg_a; + float bg_r, bg_g, bg_b, bg_a; + + /* Geometry */ + int width; /* 0 = not set */ + int height; /* 0 = not set */ + int flex; /* 0 = hug content, >0 = expand */ + + /* Padding (top, right, bottom, left) */ + int pad_top, pad_right, pad_bottom, pad_left; + + /* Corner radius */ + int corner_radius; + + /* State */ + int disabled; /* 0 = enabled, 1 = disabled */ + int hidden; /* 0 = visible, 1 = hidden */ + + /* Event callbacks — El function name resolved at event time via dlsym */ + char* cb_click; /* on_click / on_submit */ + char* cb_change; /* on_change */ +} ElWidget; + +static ElWidget _el_widgets[EL_${PLATFORM_UPPER}_MAX_WIDGETS]; + +/* ── Slot management ──────────────────────────────────────────────────────── */ + +static int64_t el_widget_alloc(ElWidgetKind kind, void* native) { + for (int i = 1; i < EL_${PLATFORM_UPPER}_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + memset(&_el_widgets[i], 0, sizeof(ElWidget)); + _el_widgets[i].kind = kind; + _el_widgets[i].native = native; + return (int64_t)i; + } + } + fprintf(stderr, "el_${PLATFORM_LOWER}: widget table full (max %d)\n", + EL_${PLATFORM_UPPER}_MAX_WIDGETS); + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_${PLATFORM_UPPER}_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; + /* TODO: release w->native (toolkit-specific) */ + free(w->text); + free(w->cb_click); + free(w->cb_change); + memset(w, 0, sizeof(ElWidget)); /* sets kind = EL_WIDGET_FREE (0) */ +} + +/* ── Callback dispatcher ─────────────────────────────────────────────────── */ +/* + * Invoke an El function by symbol name. The El function must have the + * compiled signature: void fn(int64_t handle, int64_t data) + * where data is a const char* cast to int64_t (el String representation). + * + * On platforms without dlsym (e.g., Windows), replace with: + * GetProcAddress(GetModuleHandle(NULL), fn_name) + * On embedded targets without dynamic linking, maintain a manual symbol table. + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_${PLATFORM_LOWER}_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) { + fprintf(stderr, "el_${PLATFORM_LOWER}: callback symbol not found: %s\n", fn_name); + return; + } + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── Lifecycle ────────────────────────────────────────────────────────────── */ + +/* + * el_${PLATFORM_LOWER}_init — initialize the platform toolkit. + * Must be idempotent (safe to call more than once). + * Called once from __native_init before any widget creation. + */ +void el_${PLATFORM_LOWER}_init(void) { + static int done = 0; + if (done) return; + done = 1; + + /* TODO: initialize your platform toolkit here. + * Examples: + * gtk_init(NULL, NULL); // GTK4 + * SDL_Init(SDL_INIT_VIDEO); // SDL2 + * lv_init(); // LVGL + */ +} + +/* + * el_${PLATFORM_LOWER}_run_loop — start the platform event loop. + * On most platforms this NEVER returns. Exceptions: Android (no-op). + * Must be called from the main thread. + */ +void el_${PLATFORM_LOWER}_run_loop(void) { + /* TODO: start the platform event/render loop. + * Examples: + * gtk_main(); // GTK4 + * while (1) { SDL_PollEvent(...); render(); SDL_Delay(16); } // SDL2 + * while (1) { lv_task_handler(); usleep(5000); } // LVGL + */ +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +int64_t el_${PLATFORM_LOWER}_window_create(const char* title, int w, int h, + int mw, int mh) { + /* TODO: create a top-level window. + * title may be NULL — treat as "". + * mw/mh are minimum dimensions (0 = no minimum). + * Return slot handle on success, -1 on failure. + */ + (void)title; (void)w; (void)h; (void)mw; (void)mh; + return -1; /* TODO: implement */ +} + +void el_${PLATFORM_LOWER}_window_show(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* TODO: make the window visible. */ +} + +void el_${PLATFORM_LOWER}_window_set_title(int64_t handle, const char* title) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* TODO: update the window title. title may be NULL — treat as "". */ + (void)title; +} + +/* ── Layout containers ────────────────────────────────────────────────────── */ + +int64_t el_${PLATFORM_LOWER}_vstack_create(int spacing) { + /* TODO: create a vertical linear container with the given spacing (px). */ + (void)spacing; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_hstack_create(int spacing) { + /* TODO: create a horizontal linear container with the given spacing (px). */ + (void)spacing; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_zstack_create(void) { + /* TODO: create a z-axis overlay container (children overlap). */ + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_scroll_create(void) { + /* TODO: create a scrollable container (vertical scroll, first child = content). */ + return -1; /* TODO: implement */ +} + +/* ── Leaf widgets ─────────────────────────────────────────────────────────── */ + +int64_t el_${PLATFORM_LOWER}_label_create(const char* text) { + /* TODO: create a non-editable text label. text may be NULL — treat as "". */ + (void)text; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_button_create(const char* label) { + /* TODO: create a clickable button with the given label text. + * Wire up the platform event handler so on_click callbacks fire later. */ + (void)label; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_text_field_create(const char* placeholder) { + /* TODO: create a single-line text input. placeholder may be NULL. */ + (void)placeholder; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_text_area_create(const char* placeholder) { + /* TODO: create a multi-line text input (scrollable). placeholder may be NULL. */ + (void)placeholder; + return -1; /* TODO: implement */ +} + +int64_t el_${PLATFORM_LOWER}_image_create(const char* path_or_name) { + /* TODO: create an image widget. + * Try path_or_name as a filesystem path first, then as a named resource. + * If neither resolves, create an empty image widget (do not crash). */ + (void)path_or_name; + return -1; /* TODO: implement */ +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_${PLATFORM_LOWER}_widget_set_text(int64_t handle, const char* text) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* TODO: update text on label, button, text field, text area, or window title. + * Dispatch on w->kind. text may be NULL — treat as "". + * Cache in w->text if the platform has no get-text API. */ + free(w->text); + w->text = strdup(text ? text : ""); +} + +const char* el_${PLATFORM_LOWER}_widget_get_text(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return ""; + /* TODO: return the current text of the widget. + * If the platform provides a get-text API, use it. + * Otherwise return w->text (populated by set_text). + * NEVER return NULL — return "" on failure. */ + return w->text ? w->text : ""; +} + +void el_${PLATFORM_LOWER}_widget_set_color(int64_t handle, + float r, float g, float b, float a) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->fg_r = r; w->fg_g = g; w->fg_b = b; w->fg_a = a; + /* TODO: apply foreground (text) color to the platform widget. */ +} + +void el_${PLATFORM_LOWER}_widget_set_bg_color(int64_t handle, + float r, float g, float b, float a) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->bg_r = r; w->bg_g = g; w->bg_b = b; w->bg_a = a; + /* TODO: apply background color to the platform widget. */ +} + +void el_${PLATFORM_LOWER}_widget_set_font(int64_t handle, + const char* family, int size, int bold) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* TODO: set font on text-bearing widgets (label, button, text field, text area). + * family may be "system" — use the platform default font in that case. + * Fall back to system font if family is not found. */ + (void)family; (void)size; (void)bold; +} + +void el_${PLATFORM_LOWER}_widget_set_padding(int64_t handle, + int top, int right, + int bottom, int left) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->pad_top = top; w->pad_right = right; + w->pad_bottom = bottom; w->pad_left = left; + /* TODO: apply padding/insets to the platform container or text widget. */ +} + +void el_${PLATFORM_LOWER}_widget_set_width(int64_t handle, int width) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->width = width; + /* TODO: apply a fixed-width constraint to the platform widget. */ +} + +void el_${PLATFORM_LOWER}_widget_set_height(int64_t handle, int height) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->height = height; + /* TODO: apply a fixed-height constraint to the platform widget. */ +} + +void el_${PLATFORM_LOWER}_widget_set_flex(int64_t handle, int flex) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->flex = flex; + /* TODO: set the flex/expand factor. + * flex > 0 → widget expands to fill available space. + * flex == 0 → widget hugs content size. + * Maps to: content hugging priority (AppKit), hexpand/vexpand (GTK4), + * layout_weight (Android), stretch factor (SDL2 manual layout). */ +} + +void el_${PLATFORM_LOWER}_widget_set_corner_radius(int64_t handle, int radius) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->corner_radius = radius; + /* TODO: apply rounded corners (requires GPU layer / backing surface on most platforms). */ +} + +void el_${PLATFORM_LOWER}_widget_set_disabled(int64_t handle, int disabled) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->disabled = disabled; + /* TODO: enable or disable the widget (buttons, text fields). No-op for containers/labels. */ +} + +void el_${PLATFORM_LOWER}_widget_set_hidden(int64_t handle, int hidden) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->hidden = hidden; + /* TODO: show or hide the widget. + * hidden=1 → invisible but still occupies layout space (visibility:hidden semantics). + * For windows: minimize or hide. */ +} + +/* ── Tree management ─────────────────────────────────────────────────────── */ + +void el_${PLATFORM_LOWER}_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; + + /* TODO: attach child to parent. Dispatch on pw->kind: + * EL_WIDGET_WINDOW → add to root content view/container + * EL_WIDGET_VSTACK → add as vertical child + * EL_WIDGET_HSTACK → add as horizontal child + * EL_WIDGET_ZSTACK → add as overlapping subview + * EL_WIDGET_SCROLL → set as the scrollable content view (first child only) + * default → add as plain subview + * Do NOT add a window widget as a child. */ + (void)pw; (void)cw; +} + +void el_${PLATFORM_LOWER}_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; + /* TODO: detach child from parent. The child slot remains allocated (not destroyed). */ + (void)pw; (void)cw; +} + +void el_${PLATFORM_LOWER}_widget_destroy(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* TODO: remove the widget from its parent/superview. + * For windows: close the window. + * Free any platform delegate/target objects stored in side tables. + * Then free the slot: */ + el_widget_free(handle); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_${PLATFORM_LOWER}_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_${PLATFORM_LOWER}_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_${PLATFORM_LOWER}_widget_on_submit(int64_t handle, const char* fn_name) { + /* Submit (Enter key in text field) reuses the cb_click slot, matching AppKit convention. */ + el_${PLATFORM_LOWER}_widget_on_click(handle, fn_name); +} + +/* ── Example event handler (adapt to your platform's callback mechanism) ─── */ +/* + * When a button is clicked in your platform event loop, call: + * + * ElWidget* w = el_widget_get(slot_index); + * if (w && w->cb_click) { + * el_${PLATFORM_LOWER}_invoke_cb(w->cb_click, slot_index, ""); + * } + * + * When a text field changes: + * + * ElWidget* w = el_widget_get(slot_index); + * if (w && w->cb_change) { + * el_${PLATFORM_LOWER}_invoke_cb(w->cb_change, slot_index, current_text); + * } + */ + +#endif /* EL_TARGET_${PLATFORM_UPPER} */ +BRIDGE_FILE + +chmod 644 "${OUTPUT_FILE}" + +# ── Print next-steps instructions ──────────────────────────────────────────── + +UPPER="${PLATFORM_UPPER}" +LOWER="${PLATFORM_LOWER}" + +cat << INSTRUCTIONS + +Created: el_${LOWER}.c + +Next steps: + ──────────────────────────────────────────────────────────────────────────── + 1. Add to el_native_target.h (inside a new #ifdef EL_TARGET_${UPPER} block): + + #ifdef EL_TARGET_${UPPER} + + void __native_init(void); + void __native_run_loop(void); + + el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); + void __window_show(el_val_t handle); + void __window_set_title(el_val_t handle, el_val_t title); + + el_val_t __vstack_create(el_val_t spacing); + el_val_t __hstack_create(el_val_t spacing); + el_val_t __zstack_create(void); + el_val_t __scroll_create(void); + + el_val_t __label_create(el_val_t text); + el_val_t __button_create(el_val_t label); + el_val_t __text_field_create(el_val_t placeholder); + el_val_t __text_area_create(el_val_t placeholder); + el_val_t __image_create(el_val_t path_or_name); + + void __widget_set_text(el_val_t handle, el_val_t text); + el_val_t __widget_get_text(el_val_t handle); + void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); + void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); + void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); + void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); + void __widget_set_width(el_val_t handle, el_val_t width); + void __widget_set_height(el_val_t handle, el_val_t height); + void __widget_set_flex(el_val_t handle, el_val_t flex); + void __widget_set_corner_radius(el_val_t handle, el_val_t radius); + void __widget_set_disabled(el_val_t handle, el_val_t disabled); + void __widget_set_hidden(el_val_t handle, el_val_t hidden); + + void __widget_add_child(el_val_t parent, el_val_t child); + void __widget_remove_child(el_val_t parent, el_val_t child); + void __widget_destroy(el_val_t handle); + + void __widget_on_click(el_val_t handle, el_val_t fn_name); + void __widget_on_change(el_val_t handle, el_val_t fn_name); + void __widget_on_submit(el_val_t handle, el_val_t fn_name); + + el_val_t __manifest_read(el_val_t path); + + #endif /* EL_TARGET_${UPPER} */ + + ──────────────────────────────────────────────────────────────────────────── + 2. Add to el_seed.c (copy the EL_TARGET_MACOS block as a template and + substitute el_appkit_ → el_${LOWER}_): + + #ifdef EL_TARGET_${UPPER} + + /* Forward declarations — implemented in el_${LOWER}.c */ + extern void el_${LOWER}_init(void); + extern void el_${LOWER}_run_loop(void); + extern int64_t el_${LOWER}_window_create(const char* title, int w, int h, int mw, int mh); + extern void el_${LOWER}_window_show(int64_t handle); + extern void el_${LOWER}_window_set_title(int64_t handle, const char* title); + extern int64_t el_${LOWER}_vstack_create(int spacing); + extern int64_t el_${LOWER}_hstack_create(int spacing); + extern int64_t el_${LOWER}_zstack_create(void); + extern int64_t el_${LOWER}_scroll_create(void); + extern int64_t el_${LOWER}_label_create(const char* text); + extern int64_t el_${LOWER}_button_create(const char* label); + extern int64_t el_${LOWER}_text_field_create(const char* placeholder); + extern int64_t el_${LOWER}_text_area_create(const char* placeholder); + extern int64_t el_${LOWER}_image_create(const char* path_or_name); + extern void el_${LOWER}_widget_set_text(int64_t handle, const char* text); + extern const char* el_${LOWER}_widget_get_text(int64_t handle); + extern void el_${LOWER}_widget_set_color(int64_t h, float r, float g, float b, float a); + extern void el_${LOWER}_widget_set_bg_color(int64_t h, float r, float g, float b, float a); + extern void el_${LOWER}_widget_set_font(int64_t h, const char* family, int size, int bold); + extern void el_${LOWER}_widget_set_padding(int64_t h, int top, int right, int bottom, int left); + extern void el_${LOWER}_widget_set_width(int64_t h, int width); + extern void el_${LOWER}_widget_set_height(int64_t h, int height); + extern void el_${LOWER}_widget_set_flex(int64_t h, int flex); + extern void el_${LOWER}_widget_set_corner_radius(int64_t h, int radius); + extern void el_${LOWER}_widget_set_disabled(int64_t h, int disabled); + extern void el_${LOWER}_widget_set_hidden(int64_t h, int hidden); + extern void el_${LOWER}_widget_add_child(int64_t parent, int64_t child); + extern void el_${LOWER}_widget_remove_child(int64_t parent, int64_t child); + extern void el_${LOWER}_widget_destroy(int64_t handle); + extern void el_${LOWER}_widget_on_click(int64_t h, const char* fn_name); + extern void el_${LOWER}_widget_on_change(int64_t h, const char* fn_name); + extern void el_${LOWER}_widget_on_submit(int64_t h, const char* fn_name); + + void __native_init(void) { el_${LOWER}_init(); } + void __native_run_loop(void) { el_${LOWER}_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_${LOWER}_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_${LOWER}_window_show((int64_t)h); } + void __window_set_title(el_val_t h, el_val_t t) { el_${LOWER}_window_set_title((int64_t)h, EL_CSTR(t)); } + + el_val_t __vstack_create(el_val_t s) { return (el_val_t)el_${LOWER}_vstack_create((int)(int64_t)s); } + el_val_t __hstack_create(el_val_t s) { return (el_val_t)el_${LOWER}_hstack_create((int)(int64_t)s); } + el_val_t __zstack_create(void) { return (el_val_t)el_${LOWER}_zstack_create(); } + el_val_t __scroll_create(void) { return (el_val_t)el_${LOWER}_scroll_create(); } + + el_val_t __label_create(el_val_t t) { return (el_val_t)el_${LOWER}_label_create(EL_CSTR(t)); } + el_val_t __button_create(el_val_t l) { return (el_val_t)el_${LOWER}_button_create(EL_CSTR(l)); } + el_val_t __text_field_create(el_val_t p) { return (el_val_t)el_${LOWER}_text_field_create(EL_CSTR(p)); } + el_val_t __text_area_create(el_val_t p) { return (el_val_t)el_${LOWER}_text_area_create(EL_CSTR(p)); } + el_val_t __image_create(el_val_t p) { return (el_val_t)el_${LOWER}_image_create(EL_CSTR(p)); } + + void __widget_set_text(el_val_t h, el_val_t t) { el_${LOWER}_widget_set_text((int64_t)h, EL_CSTR(t)); } + el_val_t __widget_get_text(el_val_t h) { + const char* s = el_${LOWER}_widget_get_text((int64_t)h); + return EL_STR(s ? 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_${LOWER}_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_${LOWER}_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_${LOWER}_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_${LOWER}_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_${LOWER}_widget_set_width((int64_t)h, (int)(int64_t)w); } + void __widget_set_height(el_val_t h, el_val_t ht) { el_${LOWER}_widget_set_height((int64_t)h, (int)(int64_t)ht); } + void __widget_set_flex(el_val_t h, el_val_t f) { el_${LOWER}_widget_set_flex((int64_t)h, (int)(int64_t)f); } + void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_${LOWER}_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } + void __widget_set_disabled(el_val_t h, el_val_t d) { el_${LOWER}_widget_set_disabled((int64_t)h, (int)(int64_t)d); } + void __widget_set_hidden(el_val_t h, el_val_t hid) { el_${LOWER}_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } + void __widget_add_child(el_val_t p, el_val_t c) { el_${LOWER}_widget_add_child((int64_t)p, (int64_t)c); } + void __widget_remove_child(el_val_t p, el_val_t c) { el_${LOWER}_widget_remove_child((int64_t)p, (int64_t)c); } + void __widget_destroy(el_val_t h) { el_${LOWER}_widget_destroy((int64_t)h); } + void __widget_on_click(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_click((int64_t)h, EL_CSTR(fn)); } + void __widget_on_change(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_change((int64_t)h, EL_CSTR(fn)); } + void __widget_on_submit(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_submit((int64_t)h, EL_CSTR(fn)); } + + #endif /* EL_TARGET_${UPPER} */ + + ──────────────────────────────────────────────────────────────────────────── + 3. Implement each TODO in el_${LOWER}.c. + + 4. Compile: + cc -DEL_TARGET_${UPPER} -Wall \\ + \$(pkg-config --cflags 2>/dev/null) \\ + -c el_${LOWER}.c -o el_${LOWER}.o + + 5. Link: + cc el_${LOWER}.o el_seed.o el_runtime.o -o myapp \\ + \$(pkg-config --libs 2>/dev/null) \\ + -ldl -lpthread + + See PLATFORM_BRIDGE_SPEC.md for the full bridge contract and gotchas. +INSTRUCTIONS diff --git a/ui/examples/native-hello-android/.gitignore b/ui/examples/native-hello-android/.gitignore new file mode 100644 index 0000000..0ca7c87 --- /dev/null +++ b/ui/examples/native-hello-android/.gitignore @@ -0,0 +1,6 @@ +local.properties +.gradle/ +build/ +app/build/ +*.iml +.idea/ diff --git a/ui/examples/native-hello-android/app/build.gradle b/ui/examples/native-hello-android/app/build.gradle new file mode 100644 index 0000000..dd1dd28 --- /dev/null +++ b/ui/examples/native-hello-android/app/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.neuron.el' + compileSdk 34 + + defaultConfig { + applicationId "com.neuron.el" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + externalNativeBuild { + cmake { + cppFlags "" + arguments "-DANDROID_STL=c++_shared" + } + } + + ndk { + // Build for the two most relevant ABIs. Add x86/x86_64 for emulator. + abiFilters "arm64-v8a", "armeabi-v7a", "x86_64" + } + } + + externalNativeBuild { + cmake { + path "src/main/jni/CMakeLists.txt" + version "3.22.1" + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + jniDebuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // No third-party dependencies — el-native uses only android.* framework classes. + implementation 'androidx.appcompat:appcompat:1.6.1' +} diff --git a/ui/examples/native-hello-android/app/proguard-rules.pro b/ui/examples/native-hello-android/app/proguard-rules.pro new file mode 100644 index 0000000..c0c619f --- /dev/null +++ b/ui/examples/native-hello-android/app/proguard-rules.pro @@ -0,0 +1,4 @@ +# Add project specific ProGuard rules here. +# Keep ElBridge and MainActivity so JNI symbol names stay intact. +-keep class com.neuron.el.ElBridge { *; } +-keep class com.neuron.el.MainActivity { *; } diff --git a/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml b/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..780d219 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java new file mode 100644 index 0000000..25f72d6 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElApp.java @@ -0,0 +1,18 @@ +package com.neuron.el; + +import android.app.Application; + +/** + * ElApp — Application subclass for native-hello-android. + * + * Currently minimal: exists as an anchor for future app-level initialisation + * (crash reporting, global state, etc.). Listed in AndroidManifest.xml as + * android:name=".ElApp". + */ +public class ElApp extends Application { + + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java new file mode 100644 index 0000000..52cb402 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/ElBridge.java @@ -0,0 +1,711 @@ +/* + * ElBridge.java — Android Java companion to el_android.c. + * + * All public methods are static. The C JNI layer calls these to create views, + * set properties, and manage the widget tree. Views are identified by integer + * slot indices matching the C-side handle values. + * + * Threading: every method that touches a View dispatches to the UI thread + * using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch + * until the UI thread completes the operation. This mirrors the AppKit + * dispatch_sync(main_queue, ^{}) pattern in el_appkit.m. + * + * Callbacks: Java sets listeners on views that call back into C via: + * nativeOnClick(int slot) + * nativeOnChange(int slot, String text) + * nativeOnSubmit(int slot, String text) + * These are declared native and implemented in el_android.c. + * + * Usage (in your Activity.onCreate): + * System.loadLibrary("elruntime"); + * ElBridge.init(this); + * + * The native library calls __native_init() which calls nativeRegisterActivity + * via the C side; alternatively call ElBridge.init(this) directly from Java. + * + * Compile requirements: + * Android minSdkVersion 21 (Lollipop) or higher. + * No third-party dependencies — uses only android.* framework classes. + * For image loading from arbitrary file paths, BitmapFactory is used. + * To replace with Glide/Picasso, edit createImageView only. + */ + +package com.neuron.el; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.util.concurrent.CountDownLatch; + +public class ElBridge { + + /* ── Native callbacks (implemented in el_android.c) ─────────────────── */ + + public static native void nativeOnClick(int slot); + public static native void nativeOnChange(int slot, String text); + public static native void nativeOnSubmit(int slot, String text); + public static native void nativeRegisterActivity(Activity activity); + + /* ── State ───────────────────────────────────────────────────────────── */ + + private static final int MAX_SLOTS = 4096; + + private static Activity sActivity; + private static Handler sUiHandler; + private static View[] sViews = new View[MAX_SLOTS]; + private static int sNextSlot = 1; /* slot 0 reserved / null */ + + /* ── Init ────────────────────────────────────────────────────────────── */ + + /** + * Must be called from the Activity before any widget operations. + * Typically called from Activity.onCreate after System.loadLibrary. + */ + public static void init(Activity activity) { + sActivity = activity; + sUiHandler = new Handler(Looper.getMainLooper()); + nativeRegisterActivity(activity); + } + + /* ── Slot management ─────────────────────────────────────────────────── */ + + private static int allocSlot(View v) { + /* Find a free slot starting from sNextSlot, wrap around. */ + for (int i = 0; i < MAX_SLOTS - 1; i++) { + int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1; + if (sViews[idx] == null) { + sViews[idx] = v; + sNextSlot = (idx % (MAX_SLOTS - 1)) + 1; + return idx; + } + } + android.util.Log.e("ElBridge", "allocSlot: slot table full"); + return -1; + } + + private static View getView(int slot) { + if (slot <= 0 || slot >= MAX_SLOTS) return null; + return sViews[slot]; + } + + /* ── UI-thread dispatch helper ───────────────────────────────────────── */ + + /* + * Dispatch r on the UI thread and block until it completes. + * Safe to call from the UI thread itself (runs inline without posting). + */ + private static void runSync(final Runnable r) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + } else { + final CountDownLatch latch = new CountDownLatch(1); + sUiHandler.post(new Runnable() { + @Override public void run() { + try { r.run(); } finally { latch.countDown(); } + } + }); + try { latch.await(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /* ── Integer slot returning runSync helper ───────────────────────────── */ + + private interface IntSupplier { int get(); } + + private static int runSyncInt(final IntSupplier s) { + final int[] result = { -1 }; + runSync(new Runnable() { + @Override public void run() { result[0] = s.get(); } + }); + return result[0]; + } + + /* ── Context accessor ────────────────────────────────────────────────── */ + + private static Context ctx() { return sActivity; } + + /* ── View creation ───────────────────────────────────────────────────── */ + + /** + * Create a LinearLayout. + * @param orientation 1=VERTICAL, 0=HORIZONTAL + * @param spacing gap between children in dp; applied as bottom/right margin + */ + public static int createLinearLayout(final int orientation, final int spacing) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + LinearLayout ll = new LinearLayout(ctx()); + ll.setOrientation(orientation == 1 + ? LinearLayout.VERTICAL + : LinearLayout.HORIZONTAL); + ll.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + /* Spacing is stored so addChild can apply margins. */ + ll.setTag(R_TAG_SPACING, spacing); + return allocSlot(ll); + } + }); + } + + /** Create a FrameLayout (ZStack equivalent). */ + public static int createFrameLayout() { + return runSyncInt(new IntSupplier() { + @Override public int get() { + FrameLayout fl = new FrameLayout(ctx()); + fl.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(fl); + } + }); + } + + /** Create a ScrollView. */ + public static int createScrollView() { + return runSyncInt(new IntSupplier() { + @Override public int get() { + ScrollView sv = new ScrollView(ctx()); + sv.setLayoutParams(new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + sv.setFillViewport(true); + return allocSlot(sv); + } + }); + } + + /** Create a TextView with initial text. */ + public static int createTextView(final String text) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + TextView tv = new TextView(ctx()); + tv.setText(text != null ? text : ""); + tv.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(tv); + } + }); + } + + /** Create a Button with a label. */ + public static int createButton(final String label) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + Button btn = new Button(ctx()); + btn.setText(label != null ? label : ""); + btn.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(btn); + } + }); + } + + /** + * Create an EditText. + * @param placeholder hint text + * @param singleLine true = single-line text field; false = multi-line text area + */ + public static int createEditText(final String placeholder, final boolean singleLine) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + EditText et = new EditText(ctx()); + et.setHint(placeholder != null ? placeholder : ""); + if (singleLine) { + et.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + et.setMaxLines(1); + et.setSingleLine(true); + } else { + et.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + et.setMinLines(3); + et.setSingleLine(false); + et.setGravity(Gravity.TOP | Gravity.START); + } + et.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(et); + } + }); + } + + /** + * Create an ImageView, loading from a file path via BitmapFactory. + * If path is null/empty the ImageView is created with no image. + */ + public static int createImageView(final String path) { + return runSyncInt(new IntSupplier() { + @Override public int get() { + ImageView iv = new ImageView(ctx()); + iv.setScaleType(ImageView.ScaleType.FIT_CENTER); + iv.setAdjustViewBounds(true); + if (path != null && !path.isEmpty()) { + Bitmap bmp = BitmapFactory.decodeFile(path); + if (bmp != null) { + iv.setImageBitmap(bmp); + } else { + android.util.Log.w("ElBridge", + "createImageView: failed to decode " + path); + } + } + iv.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return allocSlot(iv); + } + }); + } + + /* ── Window operations ───────────────────────────────────────────────── */ + + /** Set the Activity's content view to the view at slot. */ + public static void setContentView(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null && sActivity != null) { + sActivity.setContentView(v); + } + } + }); + } + + /** Set the Activity title. */ + public static void setTitle(final String title) { + runSync(new Runnable() { + @Override public void run() { + if (sActivity != null) { + sActivity.setTitle(title != null ? title : ""); + } + } + }); + } + + /* ── Tree operations ─────────────────────────────────────────────────── */ + + /** + * Add child view to parent view. + * LinearLayout: child added as arranged child with spacing margin. + * ScrollView: child replaces current document view. + * FrameLayout / other ViewGroup: plain addView. + */ + public static void addChild(final int parentSlot, final int childSlot) { + runSync(new Runnable() { + @Override public void run() { + View parent = getView(parentSlot); + View child = getView(childSlot); + if (parent == null || child == null) return; + + /* Remove child from existing parent first. */ + if (child.getParent() instanceof ViewGroup) { + ((ViewGroup) child.getParent()).removeView(child); + } + + if (parent instanceof LinearLayout) { + LinearLayout ll = (LinearLayout) parent; + Object tag = ll.getTag(R_TAG_SPACING); + int spacing = (tag instanceof Integer) ? (Integer) tag : 0; + LinearLayout.LayoutParams lp; + Object existingLp = child.getLayoutParams(); + if (existingLp instanceof LinearLayout.LayoutParams) { + lp = (LinearLayout.LayoutParams) existingLp; + } else { + lp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + /* Apply spacing as margin on the leading/top edge (after first child). */ + if (ll.getChildCount() > 0 && spacing > 0) { + int px = dpToPx(spacing); + if (ll.getOrientation() == LinearLayout.VERTICAL) { + lp.topMargin = px; + } else { + lp.leftMargin = px; + } + } + child.setLayoutParams(lp); + ll.addView(child); + } else if (parent instanceof ScrollView) { + ScrollView sv = (ScrollView) parent; + sv.removeAllViews(); + sv.addView(child); + } else if (parent instanceof ViewGroup) { + ((ViewGroup) parent).addView(child); + } + } + }); + } + + /** Remove child from its parent. */ + public static void removeChild(final int parentSlot, final int childSlot) { + runSync(new Runnable() { + @Override public void run() { + View parent = getView(parentSlot); + View child = getView(childSlot); + if (parent instanceof ViewGroup && child != null) { + ((ViewGroup) parent).removeView(child); + } + } + }); + } + + /** Remove the view from its parent and release the slot. */ + public static void destroyView(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + if (v.getParent() instanceof ViewGroup) { + ((ViewGroup) v.getParent()).removeView(v); + } + sViews[slot] = null; + } + }); + } + + /* ── Property setters ────────────────────────────────────────────────── */ + + public static void setText(final int slot, final String text) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + String s = text != null ? text : ""; + if (v instanceof EditText) { + ((EditText) v).setText(s); + } else if (v instanceof Button) { + ((Button) v).setText(s); + } else if (v instanceof TextView) { + ((TextView) v).setText(s); + } + } + }); + } + + public static String getText(final int slot) { + final String[] result = { "" }; + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v instanceof TextView) { + CharSequence cs = ((TextView) v).getText(); + result[0] = cs != null ? cs.toString() : ""; + } + } + }); + return result[0]; + } + + /** Set foreground text color. Components in [0,1]. */ + public static void setTextColor(final int slot, final float r, final float g, + final float b, final float a) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v instanceof TextView) { + ((TextView) v).setTextColor(floatToArgb(r, g, b, a)); + } + } + }); + } + + /** Set background color using a GradientDrawable so corner radius is preserved. */ + public static void setBackgroundColor(final int slot, final float r, final float g, + final float b, final float a) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ensureGradientBackground(v); + GradientDrawable gd = (GradientDrawable) v.getBackground(); + gd.setColor(floatToArgb(r, g, b, a)); + } + }); + } + + /** + * Set font family and size. + * family: "system" or null → system default; otherwise tries to load by name. + * bold: if true uses Typeface.BOLD. + */ + public static void setFont(final int slot, final String family, + final int sizeSp, final boolean bold) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof TextView)) return; + TextView tv = (TextView) v; + Typeface tf; + if (family != null && !family.isEmpty() + && !family.equals("system")) { + Typeface base = Typeface.create(family, + bold ? Typeface.BOLD : Typeface.NORMAL); + tf = (base != null) ? base + : Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL); + } else { + tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL); + } + tv.setTypeface(tf); + tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp); + } + }); + } + + /** Set padding in dp. */ + public static void setPadding(final int slot, final int top, final int right, + final int bottom, final int left) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) { + v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom)); + } + } + }); + } + + /** Set explicit width in dp. Passes MATCH_PARENT for negative values. */ + public static void setWidth(final int slot, final int widthDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp == null) lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.width = widthDp < 0 + ? ViewGroup.LayoutParams.MATCH_PARENT + : dpToPx(widthDp); + v.setLayoutParams(lp); + } + }); + } + + /** Set explicit height in dp. */ + public static void setHeight(final int slot, final int heightDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp == null) lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.height = heightDp < 0 + ? ViewGroup.LayoutParams.MATCH_PARENT + : dpToPx(heightDp); + v.setLayoutParams(lp); + } + }); + } + + /** + * Set flex weight on a child of a LinearLayout. + * flex > 0 → weight = flex, width/height = 0dp (expand). + * flex == 0 → weight = 0, wrap_content (shrink to content). + */ + public static void setFlex(final int slot, final int flex) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp; + if (flex > 0) { + llp.weight = (float) flex; + /* Determine orientation from parent to set 0dp on the right axis. */ + if (v.getParent() instanceof LinearLayout) { + LinearLayout parent = (LinearLayout) v.getParent(); + if (parent.getOrientation() == LinearLayout.VERTICAL) { + llp.height = 0; + } else { + llp.width = 0; + } + } + } else { + llp.weight = 0f; + } + v.setLayoutParams(llp); + } + } + }); + } + + /** Set corner radius in dp using a GradientDrawable background. */ + public static void setCornerRadius(final int slot, final float radiusDp) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + ensureGradientBackground(v); + GradientDrawable gd = (GradientDrawable) v.getBackground(); + gd.setCornerRadius(dpToPxF(radiusDp)); + } + }); + } + + public static void setEnabled(final int slot, final boolean enabled) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) v.setEnabled(enabled); + } + }); + } + + /** + * Show or hide a view. + * @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics) + */ + public static void setVisibility(final int slot, final boolean visible) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE); + } + }); + } + + /* ── Event listener registration ─────────────────────────────────────── */ + + /** Register an OnClickListener that calls back into C nativeOnClick. */ + public static void setOnClickListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (v == null) return; + final int capturedSlot = slot; + v.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + nativeOnClick(capturedSlot); + } + }); + } + }); + } + + /** + * Register a TextWatcher on an EditText that calls back nativeOnChange + * for every text change. + */ + public static void setOnChangeListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof EditText)) return; + final int capturedSlot = slot; + ((EditText) v).addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, + int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, + int before, int count) {} + @Override public void afterTextChanged(Editable s) { + nativeOnChange(capturedSlot, s != null ? s.toString() : ""); + } + }); + } + }); + } + + /** + * Register an OnEditorActionListener on a single-line EditText that calls + * nativeOnSubmit when the user presses the action/enter key. + */ + public static void setOnSubmitListener(final int slot) { + runSync(new Runnable() { + @Override public void run() { + View v = getView(slot); + if (!(v instanceof EditText)) return; + final int capturedSlot = slot; + ((EditText) v).setOnEditorActionListener( + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView tv, int actionId, + android.view.KeyEvent event) { + nativeOnSubmit(capturedSlot, + tv.getText() != null ? tv.getText().toString() : ""); + return true; + } + }); + } + }); + } + + /* ── Internal helpers ─────────────────────────────────────────────────── */ + + /* + * Tag key used to stash the spacing value on LinearLayouts so addChild + * can apply the correct margin between children. + * We use a stable integer resource-id-like value; because we do not have + * a resources file here we use View.generateViewId() lazily. + */ + private static int sSpacingTagKey = 0; + + private static int R_TAG_SPACING; + static { + R_TAG_SPACING = View.generateViewId(); + } + + /** Convert dp to pixels using the Activity's display metrics. */ + private static int dpToPx(float dp) { + if (sActivity == null) return (int) dp; + float density = sActivity.getResources().getDisplayMetrics().density; + return Math.round(dp * density); + } + + private static float dpToPxF(float dp) { + if (sActivity == null) return dp; + float density = sActivity.getResources().getDisplayMetrics().density; + return dp * density; + } + + /** Convert RGBA float components (0–1) to an Android ARGB int. */ + private static int floatToArgb(float r, float g, float b, float a) { + int ai = Math.round(a * 255f); + int ri = Math.round(r * 255f); + int gi = Math.round(g * 255f); + int bi = Math.round(b * 255f); + return Color.argb(ai, ri, gi, bi); + } + + /** + * Ensure the view has a GradientDrawable background so that both color + * and corner radius can be set independently. If the current background + * is already a GradientDrawable it is reused; otherwise a new transparent + * one is installed. + */ + private static void ensureGradientBackground(View v) { + if (!(v.getBackground() instanceof GradientDrawable)) { + GradientDrawable gd = new GradientDrawable(); + gd.setColor(Color.TRANSPARENT); + v.setBackground(gd); + } + } +} diff --git a/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java new file mode 100644 index 0000000..47eec0e --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/java/com/neuron/el/MainActivity.java @@ -0,0 +1,38 @@ +package com.neuron.el; + +import android.app.Activity; +import android.os.Bundle; + +/** + * MainActivity — entry point for native-hello-android. + * + * Loads the el native shared library, initialises ElBridge with the Activity + * reference (required before any widget operations), then hands control to the + * compiled el program via nativeMain(). + * + * The el boot sequence (native_init → window_from_manifest → app_build → + * window_show) runs inside nativeMain. __native_run_loop is a no-op on Android; + * the Activity lifecycle owns the UI thread after onCreate returns. + */ +public class MainActivity extends Activity { + + static { + System.loadLibrary("elnative"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Register this Activity with the bridge BEFORE nativeMain so that + // el_android_init can look up ElBridge methods and __native_init works. + ElBridge.init(this); + // Run the compiled el program. + nativeMain(); + } + + /** + * Implemented in el_android.c as Java_com_neuron_el_MainActivity_nativeMain. + * Calls the el program's main(), which runs the full boot sequence. + */ + private native void nativeMain(); +} diff --git a/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt b/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000..fbc3dea --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.22.1) +project("elnative") + +# +# CMakeLists.txt for native-hello-android +# +# Sources: +# el_android.c — Android JNI widget bridge +# el_seed.c — OS-boundary __-prefixed primitives +# el_runtime.c — High-level el builtins (str_len, json_*, etc.) +# el_native_vessel.c — el-native vessel (compiled from vessels/el-native/src/main.el) +# native_hello.c — App entry point (compiled from examples/native-hello/src/main.el) +# +# el_runtime.c and el_seed.c both define __-prefixed symbols. On Android the +# linker accepts duplicate weak symbols; the shared library loads one copy. +# The EL_TARGET_ANDROID guard in el_android.c ensures only the Android widget +# backend is compiled in. +# + +add_library( + elnative + SHARED + + # native_hello.c listed first so its main() takes precedence over the + # vessel's main() when --allow-multiple-definition is in effect. + native_hello.c + el_native_vessel.c + el_android.c + el_seed.c + el_runtime.c +) + +# EL_TARGET_ANDROID activates the Android JNI widget backend in el_android.c +# and the corresponding guards in el_seed.c / el_native_target.h. +target_compile_definitions(elnative PRIVATE + EL_TARGET_ANDROID +) + +target_include_directories(elnative PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# el_runtime.c and el_seed.c both define __-prefixed OS-boundary symbols. +# el_runtime.c's copies are weaker definitions; allow-multiple-definition lets +# the linker pick one copy silently (el_seed.c listed later = its copy wins +# in the Android linker's right-to-left resolution order). +target_link_options(elnative PRIVATE + "-Wl,--allow-multiple-definition" +) + +find_library(log-lib log) +find_library(android-lib android) + +target_link_libraries(elnative + ${log-lib} + ${android-lib} +) diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_android.c b/ui/examples/native-hello-android/app/src/main/jni/el_android.c new file mode 100644 index 0000000..5f629ba --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_android.c @@ -0,0 +1,964 @@ +/* + * el_android.c — Android JNI backend for the el native widget system. + * + * This file implements the Android widget layer that el_seed.c calls through + * to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart + * to el_appkit.m and presents the same C API surface. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_android_* C-callable functions declared here + * → ElBridge static methods in Java via JNI + * → android.view.View subclasses on the UI thread + * + * Widget handles: every widget (window root, view, control) is assigned an + * int64_t slot index into view_slots[]. The el program holds these as opaque + * Int values. Slot 0 is never valid (null handle = -1 convention). + * + * Threading: Android requires all UI operations to run on the main (UI) thread. + * Every JNI call that mutates a View is dispatched through + * Activity.runOnUiThread(Runnable) if the current thread is not the UI thread. + * el_android_run_loop is a no-op — Android lifecycle is driven by the Activity. + * + * Callback mechanism: when a widget fires an event Java calls + * nativeOnClick / nativeOnChange / nativeOnSubmit + * The C side looks up the registered El function name for that slot, then: + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This matches the __thread_create pattern in el_seed.c exactly. + * + * Compile / link (as part of libelruntime.so): + * Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID. + * Link flags: -landroid -llog -ldl + * + * Java companion: ElBridge.java in the same directory must be compiled into + * the Android application's APK (package com.neuron.el). + */ + +#ifdef EL_TARGET_ANDROID + +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Logging ─────────────────────────────────────────────────────────────── */ + +#define EL_TAG "ElAndroid" +#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__) +#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__) +#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__) + +/* ── JNI global state ────────────────────────────────────────────────────── */ + +static JavaVM *g_jvm = NULL; +static jobject g_activity = NULL; /* global ref to Activity */ +static jclass g_bridge_class = NULL; /* global ref to ElBridge class */ + +/* Cached method IDs on ElBridge — filled in el_android_init(). */ +static jmethodID g_mid_createLinearLayout = NULL; +static jmethodID g_mid_createFrameLayout = NULL; +static jmethodID g_mid_createScrollView = NULL; +static jmethodID g_mid_createTextView = NULL; +static jmethodID g_mid_createButton = NULL; +static jmethodID g_mid_createEditText = NULL; +static jmethodID g_mid_createImageView = NULL; +static jmethodID g_mid_setContentView = NULL; +static jmethodID g_mid_setTitle = NULL; +static jmethodID g_mid_addChild = NULL; +static jmethodID g_mid_removeChild = NULL; +static jmethodID g_mid_destroyView = NULL; +static jmethodID g_mid_setText = NULL; +static jmethodID g_mid_getText = NULL; +static jmethodID g_mid_setTextColor = NULL; +static jmethodID g_mid_setBackgroundColor = NULL; +static jmethodID g_mid_setFont = NULL; +static jmethodID g_mid_setPadding = NULL; +static jmethodID g_mid_setWidth = NULL; +static jmethodID g_mid_setHeight = NULL; +static jmethodID g_mid_setFlex = NULL; +static jmethodID g_mid_setCornerRadius = NULL; +static jmethodID g_mid_setEnabled = NULL; +static jmethodID g_mid_setVisibility = NULL; +static jmethodID g_mid_setOnClickListener = NULL; +static jmethodID g_mid_setOnChangeListener = NULL; +static jmethodID g_mid_setOnSubmitListener = NULL; +static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */ + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_ANDROID_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + jint slot; /* Java-side slot index (matches C index) */ + char *cb_click; /* El function name for click / submit events */ + char *cb_change; /* El function name for value-change events */ +} ElWidget; + +static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS]; + +static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) { + for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].slot = slot; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + return (int64_t)i; + } + } + EL_LOGE("el_widget_alloc: slot table full"); + return -1; +} + +static ElWidget *el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + w->kind = EL_WIDGET_FREE; + w->slot = -1; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── JNI environment helpers ─────────────────────────────────────────────── */ + +/* + * Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if + * needed (detaches in el_jni_detach_if_attached — call in pairs). + */ +static int g_was_attached = 0; /* thread-local would be cleaner but this is + safe for single-threaded el programs */ + +static JNIEnv *el_jni_env(void) { + if (!g_jvm) return NULL; + JNIEnv *env = NULL; + jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6); + if (rc == JNI_OK) { g_was_attached = 0; return env; } + if (rc == JNI_EDETACHED) { + if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) { + g_was_attached = 1; + return env; + } + } + EL_LOGE("el_jni_env: failed to obtain JNIEnv"); + return NULL; +} + +static void el_jni_detach_if_attached(void) { + if (g_was_attached && g_jvm) { + (*g_jvm)->DetachCurrentThread(g_jvm); + g_was_attached = 0; + } +} + +/* ── UI-thread dispatch ──────────────────────────────────────────────────── */ +/* + * Most ElBridge static methods already dispatch to the UI thread internally + * (they call Activity.runOnUiThread). The helper below is available for + * cases where the caller needs to be sure the call has completed before + * returning (ElBridge methods marked "sync" use a CountDownLatch internally). + * + * For the current implementation we call ElBridge methods directly; ElBridge + * itself marshals to the UI thread via Activity.runOnUiThread + latch. + * This keeps the C side simple and mirrors the AppKit dispatch_sync pattern. + */ + +/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */ + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)reserved; + g_jvm = vm; + EL_LOGI("JNI_OnLoad: el Android bridge loaded"); + return JNI_VERSION_1_6; +} + +/* ── el_android_init ─────────────────────────────────────────────────────── */ +/* + * Called from __native_init(). The Activity must have already called + * ElBridge.registerActivity(activity) from Java before this runs, which sets + * g_activity via the nativeRegisterActivity JNI method below. + * + * Caches all method IDs used later so individual widget calls avoid repeated + * FindClass / GetStaticMethodID lookups. + */ +void el_android_init(void) { + static int done = 0; + if (done) return; + done = 1; + + JNIEnv *env = el_jni_env(); + if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; } + + jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge"); + if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; } + g_bridge_class = (*env)->NewGlobalRef(env, cls); + (*env)->DeleteLocalRef(env, cls); + +#define CACHE_STATIC(var, name, sig) \ + var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \ + if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig) + + CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I"); + CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I"); + CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I"); + CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I"); + CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I"); + CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V"); + CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V"); + CACHE_STATIC(g_mid_addChild, "addChild", "(II)V"); + CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V"); + CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V"); + CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V"); + CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;"); + CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V"); + CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V"); + CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V"); + CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V"); + CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V"); + CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V"); + CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V"); + CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V"); + CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V"); + CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V"); + CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V"); + CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V"); + CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V"); +#undef CACHE_STATIC + + el_jni_detach_if_attached(); + EL_LOGI("el_android_init: complete"); +} + +/* ── JNI: Activity registration ─────────────────────────────────────────── */ + +/* + * Called from Java: ElBridge.registerActivity(activity) calls back here. + * Stores a global reference to the Activity so C code can dispatch to it. + */ +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls, + jobject activity) { + (void)cls; + if (g_activity) { + (*env)->DeleteGlobalRef(env, g_activity); + g_activity = NULL; + } + if (activity) { + g_activity = (*env)->NewGlobalRef(env, activity); + EL_LOGI("nativeRegisterActivity: activity registered"); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. + * Signature matches AppKit: fn(handle: Int, data: String) -> Void + * compiled to: void fn(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_android_invoke_cb(const char *fn_name, int64_t handle, + const char *data) { + if (!fn_name || !*fn_name) return; + void *sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; } + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */ + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) { + (void)env; (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_click) { + el_android_invoke_cb(w->cb_click, handle, ""); + } +} + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls, + jint slot, jstring text) { + (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_change) { + const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : ""; + el_android_invoke_cb(w->cb_change, handle, ctext); + if (text) (*env)->ReleaseStringUTFChars(env, text, ctext); + } +} + +JNIEXPORT void JNICALL +Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls, + jint slot, jstring text) { + (void)cls; + int64_t handle = (int64_t)slot; + ElWidget *w = el_widget_get(handle); + if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */ + const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : ""; + el_android_invoke_cb(w->cb_click, handle, ctext); + if (text) (*env)->ReleaseStringUTFChars(env, text, ctext); + } +} + +/* ── Helper: jstring from C string ──────────────────────────────────────── */ + +static jstring el_jstr(JNIEnv *env, const char *s) { + return (*env)->NewStringUTF(env, s ? s : ""); +} + +/* ── Window ──────────────────────────────────────────────────────────────── */ + +/* + * el_android_window_create — on Android a "window" is the root LinearLayout + * set as the Activity's content view. We create a vertical LinearLayout and + * store it. el_android_window_show calls setContentView on the Activity. + */ +int64_t el_android_window_create(const char *title, int width, int height, + int min_width, int min_height) { + (void)width; (void)height; (void)min_width; (void)min_height; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + + /* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)1 /* VERTICAL */, (jint)0); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; + } + + /* Set activity title */ + if (g_mid_setTitle && title) { + jstring jtitle = el_jstr(env, title); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); + } + + int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot); + el_jni_detach_if_attached(); + return handle; +} + +void el_android_window_show(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_window_set_title(int64_t handle, const char *title) { + (void)handle; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jtitle = el_jstr(env, title); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +int64_t el_android_vstack_create(int spacing) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)1 /* VERTICAL */, (jint)spacing); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_hstack_create(int spacing) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createLinearLayout, + (jint)0 /* HORIZONTAL */, (jint)spacing); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_zstack_create(void) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createFrameLayout); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_scroll_create(void) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createScrollView); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_android_label_create(const char *text) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jt = el_jstr(env, text); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createTextView, jt); + (*env)->DeleteLocalRef(env, jt); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_button_create(const char *label) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jl = el_jstr(env, label); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createButton, jl); + (*env)->DeleteLocalRef(env, jl); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_text_field_create(const char *placeholder) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, placeholder); + /* singleLine = true */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createEditText, jp, (jboolean)JNI_TRUE); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_text_area_create(const char *placeholder) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, placeholder); + /* singleLine = false → multiline EditText */ + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createEditText, jp, (jboolean)JNI_FALSE); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +int64_t el_android_image_create(const char *path) { + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return -1; + jstring jp = el_jstr(env, path); + jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class, + g_mid_createImageView, jp); + (*env)->DeleteLocalRef(env, jp); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; } + int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot); + el_jni_detach_if_attached(); + return h; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_android_widget_set_text(int64_t handle, const char *text) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jt = el_jstr(env, text); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText, + (jint)w->slot, jt); + (*env)->DeleteLocalRef(env, jt); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +const char *el_android_widget_get_text(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return ""; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return ""; + jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class, + g_mid_getText, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; } + const char *result = ""; + if (js) { + const char *cstr = (*env)->GetStringUTFChars(env, js, NULL); + result = cstr ? strdup(cstr) : ""; + if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr); + (*env)->DeleteLocalRef(env, js); + } + el_jni_detach_if_attached(); + return result; +} + +void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor, + (jint)w->slot, (jfloat)r, (jfloat)g, + (jfloat)b, (jfloat)a); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor, + (jint)w->slot, (jfloat)r, (jfloat)g, + (jfloat)b, (jfloat)a); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + jstring jfam = el_jstr(env, family); + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont, + (jint)w->slot, jfam, (jint)size, + (jboolean)(bold ? JNI_TRUE : JNI_FALSE)); + (*env)->DeleteLocalRef(env, jfam); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding, + (jint)w->slot, (jint)top, (jint)right, + (jint)bottom, (jint)left); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_width(int64_t handle, int width) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth, + (jint)w->slot, (jint)width); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_height(int64_t handle, int height) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight, + (jint)w->slot, (jint)height); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_flex(int64_t handle, int flex) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex, + (jint)w->slot, (jint)flex); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_corner_radius(int64_t handle, int radius) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius, + (jint)w->slot, (jfloat)radius); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_disabled(int64_t handle, int disabled) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled, + (jint)w->slot, + (jboolean)(disabled ? JNI_FALSE : JNI_TRUE)); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_set_hidden(int64_t handle, int hidden) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + /* visible=true means NOT hidden */ + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility, + (jint)w->slot, + (jboolean)(hidden ? JNI_FALSE : JNI_TRUE)); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +void el_android_widget_add_child(int64_t parent, int64_t child) { + ElWidget *pw = el_widget_get(parent); + ElWidget *cw = el_widget_get(child); + if (!pw || !cw) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild, + (jint)pw->slot, (jint)cw->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_remove_child(int64_t parent, int64_t child) { + ElWidget *pw = el_widget_get(parent); + ElWidget *cw = el_widget_get(child); + if (!pw || !cw) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild, + (jint)pw->slot, (jint)cw->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_android_widget_on_click(int64_t handle, const char *fn_name) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_click) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_on_change(int64_t handle, const char *fn_name) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_change) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +void el_android_widget_on_submit(int64_t handle, const char *fn_name) { + /* Submit stored in cb_click, same as AppKit. */ + ElWidget *w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + if (!w->cb_click) return; + JNIEnv *env = el_jni_env(); + if (!env || !g_bridge_class) return; + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + el_jni_detach_if_attached(); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_android_widget_destroy(int64_t handle) { + ElWidget *w = el_widget_get(handle); + if (!w) return; + JNIEnv *env = el_jni_env(); + if (env && g_bridge_class) { + (*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView, + (jint)w->slot); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + } + el_widget_free(handle); + el_jni_detach_if_attached(); +} + +/* ── Manifest reader ─────────────────────────────────────────────────────── */ +/* + * __manifest_read: parse the app{} block from a manifest file. + * Returns the raw file contents as an el_val_t (const char* cast). + * The caller (el program) parses the returned string. + * Reads from the filesystem; for APK assets use the AssetManager path instead. + */ +static char *el_read_file(const char *path) { + if (!path || !*path) return NULL; + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len <= 0) { fclose(f); return NULL; } + char *buf = (char *)malloc((size_t)len + 1); + if (!buf) { fclose(f); return NULL; } + fread(buf, 1, (size_t)len, f); + buf[len] = '\0'; + fclose(f); + return buf; +} + +el_val_t el_android_manifest_read(const char *path) { + char *contents = el_read_file(path); + if (!contents) return (el_val_t)(uintptr_t)""; + return (el_val_t)(uintptr_t)contents; /* caller owns allocation */ +} + +/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */ +/* + * These are the functions declared in el_native_target.h under EL_TARGET_ANDROID. + * They forward to the el_android_* internal functions above. + * + * The el_val_t / int64_t ABI matches the AppKit functions exactly: + * - Integer params passed as int64_t, extracted with (int) + * - String params passed as int64_t, extracted with (const char*)(uintptr_t) + * - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted + * with el_to_float / bit-cast union + */ + +static inline float el_val_to_float(el_val_t v) { + union { double d; int64_t i; } u; + u.i = v; + return (float)u.d; +} + +void __native_init(void) { + el_android_init(); +} + +void __native_run_loop(void) { + /* No-op on Android — lifecycle is driven by the Activity. */ +} + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_android_window_create( + (const char *)(uintptr_t)title, + (int)width, (int)height, (int)min_width, (int)min_height); +} + +void __window_show(el_val_t handle) { + el_android_window_show((int64_t)handle); +} + +void __window_set_title(el_val_t handle, el_val_t title) { + el_android_window_set_title((int64_t)handle, + (const char *)(uintptr_t)title); +} + +el_val_t __vstack_create(el_val_t spacing) { + return (el_val_t)el_android_vstack_create((int)spacing); +} + +el_val_t __hstack_create(el_val_t spacing) { + return (el_val_t)el_android_hstack_create((int)spacing); +} + +el_val_t __zstack_create(void) { + return (el_val_t)el_android_zstack_create(); +} + +el_val_t __scroll_create(void) { + return (el_val_t)el_android_scroll_create(); +} + +el_val_t __label_create(el_val_t text) { + return (el_val_t)el_android_label_create((const char *)(uintptr_t)text); +} + +el_val_t __button_create(el_val_t label) { + return (el_val_t)el_android_button_create((const char *)(uintptr_t)label); +} + +el_val_t __text_field_create(el_val_t placeholder) { + return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder); +} + +el_val_t __text_area_create(el_val_t placeholder) { + return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder); +} + +el_val_t __image_create(el_val_t path_or_name) { + return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name); +} + +void __widget_set_text(el_val_t handle, el_val_t text) { + el_android_widget_set_text((int64_t)handle, + (const char *)(uintptr_t)text); +} + +el_val_t __widget_get_text(el_val_t handle) { + return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle); +} + +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_android_widget_set_color((int64_t)handle, + el_val_to_float(r), el_val_to_float(g), + el_val_to_float(b), el_val_to_float(a)); +} + +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_android_widget_set_bg_color((int64_t)handle, + el_val_to_float(r), el_val_to_float(g), + el_val_to_float(b), el_val_to_float(a)); +} + +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold) { + el_android_widget_set_font((int64_t)handle, + (const char *)(uintptr_t)family, + (int)size, (int)bold); +} + +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_android_widget_set_padding((int64_t)handle, + (int)top, (int)right, (int)bottom, (int)left); +} + +void __widget_set_width(el_val_t handle, el_val_t width) { + el_android_widget_set_width((int64_t)handle, (int)width); +} + +void __widget_set_height(el_val_t handle, el_val_t height) { + el_android_widget_set_height((int64_t)handle, (int)height); +} + +void __widget_set_flex(el_val_t handle, el_val_t flex) { + el_android_widget_set_flex((int64_t)handle, (int)flex); +} + +void __widget_set_corner_radius(el_val_t handle, el_val_t radius) { + el_android_widget_set_corner_radius((int64_t)handle, (int)radius); +} + +void __widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_android_widget_set_disabled((int64_t)handle, (int)disabled); +} + +void __widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_android_widget_set_hidden((int64_t)handle, (int)hidden); +} + +void __widget_add_child(el_val_t parent, el_val_t child) { + el_android_widget_add_child((int64_t)parent, (int64_t)child); +} + +void __widget_remove_child(el_val_t parent, el_val_t child) { + el_android_widget_remove_child((int64_t)parent, (int64_t)child); +} + +void __widget_destroy(el_val_t handle) { + el_android_widget_destroy((int64_t)handle); +} + +void __widget_on_click(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_click((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +void __widget_on_change(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_change((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +void __widget_on_submit(el_val_t handle, el_val_t fn_name) { + el_android_widget_on_submit((int64_t)handle, + (const char *)(uintptr_t)fn_name); +} + +el_val_t __manifest_read(el_val_t path) { + return el_android_manifest_read((const char *)(uintptr_t)path); +} + +/* ── MainActivity JNI entry point ─────────────────────────────────────────── */ +/* + * Java_com_neuron_el_MainActivity_nativeMain — invoked from MainActivity.onCreate + * after ElBridge.init(this). Calls the el program's compiled main() which runs + * the boot sequence: native_init → window_from_manifest → app_build → window_show. + * __native_run_loop is a no-op on Android; the Activity lifecycle drives the UI. + */ +JNIEXPORT void JNICALL +Java_com_neuron_el_MainActivity_nativeMain(JNIEnv *env, jobject obj) { + (void)env; (void)obj; + extern int main(int argc, char **argv); + char *argv[] = {"el-app", NULL}; + main(1, argv); +} + +#endif /* EL_TARGET_ANDROID */ diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h b/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c b/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c new file mode 100644 index 0000000..0682965 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int el_vessel_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _tl_arena.count = 0; +} + +/* ── Scoped arena for CLI use ─────────────────────────────────────────────── * + * CLI programs never call el_request_start/end, so all strdup allocations are + * permanent. el_arena_push/pop let the compiler free intermediate strings + * after each compilation unit. + * + * el_arena_push() — activates the arena if not already active, saves the + * current arena count as a mark, and returns it as an el_val_t Int. + * el_arena_pop(mark) — frees all strings allocated since the push mark and + * resets the count. If count reaches 0, deactivates the arena. + */ +#define EL_ARENA_SCOPE_DEPTH 32 +static _Thread_local size_t _tl_arena_scope[EL_ARENA_SCOPE_DEPTH]; +static _Thread_local int _tl_arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_tl_arena_active) { + _tl_arena_active = 1; + } + if (_tl_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) { + _tl_arena_scope[_tl_arena_scope_depth++] = _tl_arena.count; + } + return (el_val_t)(int64_t)_tl_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _tl_arena.count) save = 0; + for (size_t i = save; i < _tl_arena.count; i++) { + if (_tl_arena.ptrs[i]) { + free(_tl_arena.ptrs[i]); + _tl_arena.ptrs[i] = NULL; + } + } + _tl_arena.count = save; + if (_tl_arena_scope_depth > 0) _tl_arena_scope_depth--; + if (save == 0) _tl_arena_active = 0; + return 0; +} + +/* Persistent allocation — bypasses the arena (state_set, engram internals). */ +static char* el_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +el_val_t print(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) fputs(str, stdout); + return 0; +} + +el_val_t readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +/* __read_n — read exactly n bytes from stdin. + * Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read + * exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and + * returns the buffer as an El String. Returns "" on EOF or I/O error. + * + * Used by the El LSP server to read JSON-RPC message bodies after parsing + * the Content-Length header. readline() cannot be used for the body because + * it stops at the first \n and LSP JSON bodies are not newline-terminated. */ +el_val_t __read_n(el_val_t nv) { + int64_t n = EL_INT(nv); + if (n <= 0) return el_wrap_str(el_strdup("")); + char* buf = malloc((size_t)n + 1); + if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)n, stdin); + buf[got] = '\0'; + if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); } + /* Track in arena so the allocation is freed when the request ends. */ + el_arena_track(buf); + return el_wrap_str(buf); +} + +/* __print_raw — write a string to stdout without any modification. + * Unlike println/print (which call puts/fputs and may add newlines or flush + * in platform-specific ways), this uses fwrite with the exact byte count so + * that embedded \r\n pairs in LSP Content-Length headers survive intact. */ +void __print_raw(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return; + size_t len = strlen(s); + fwrite(s, 1, len, stdout); + fflush(stdout); +} + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)atoll(s); +} + +/* native_str_to_int — El compiler-generated alias for str_to_int. + * Converts a string el_val_t to its integer representation. */ +el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); } + +el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * the loop is observable. + */ + +/* ── JSON error helper (used by HTTP, PQ, crypto stubs) ─────────────────── */ + +/* JSON-escape an arbitrary C string into an allocated buffer. */ +static char* json_escape_alloc(const char* s) { + if (!s) return el_strdup(""); + JsonBuf b; jb_init(&b); + for (const char* p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': jb_puts(&b, "\\\""); break; + case '\\': jb_puts(&b, "\\\\"); break; + case '\n': jb_puts(&b, "\\n"); break; + case '\r': jb_puts(&b, "\\r"); break; + case '\t': jb_puts(&b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(&b, tmp); + } else jb_putc(&b, (char)c); + } + } + return b.buf; +} + +static el_val_t http_error_json(const char* msg) { + char* esc = json_escape_alloc(msg ? msg : "unknown error"); + char* buf = el_strbuf(strlen(esc) + 16); + sprintf(buf, "{\"error\":\"%s\"}", esc); + free(esc); + return el_wrap_str(buf); +} + +#ifdef HAVE_CURL +/* ── HTTP client write-callback buffer ───────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* HTTP timeout (ms) — read once from EL_HTTP_TIMEOUT_MS, default 60000. + * Applied via CURLOPT_TIMEOUT_MS on every libcurl request. */ +static long _el_http_timeout_ms = -1; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* http_post_json_with_headers — POST with Content-Type: application/json plus + * any additional headers supplied as an El map. Combines http_post_json and + * http_post_with_headers: the Content-Type header is always prepended so + * callers do not have to include it in their map. */ +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + /* Append caller-supplied headers from the map */ + ElMap* m = as_map(headers_map); + if (m) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve(el_val_t port, el_val_t handler) { + /* If `handler` looks like a string name, register it as the active handler. */ + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; } + /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6. + * This makes `localhost` work in browsers that resolve it to ::1 first. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler_v2(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler4); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler_v2(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + return 0; + } + /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + return 1; +} + +// stdout_to_file / stdout_restore — redirect process stdout to a file and +// restore it. Used by the compiler's JS post-processing pipeline to capture +// codegen output before piping through terser / obfuscator. +#include +static int _el_saved_stdout_fd = -1; + +el_val_t stdout_to_file(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return (el_val_t)(int64_t)-1; + fflush(stdout); + _el_saved_stdout_fd = dup(STDOUT_FILENO); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return (el_val_t)(int64_t)-1; + dup2(fd, STDOUT_FILENO); + close(fd); + return (el_val_t)(int64_t)0; +} + +el_val_t stdout_restore(void) { + if (_el_saved_stdout_fd >= 0) { + fflush(stdout); + dup2(_el_saved_stdout_fd, STDOUT_FILENO); + close(_el_saved_stdout_fd); + _el_saved_stdout_fd = -1; + } + return (el_val_t)(int64_t)0; +} + +// exec_command — run a shell command, return exit code (0 = success). +// Used by elb and other El tooling to invoke subprocesses. +el_val_t exec_command(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + return lst; +} + +/* fs_list_json — return directory entries as a JSON array of strings. + * Returns "[]" for missing or non-directory paths. Excludes "." and "..". */ +el_val_t fs_list_json(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return EL_STR("[]"); + DIR* d = opendir(path); + if (!d) return EL_STR("[]"); + /* Collect entries first so we can build the JSON in one pass. */ + char** names = NULL; + size_t count = 0, cap = 0; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + if (count >= cap) { + cap = cap ? cap * 2 : 16; + names = realloc(names, cap * sizeof(char*)); + if (!names) { closedir(d); return EL_STR("[]"); } + } + names[count++] = strdup(e->d_name); + } + closedir(d); + /* Build JSON array. */ + size_t sz = 3; /* "[]" + NUL */ + for (size_t i = 0; i < count; i++) sz += strlen(names[i]) * 2 + 6; /* conservative */ + char* buf = malloc(sz); + if (!buf) { for (size_t i = 0; i < count; i++) free(names[i]); free(names); return EL_STR("[]"); } + size_t pos = 0; + buf[pos++] = '['; + for (size_t i = 0; i < count; i++) { + if (i > 0) buf[pos++] = ','; + buf[pos++] = '"'; + for (const char* p = names[i]; *p; p++) { + if (*p == '"' || *p == '\\') buf[pos++] = '\\'; + else if (*p == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; continue; } + else if (*p == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; continue; } + buf[pos++] = *p; + } + buf[pos++] = '"'; + free(names[i]); + } + free(names); + buf[pos++] = ']'; + buf[pos] = '\0'; + return el_wrap_str(buf); +} + +/* fs_exists — true iff stat(path) succeeds. Symlinks are followed. */ +el_val_t fs_exists(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* ── html_raw ──────────────────────────────────────────────────────────────── + * Identity passthrough for raw HTML template interpolation. + * El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is + * without any escaping. The caller is responsible for safety. + */ +el_val_t html_raw(el_val_t s) { + return s; +} + +/* ── html_escape ───────────────────────────────────────────────────────────── + * Escape < > " ' & for safe HTML text interpolation. + * El's {expr} in HTML templates compiles to html_escape(expr). + */ +el_val_t html_escape(el_val_t sv) { + const char* src = EL_CSTR(sv); + if (!src) return EL_STR(""); + size_t len = strlen(src); + /* Worst case: every byte → 6 chars (") */ + char* out = (char*)malloc(len * 6 + 1); + if (!out) return sv; + el_arena_track(out); + char* p = out; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + switch (c) { + case '&': memcpy(p, "&", 5); p += 5; break; + case '<': memcpy(p, "<", 4); p += 4; break; + case '>': memcpy(p, ">", 4); p += 4; break; + case '"': memcpy(p, """, 6); p += 6; break; + case '\'': memcpy(p, "'", 5); p += 5; break; + default: *p++ = (char)c; break; + } + } + *p = '\0'; + return el_wrap_str(out); +} + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input, allowlist_json) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); break; + case '\n': jb_puts(b, "\\n"); break; + case '\r': jb_puts(b, "\\r"); break; + case '\t': jb_puts(b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; + snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(b, tmp); + } else { + jb_putc(b, (char)c); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + /* raw_val is the JSON value as-is (already encoded by the caller). + * If it looks like a plain (non-JSON) string, wrap it as a JSON string. + * Convention: callers pass pre-encoded values like "\"bob\"" for strings, + * "42" for numbers, "true"/"false" for booleans. */ + const char* raw_val = EL_CSTR(value); + if (!k) k = ""; + if (!raw_val) raw_val = "null"; + if (!json || !*json) { + /* Build a fresh object */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(parsed); +} + +/* json_escape_string — escape a string value for embedding in JSON. + * Returns the escaped content WITHOUT surrounding quotes. + * "say \"hello\"" -> "say \\\"hello\\\"" */ +el_val_t json_escape_string(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + /* Worst case: every char needs a 2-char escape. */ + char* out = malloc(n * 2 + 1); + if (!out) return el_wrap_str(el_strdup("")); + size_t j = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '"') { out[j++] = '\\'; out[j++] = '"'; } + else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; } + else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; } + else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; } + else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; } + else { out[j++] = (char)c; } + } + out[j] = '\0'; + el_val_t result = el_wrap_str(el_strdup(out)); + free(out); + return result; +} + +/* json_build_object — build a JSON object from a flat key-value list. + * kvs is [key0, val0, key1, val1, ...]. Values are raw JSON (pass + * strings as "\"value\"" or use json_escape_string). */ +el_val_t json_build_object(el_val_t kvs) { + el_val_t list = kvs; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + int first = 1; + for (int64_t i = 0; i + 1 < n; i += 2) { + el_val_t k = el_list_get(list, (el_val_t)i); + el_val_t v = el_list_get(list, (el_val_t)(i + 1)); + const char* ks = EL_CSTR(k); + const char* vs = EL_CSTR(v); + if (!ks || !vs) continue; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '"'); + jb_puts(&b, ks); + jb_puts(&b, "\":\""); + /* escape the value string */ + size_t vn = strlen(vs); + for (size_t j = 0; j < vn; j++) { + unsigned char c = (unsigned char)vs[j]; + if (c == '"') { jb_putc(&b, '\\'); jb_putc(&b, '"'); } + else if (c == '\\') { jb_putc(&b, '\\'); jb_putc(&b, '\\'); } + else if (c == '\n') { jb_putc(&b, '\\'); jb_putc(&b, 'n'); } + else if (c == '\r') { jb_putc(&b, '\\'); jb_putc(&b, 'r'); } + else if (c == '\t') { jb_putc(&b, '\\'); jb_putc(&b, 't'); } + else { jb_putc(&b, (char)c); } + } + jb_putc(&b, '"'); + } + jb_putc(&b, '}'); + return el_wrap_str(b.buf); +} + +/* json_build_array — build a JSON array from a list of raw JSON values. + * items is ["\"alpha\"", "\"beta\"", "42", "true", ...]. */ +el_val_t json_build_array(el_val_t items) { + el_val_t list = items; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < n; i++) { + el_val_t v = el_list_get(list, (el_val_t)i); + const char* vs = EL_CSTR(v); + if (!vs) continue; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, vs); + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + if (!fmt_str || *fmt_str == '\0' || strcmp(fmt_str, "ISO") == 0) { + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* Return a JSON string so callers can use json_get to extract fields. */ + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"year\":%d,\"month\":%d,\"day\":%d,\"hour\":%d,\"minute\":%d,\"second\":%d,\"ms\":%d}", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +el_val_t now(void) { + return el_now_instant(); +} + +/* now_ns — return current Unix time as nanoseconds (Int). + * Thin wrapper over el_now_instant for use in test timing. */ +el_val_t now_ns(void) { + return el_now_instant(); +} + +/* unix_seconds(n) — Instant from a Unix-epoch second count. + * unix_millis(n) — Instant from a Unix-epoch millisecond count. */ +el_val_t unix_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + if (i > 0) jb_putc(&b, ','); + jb_putc(&b, '"'); + jb_emit_escaped(&b, _state_entries[i].key); + jb_putc(&b, '"'); + } + jb_putc(&b, ']'); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(b.buf); +} + +/* Returns 1 (true) if the key is present in the state store, else 0 (false). */ +el_val_t state_has(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + int found = (e != NULL) ? 1 : 0; + pthread_mutex_unlock(&_state_mu); + return (el_val_t)found; +} + +/* Returns the value for key, or default_val if the key is absent. */ +el_val_t state_get_or(el_val_t key, el_val_t default_val) { + const char* k = EL_CSTR(key); + if (!k) return default_val; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + char* copy = el_strdup(e->value); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(copy); + } + pthread_mutex_unlock(&_state_mu); + return default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + double v = el_to_float(f); + /* Normalize NaN to "nan" regardless of sign — platform-independent. */ + if (isnan(v)) { + snprintf(buf, sizeof(buf), "nan"); + } else { + snprintf(buf, sizeof(buf), "%g", v); + } + return el_wrap_str(el_strdup(buf)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code) { + exit((int)code); + return 0; /* unreachable */ +} + +/* getpid_now — current process id. Named with the _now suffix to avoid + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +el_val_t getpid_now(void) { + return (el_val_t)getpid(); +} + +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + +/* ── args() — command-line argument access ────────────────────────────────── + * Compiled El programs call args() to get a list of CLI arguments. + * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* Use plain strdup (not el_strdup) so arena doesn't track these pointers. + * The BFS loop manually frees them below — arena would double-free them. */ + frontier[fc] = strdup(sid); frontier_h[fc] = 0; fc++; + visited[vc++] = strdup(sid); + + int first = 1; + while (fc > 0) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + if (vc < 1024) visited[vc++] = strdup(peer); + if (fc < 1024 && h + 1 < depth) { frontier[fc] = strdup(peer); frontier_h[fc] = h + 1; fc++; } + } + free(cur); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(buf)); +} + +#ifdef HAVE_CURL +/* ── DHARMA network ───────────────────────────────────────────────────────── + * Real implementation. Peers are addressed by `dharma_id` — either bare + * (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * and returns a JSON-string el_val_t result. Iteration is capped at 10. + */ + +#ifdef HAVE_CURL +static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5"; +static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages"; +static const char* LLM_VERSION = "2023-06-01"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5"))); + return lst; +} +#endif /* HAVE_CURL */ + +/* ── Native VM builtin aliases ────────────────────────────────────────────── + * El source files use native_* names (El VM builtins). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + return trace_span_end(h); +} + +#endif /* HAVE_CURL — OTLP */ + +/* ── Threading seed primitives ─────────────────────────────────────────────── + * __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid + * __thread_join(tid) -> String join thread, return result string + * __mutex_new() -> Int allocate a mutex, return handle + * __mutex_lock(m) lock mutex m + * __mutex_unlock(m) unlock mutex m + * + * Every El fn compiles to a global C symbol. __thread_create uses dlsym to + * look up the function by name and run it in a pthread. This means any El fn + * with signature (String) -> String is directly threadable. + */ + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + ElFn1 fn; + el_val_t arg; + el_val_t result; +} ElThreadArg; + +#define EL_THREAD_MAX 256 + +typedef struct { + pthread_t tid; + ElThreadArg* arg; + int alive; +} ElThread; + +static ElThread _threads[EL_THREAD_MAX]; +static int _thread_count = 0; +static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +static void* el_thread_runner(void* raw) { + ElThreadArg* a = (ElThreadArg*)raw; + a->result = a->fn(a->arg); + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) { + const char* sym = EL_CSTR(fn_name_v); + if (!sym || !*sym) return EL_INT(-1); + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym); + return EL_INT(-1); + } + ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg)); + if (!a) return EL_INT(-1); + a->fn = (ElFn1)p; + a->arg = arg_v; + a->result = EL_STR(""); + + pthread_mutex_lock(&_thread_alloc_mu); + if (_thread_count >= EL_THREAD_MAX) { + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + fprintf(stderr, "[__thread_create] thread table full\n"); + return EL_INT(-1); + } + int slot = _thread_count++; + _threads[slot].arg = a; + _threads[slot].alive = 1; + pthread_mutex_unlock(&_thread_alloc_mu); + + if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) { + pthread_mutex_lock(&_thread_alloc_mu); + _thread_count--; + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + return EL_INT(-1); + } + return EL_INT(slot); +} + +el_val_t __thread_join(el_val_t tid_v) { + int slot = (int)(int64_t)tid_v; + if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR(""); + pthread_join(_threads[slot].tid, NULL); + el_val_t result = _threads[slot].arg->result; + free(_threads[slot].arg); + _threads[slot].alive = 0; + return result; +} + +/* Mutex table */ + +#define EL_MUTEX_MAX 64 + +typedef struct { + pthread_mutex_t mu; + int allocated; +} ElMutexEntry; + +static ElMutexEntry _mutexes[EL_MUTEX_MAX]; +static int _mutex_count = 0; +static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_mutex_alloc_mu); + if (_mutex_count >= EL_MUTEX_MAX) { + pthread_mutex_unlock(&_mutex_alloc_mu); + fprintf(stderr, "[__mutex_new] mutex table full\n"); + return EL_INT(-1); + } + int slot = _mutex_count++; + pthread_mutex_init(&_mutexes[slot].mu, NULL); + _mutexes[slot].allocated = 1; + pthread_mutex_unlock(&_mutex_alloc_mu); + return EL_INT(slot); +} + +void __mutex_lock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_lock(&_mutexes[slot].mu); +} + +void __mutex_unlock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_unlock(&_mutexes[slot].mu); +} + +/* ── Channels ─────────────────────────────────────────────────────────────── * + * Buffered MPMC channel backed by a mutex + condvar + circular buffer. + * channel_new(capacity) -> Int (handle) + * channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded) + * channel_recv(ch) -> String — blocks until a message is available + * channel_try_recv(ch) -> String — non-blocking, returns "" if empty + * channel_close(ch) — signal no more sends; recv drains remaining + * + * Bounded channels (cap > 0): circular buffer, sender blocks when full. + * Unbounded channels (cap == 0): dynamic array, sender never blocks. + */ +#define EL_CHANNEL_MAX 64 +#define EL_CHANNEL_BUF 1024 + +typedef struct { + char** buf; + int cap; /* 0 = unbounded (grows dynamically) */ + int head, tail, count; + int dyn_cap; /* allocated slots for unbounded mode */ + int closed; + pthread_mutex_t mu; + pthread_cond_t not_empty; + pthread_cond_t not_full; +} ElChannel; + +static ElChannel _channels[EL_CHANNEL_MAX]; +static int _channel_count = 0; +static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __channel_new(el_val_t capacity_v) { + int cap = (int)(int64_t)capacity_v; + if (cap < 0) cap = 0; + + pthread_mutex_lock(&_channel_alloc_mu); + if (_channel_count >= EL_CHANNEL_MAX) { + pthread_mutex_unlock(&_channel_alloc_mu); + fprintf(stderr, "[__channel_new] channel table full\n"); + return EL_INT(-1); + } + int slot = _channel_count++; + pthread_mutex_unlock(&_channel_alloc_mu); + + ElChannel* ch = &_channels[slot]; + memset(ch, 0, sizeof(*ch)); + ch->cap = cap; + ch->closed = 0; + ch->head = 0; + ch->tail = 0; + ch->count = 0; + + if (cap > 0) { + /* Bounded: fixed circular buffer. */ + ch->buf = (char**)malloc((size_t)cap * sizeof(char*)); + ch->dyn_cap = cap; + } else { + /* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */ + ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*)); + ch->dyn_cap = EL_CHANNEL_BUF; + } + if (!ch->buf) { + fprintf(stderr, "[__channel_new] out of memory\n"); + return EL_INT(-1); + } + + pthread_mutex_init(&ch->mu, NULL); + pthread_cond_init(&ch->not_empty, NULL); + pthread_cond_init(&ch->not_full, NULL); + + return EL_INT(slot); +} + +void __channel_send(el_val_t ch_v, el_val_t msg_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + const char* msg = EL_CSTR(msg_v); + if (!msg) msg = ""; + char* copy = strdup(msg); /* channel owns the string */ + + pthread_mutex_lock(&ch->mu); + + if (ch->closed) { + /* Send on closed channel is a no-op (drop the message). */ + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + + if (ch->cap > 0) { + /* Bounded: block while full. */ + while (ch->count >= ch->cap && !ch->closed) { + pthread_cond_wait(&ch->not_full, &ch->mu); + } + if (ch->closed) { + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->cap; + ch->count++; + } else { + /* Unbounded: grow the buffer if needed. */ + if (ch->count >= ch->dyn_cap) { + int new_cap = ch->dyn_cap * 2; + char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*)); + if (!grown) { + pthread_mutex_unlock(&ch->mu); + free(copy); + fprintf(stderr, "[__channel_send] out of memory growing channel\n"); + return; + } + /* The circular buffer may have wrapped. Linearise it first. + * In unbounded mode head is always 0 (we append at tail, drain + * from head), so a simple memmove isn't needed — but if the + * buffer did wrap (tail < head after growth), we need to fix up. + * Simplest safe path: if tail wrapped, move the head..old_cap + * segment to new_cap..new_cap+(old_cap-head). */ + if (ch->tail < ch->head) { + /* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */ + int front = ch->dyn_cap - ch->head; + memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*)); + ch->head = ch->dyn_cap; + } + ch->buf = grown; + ch->dyn_cap = new_cap; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->dyn_cap; + ch->count++; + } + + pthread_cond_signal(&ch->not_empty); + pthread_mutex_unlock(&ch->mu); +} + +el_val_t __channel_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + /* Block until there is a message or the channel is closed and drained. */ + while (ch->count == 0 && !ch->closed) { + pthread_cond_wait(&ch->not_empty, &ch->mu); + } + + if (ch->count == 0) { + /* Closed and empty — signal EOF. */ + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + /* Hand the string to the arena so it is freed after the request. */ + el_arena_track(msg); + return EL_STR(msg); +} + +el_val_t __channel_try_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + if (ch->count == 0) { + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + el_arena_track(msg); + return EL_STR(msg); +} + +void __channel_close(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + ch->closed = 1; + /* Wake all blocked recvers and senders so they can observe the close. */ + pthread_cond_broadcast(&ch->not_empty); + pthread_cond_broadcast(&ch->not_full); + pthread_mutex_unlock(&ch->mu); +} + +/* ── DHARMA runtime additions ──────────────────────────────────────────────── + * + * Functions required by the dharma registry service. Added here so the + * released el_runtime.c includes them without requiring dharma to bundle + * its own stubs. + * + * Functions added: + * list_len — alias for el_list_len (used in handlers.el) + * list_get — alias for el_list_get (used in handlers.el) + * json_array_push — append a pre-encoded JSON element to a JSON array string + * now_millis — milliseconds since Unix epoch (alias for time_now) + * unix_timestamp_ms — same as now_millis (alias) + * time_now_ms — same as now_millis (alias) + * log_info — stderr structured log at INFO level + * log_warn — stderr structured log at WARN level + * config — reads a config value from the environment + * http_patch — HTTP PATCH with JSON Content-Type + * http_post_engram — HTTP POST with optional X-API-Key header + * http_get_engram — HTTP GET with optional X-API-Key header + * str_to_bytes — encode a string as a JSON array of byte values + * bytes_to_str — decode a JSON array of byte values back to a string + * hash_sha256 — SHA-256 hex digest of a string + */ + +/* list_len — return the number of elements in a list. */ +el_val_t list_len(el_val_t list) { + return el_list_len(list); +} + +/* list_get — return the element at index i in a list. */ +el_val_t list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\"" + * or "42") to the JSON array string arr. Returns a new JSON array string. + * Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" + * json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */ +el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) { + const char* arr = EL_CSTR(arr_v); + const char* elem = EL_CSTR(elem_v); + if (!arr || !*arr) arr = "[]"; + if (!elem || !*elem) elem = "null"; + + /* Trim whitespace, find the closing ']'. */ + const char* p = arr; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { + /* Not an array — return a single-element array. */ + size_t n = strlen(elem) + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + size_t arr_len = strlen(arr); + size_t elem_len = strlen(elem); + + /* Walk from the end to find the matching ']'. */ + const char* end = arr + arr_len - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; + if (*end != ']') { + /* Malformed — wrap elem in a new array. */ + size_t n = elem_len + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + + /* Content between '[' and ']'. */ + const char* inner_start = p + 1; + const char* inner_end = end; /* points AT ']' */ + /* Check if the array is empty (only whitespace between brackets). */ + const char* q = inner_start; + while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++; + int empty = (q == inner_end); + + /* Build: prefix + (comma if non-empty) + elem + "]" */ + size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */ + size_t sep_len = empty ? 0 : 1; /* "," if non-empty */ + size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */ + char* out = el_strbuf(out_len); + memcpy(out, arr, prefix_len); + if (!empty) out[prefix_len] = ','; + memcpy(out + prefix_len + sep_len, elem, elem_len); + out[prefix_len + sep_len + elem_len] = ']'; + out[prefix_len + sep_len + elem_len + 1] = '\0'; + return el_wrap_str(out); +} + +/* now_millis — milliseconds since Unix epoch. */ +el_val_t now_millis(void) { + return time_now(); +} + +/* unix_timestamp_ms — same as now_millis. */ +el_val_t unix_timestamp_ms(void) { + return time_now(); +} + +/* time_now_ms — same as now_millis. */ +el_val_t time_now_ms(void) { + return time_now(); +} + +/* log_info — write a structured [INFO] line to stderr. */ +void log_info(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[INFO] %s\n", msg ? msg : ""); +} + +/* log_warn — write a structured [WARN] line to stderr. */ +void log_warn(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[WARN] %s\n", msg ? msg : ""); +} + +/* config — read a configuration value from the environment. + * Returns "" if the variable is not set (same as __env_get). */ +el_val_t config(el_val_t key_v) { + const char* key = EL_CSTR(key_v); + if (!key || !*key) return EL_STR(""); + const char* val = getenv(key); + if (!val) return EL_STR(""); + return el_wrap_str(el_strdup(val)); +} + +#ifdef HAVE_CURL +/* http_patch — HTTP PATCH request with Content-Type: application/json. + * Returns the response body (same error convention as http_post_json). */ +el_val_t http_patch(el_val_t url_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_post_engram — HTTP POST with optional X-API-Key header. + * If key is "" no authentication header is sent. */ +el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_get_engram — HTTP GET with optional X-API-Key header. */ +el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + if (h) curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} +#endif /* HAVE_CURL */ + +/* str_to_bytes — encode a string as a JSON array of unsigned byte values. + * "hello" -> "[104,101,108,108,111]" + * Used by db.el to store binary content in Engram JSON nodes. */ +el_val_t str_to_bytes(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return el_wrap_str(el_strdup("[]")); + size_t n = strlen(s); + /* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */ + char* out = el_strbuf(n * 4 + 3); + size_t pos = 0; + out[pos++] = '['; + for (size_t i = 0; i < n; i++) { + unsigned char b = (unsigned char)s[i]; + if (i > 0) out[pos++] = ','; + /* Write decimal representation of b. */ + if (b >= 100) { + out[pos++] = (char)('0' + b / 100); + out[pos++] = (char)('0' + (b / 10) % 10); + out[pos++] = (char)('0' + b % 10); + } else if (b >= 10) { + out[pos++] = (char)('0' + b / 10); + out[pos++] = (char)('0' + b % 10); + } else { + out[pos++] = (char)('0' + b); + } + } + out[pos++] = ']'; + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* bytes_to_str — decode a JSON array of integer byte values back to a string. + * "[104,101,108,108,111]" -> "hello" + * Inverse of str_to_bytes. */ +el_val_t bytes_to_str(el_val_t arr_v) { + const char* s = EL_CSTR(arr_v); + if (!s) return el_wrap_str(el_strdup("")); + /* Skip whitespace, expect '['. */ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + + /* Count elements to size the output buffer. */ + int64_t n = (int64_t)json_array_len(arr_v); + if (n <= 0) return el_wrap_str(el_strdup("")); + + char* out = el_strbuf((size_t)n + 1); + size_t pos = 0; + + /* Walk the array, parse each integer, store as a byte. */ + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']' || *s == '\0') break; + /* Parse decimal integer. */ + char* end_ptr; + long v = strtol(s, &end_ptr, 10); + if (end_ptr == s) break; /* parse failure */ + s = end_ptr; + if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* hash_sha256 — return the SHA-256 hex digest of a string. + * Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */ +el_val_t hash_sha256(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)s, strlen(s), digest); + return el_hex_encode(digest, 32); +} + +/* ── __ prefixed aliases — public boundary for compiled El programs ────────── + * + * The El compiler's self-hosting back-end emits calls to __-prefixed function + * names (e.g. __println, __str_len). These wrappers forward to the existing + * el_runtime implementations so both naming conventions resolve at link time. + * + * Note: __thread_create and __thread_join are already defined above in the + * threading section; they are not repeated here. + * ──────────────────────────────────────────────────────────────────────────── */ + +/* I/O */ +el_val_t __println(el_val_t s) { return println(s); } +el_val_t __print(el_val_t s) { return print(s); } +el_val_t __readline(void) { return readline(); } + +/* String */ +el_val_t __int_to_str(el_val_t n) { return int_to_str(n); } +el_val_t __str_to_int(el_val_t s) { return str_to_int(s); } +el_val_t __float_to_str(el_val_t f) { return float_to_str(f); } +el_val_t __str_to_float(el_val_t s) { return str_to_float(s); } +el_val_t __str_len(el_val_t s) { return str_len(s); } +el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); } + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strcmp(ca, cb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strncmp(ca, cb, (size_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); } +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); } + +el_val_t __str_alloc(el_val_t n) { + if (n <= 0) n = 0; + char* buf = el_strbuf((size_t)n + 1); + memset(buf, 0, (size_t)n + 1); + return el_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* buf = (char*)(uintptr_t)s; + if (buf) buf[(size_t)i] = (char)c; + return s; +} + +/* URL encoding */ +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* Environment */ +el_val_t __env_get(el_val_t key) { return env(key); } + +/* Subprocess */ +el_val_t __exec(el_val_t cmd) { return exec(cmd); } +el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); } + +/* Process */ +el_val_t __exit_program(el_val_t code) { return exit_program(code); } + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path) { return fs_exists(path); } +el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); } +el_val_t __fs_read(el_val_t path) { return fs_read(path); } +el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); } +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); } +el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); } + +/* HTTP server (no curl dependency) */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); } +el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); } +el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); } + +/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here + * so el_runtime.c compiles standalone. When both translation units are linked + * the el_seed.c definitions win via their non-static linkage (strong symbols). + * These stubs are marked weak so they are silently overridden. */ +__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); } +__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; } +__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; } +__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; } + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); } +el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); } +el_val_t __json_array_len(el_val_t json) { return json_array_len(json); } +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); } +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); } + +/* State K/V */ +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_keys(void) { return state_keys(); } +el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); } + +/* UUID */ +el_val_t __uuid_v4(void) { return uuid_v4(); } + +/* Args */ +el_val_t __args_json(void) { return args(); } + +/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below + * with a matching stub in the #ifndef HAVE_CURL block. */ +#ifdef HAVE_CURL +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms) { + /* timeout_ms is accepted for API compatibility but ignored here; + * el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */ + (void)timeout_ms; + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map — same as __http_do but headers_map arg is a JSON-string + * rather than an ElMap. Parse it first, then delegate. */ +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + (void)timeout_ms; + /* Build a curl_slist from a JSON object {"Header":"value",...}. */ + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + /* Walk the JSON pairs with a simple parser reusing json_get_string logic. */ + /* For correctness we just call the existing json_get iteration path. + * We duplicate the key-extraction loop from headers_from_map but driven + * by JSON rather than ElMap. Use json_get_raw to iterate is not easy + * without knowing keys, so accept the JSON string and build a tmp map. */ + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map_to_file — same as __http_do_map but streams response body + * to a local file path rather than returning it as a string. */ +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path) { + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), + h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +#ifndef HAVE_CURL +/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── * + * These return a JSON error string so El programs get a clear message if they + * call HTTP/LLM functions in a curl-less build. */ +static el_val_t _no_curl_err(void) { + return el_wrap_str(el_strdup("{\"error\":\"not built with HAVE_CURL\"}")); +} +el_val_t http_get(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); } +el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t p) { (void)url; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t p) { (void)url; (void)b; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_engram(el_val_t url, el_val_t k, el_val_t b) { (void)url; (void)k; (void)b; return _no_curl_err(); } +el_val_t http_get_engram(el_val_t url, el_val_t k) { (void)url; (void)k; return _no_curl_err(); } +el_val_t llm_call(el_val_t m, el_val_t p) { (void)m; (void)p; return _no_curl_err(); } +el_val_t llm_call_system(el_val_t m, el_val_t s, el_val_t u) { (void)m; (void)s; (void)u; return _no_curl_err(); } +el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (void)m; (void)s; (void)u; (void)t; return _no_curl_err(); } +el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); } +el_val_t llm_models(void) { return el_list_empty(); } +void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; } +/* __ HTTP stubs (no-curl build) */ +el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); } +#endif /* !HAVE_CURL */ diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * EL_STR(s) cast string literal to el_val_t + * EL_CSTR(v) cast el_val_t back to const char* + * EL_INT(v) identity — el_val_t is already int64_t + * EL_NULL null / zero value + * EL_FALSE boolean false (0) + * EL_TRUE boolean true (1) + * + * Link requirements: + * -lcurl — required for the HTTP client (http_get, http_post, llm_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#define EL_INT(v) (v) +#define EL_NULL ((el_val_t)0) +#define EL_FALSE ((el_val_t)0) +#define EL_TRUE ((el_val_t)1) + +/* Float values share the el_val_t (int64) slot via a bit-cast. + * The codegen emits Float literals as `el_from_float()` so the + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +el_val_t str_len(el_val_t s); +el_val_t str_concat(el_val_t a, el_val_t b); +el_val_t int_to_str(el_val_t n); +el_val_t str_to_int(el_val_t s); +el_val_t native_str_to_int(el_val_t s); +el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end); +el_val_t str_contains(el_val_t s, el_val_t sub); +el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +void el_retain(el_val_t v); +void el_release(el_val_t v); + +/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */ +el_val_t el_arena_push(void); +el_val_t el_arena_pop(el_val_t mark); + +/* ── List ────────────────────────────────────────────────────────────────── */ + +el_val_t el_list_new(el_val_t count, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * auto-content-type contract for legacy handlers that return plain bodies. */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* SSE connection fd — set by http_worker_v2 before calling the El handler, + * cleared afterwards. Defined in el_seed.c; called from el_runtime.c. + * The getter is exposed as __http_conn_fd() to El programs. */ +void el_seed_set_http_conn_fd(int fd); + +/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default + * 60000ms). Read lazily on first use, so setting the env var any time before + * the first http_* call is sufficient. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"p":[],"a":["href","title"],"strong":[],...} + * where each value is the array of attribute names allowed for that tag. */ +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t html_raw(el_val_t s); +el_val_t html_escape(el_val_t s); + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t path); +el_val_t fs_write(el_val_t path, el_val_t content); +el_val_t fs_list(el_val_t path); +el_val_t fs_list_json(el_val_t path); +el_val_t fs_exists(el_val_t path); +el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_escape_string(el_val_t sv); +el_val_t json_build_object(el_val_t kvs); +el_val_t json_build_array(el_val_t items); + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +el_val_t time_to_parts(el_val_t ts); +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz); +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit); +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit); +el_val_t now_ns(void); + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * Both types share the el_val_t (int64) slot. Instants are nanoseconds + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_has(el_val_t key); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code); +el_val_t getpid_now(void); + +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + +/* ── CGI identity ───────────────────────────────────────────────────────────── + * Called at the start of main() in CGI programs (those with a `cgi {}` block). + * Records the program's DHARMA identity before any other code executes. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */ +el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */ +el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */ + +/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */ +el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */ +el_val_t stdout_restore(void); /* restore process stdout to terminal */ + +el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json); +el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json); +el_val_t trace_span_start(el_val_t name); +el_val_t trace_span_end(el_val_t span_handle); +el_val_t emit_event(el_val_t name, el_val_t duration_ms); + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v); +el_val_t __thread_join(el_val_t tid_v); + +/* ── __ prefixed aliases (self-hosting compiler ABI) ───────────────────────── + * The El self-hosting compiler emits calls to __-prefixed names. These are + * forwarding wrappers around the existing el_runtime functions above. */ + +/* I/O */ +el_val_t __println(el_val_t s); +el_val_t __print(el_val_t s); +el_val_t __readline(void); + +/* String */ +el_val_t __int_to_str(el_val_t n); +el_val_t __str_to_int(el_val_t s); +el_val_t __float_to_str(el_val_t f); +el_val_t __str_to_float(el_val_t s); +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); +el_val_t __str_cmp(el_val_t a, el_val_t b); +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); +el_val_t __str_concat_raw(el_val_t a, el_val_t b); +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); +el_val_t __str_alloc(el_val_t n); +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); + +/* URL encoding */ +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* Environment */ +el_val_t __env_get(el_val_t key); + +/* Subprocess */ +el_val_t __exec(el_val_t cmd); +el_val_t __exec_bg(el_val_t cmd); + +/* Process */ +el_val_t __exit_program(el_val_t code); + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path); +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_read(el_val_t path); +el_val_t __fs_write(el_val_t path, el_val_t content); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); +el_val_t __fs_list_raw(el_val_t path); + +/* HTTP server */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); +el_val_t __http_serve(el_val_t port, el_val_t handler); +el_val_t __http_serve_v2(el_val_t port, el_val_t handler); + +/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */ +el_val_t __http_conn_fd(void); +el_val_t __http_sse_open(el_val_t conn_id); +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); +el_val_t __http_sse_close(el_val_t conn_id); + +/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms); +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path); + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index); +el_val_t __json_array_get_string(el_val_t json, el_val_t index); +el_val_t __json_array_len(el_val_t json); +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json, el_val_t key); +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value); +el_val_t __json_parse_map(el_val_t json_str); +el_val_t __json_stringify_val(el_val_t val); + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s); + +/* State K/V */ +el_val_t __state_del(el_val_t key); +el_val_t __state_get(el_val_t key); +el_val_t __state_keys(void); +el_val_t __state_set(el_val_t key, el_val_t val); + +/* UUID */ +el_val_t __uuid_v4(void); + +/* Args */ +el_val_t __args_json(void); + +#ifdef __cplusplus +} +#endif diff --git a/ui/examples/native-hello-android/app/src/main/jni/el_seed.c b/ui/examples/native-hello-android/app/src/main/jni/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + return http_response(status, headers_json, body); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello-android/app/src/main/jni/native_hello.c b/ui/examples/native-hello-android/app/src/main/jni/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/jni/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml b/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c2ac18f --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui/examples/native-hello-android/app/src/main/res/values/strings.xml b/ui/examples/native-hello-android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b0cc308 --- /dev/null +++ b/ui/examples/native-hello-android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + el-native + diff --git a/ui/examples/native-hello-android/build.gradle b/ui/examples/native-hello-android/build.gradle new file mode 100644 index 0000000..3143e8a --- /dev/null +++ b/ui/examples/native-hello-android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file — configuration for all sub-projects/modules. +plugins { + id 'com.android.application' version '8.2.2' apply false +} diff --git a/ui/examples/native-hello-android/build.sh b/ui/examples/native-hello-android/build.sh new file mode 100755 index 0000000..4428bbd --- /dev/null +++ b/ui/examples/native-hello-android/build.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# build.sh — Build native-hello-android. +# +# Usage: +# ./build.sh # regenerate C sources + ./gradlew assembleDebug +# ./build.sh gen # regenerate C sources only +# ./build.sh gradle # run ./gradlew assembleDebug (skip elc step) +# ./build.sh clean # clean Gradle build outputs +# +# Requirements: +# - Android SDK with NDK and CMake installed +# - local.properties present (copy from local.properties.template and edit sdk.dir) +# - Java 11+ on PATH (for Gradle) +# - elc at ../../../lang/dist/platform/elc + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el" +EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el" +JNI_DIR="${SCRIPT_DIR}/app/src/main/jni" + +# Locate elc. +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +gen() { + echo "==> Generating native_hello.c..." + EL_MANIFEST="${EL_MANIFEST}" \ + "${ELC}" "${EL_APP_ENTRY}" \ + > "${JNI_DIR}/native_hello.c" + + echo "==> Generating el_native_vessel.c..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${JNI_DIR}/el_native_vessel.c" + # Rename the vessel's main() to avoid symbol collision with native_hello.c's main(). + # The Android JNI entry point (nativeMain) calls native_hello.c's main() explicitly. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${JNI_DIR}/el_native_vessel.c" 2>/dev/null || \ + sed -i 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${JNI_DIR}/el_native_vessel.c" + + echo "==> C source generation complete." +} + +gradle_build() { + echo "==> Running ./gradlew assembleDebug..." + cd "${SCRIPT_DIR}" + ./gradlew assembleDebug + + APK_PATH="${SCRIPT_DIR}/app/build/outputs/apk/debug/app-debug.apk" + if [ -f "${APK_PATH}" ]; then + echo "==> Build complete." + echo " APK: ${APK_PATH}" + else + echo "==> Build complete (APK path may differ — check app/build/outputs/apk/)." + fi +} + +clean() { + echo "==> Cleaning..." + cd "${SCRIPT_DIR}" + ./gradlew clean + echo "==> Clean complete." +} + +case "${1:-all}" in + gen) gen ;; + gradle) gradle_build ;; + clean) clean ;; + all|*) gen && gradle_build ;; +esac diff --git a/ui/examples/native-hello-android/gradle.properties b/ui/examples/native-hello-android/gradle.properties new file mode 100644 index 0000000..150f321 --- /dev/null +++ b/ui/examples/native-hello-android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=false diff --git a/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar b/ui/examples/native-hello-android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..033e24c4cdf41af1ab109bc7f253b2b887023340 GIT binary patch literal 63375 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≫L+crb^Bz zc+Nw%zgpZ6>!A3%lau!Pw6`Y#WPVBtAfKSsqwYDWQK-~ zz(mx=nJ6-8t`YXB{6gaZ%G}Dmn&o500Y}2Rd?e&@=hBEmB1C=$OMBfxX__2c2O4K2#(0ksclP$SHp*8jq-1&(<6(#=6&H`Nlc2RVC4->r6U}sTY<1? zn@tv7XwUs-c>Lcmrm5AE0jHI5={WgHIow6cX=UK)>602(=arbuAPZ37;{HTJSIO%9EL`Et5%J7$u_NaC(55x zH^qX^H}*RPDx)^c46x>js=%&?y?=iFs^#_rUl@*MgLD92E5y4B7#EDe9yyn*f-|pQ zi>(!bIg6zY5fLSn@;$*sN|D2A{}we*7+2(4&EhUV%Qqo5=uuN^xt_hll7=`*mJq6s zCWUB|s$)AuS&=)T&_$w>QXHqCWB&ndQ$y4-9fezybZb0bYD^zeuZ>WZF{rc>c4s`` zgKdppTB|o>L1I1hAbnW%H%EkFt%yWC|0~+o7mIyFCTyb?@*Ho)eu(x`PuO8pLikN> z6YeI`V?AUWD(~3=8>}a6nZTu~#QCK(H0+4!ql3yS`>JX;j4+YkeG$ZTm33~PLa3L} zksw7@%e-mBM*cGfz$tS4LC^SYVdBLsR}nAprwg8h2~+Cv*W0%izK+WPVK}^SsL5R_ zpA}~G?VNhJhqx2he2;2$>7>DUB$wN9_-adL@TqVLe=*F8Vsw-yho@#mTD6*2WAr6B zjtLUh`E(;#p0-&$FVw(r$hn+5^Z~9J0}k;j$jL1;?2GN9s?}LASm?*Rvo@?E+(}F& z+=&M-n`5EIz%%F^e)nnWjkQUdG|W^~O|YeY4Fz}>qH2juEere}vN$oJN~9_Th^&b{ z%IBbET*E8%C@jLTxV~h#mxoRrJCF{!CJOghjuKOyl_!Jr?@4Upo7u>fTGtfm|CH2v z&9F+>;6aFbYXLj3{yZ~Yn1J2%!)A3~j2$`jOy{XavW@t)g}}KUVjCWG0OUc7aBc=2 zR3^u=dT47=5SmT{K1aGaVZkOx|24T-J0O$b9dfB25J|7yb6frwS6wZ1^y%EWOm}S< zc1SdYhfsdLG*FB-;!QLV3D!d~hnXTGVQVck9x%=B(Kk8c3y%f0nR95_TbY;l=obSl zEE@fp0|8Q$b3(+DXh?d0FEloGhO0#11CLQT5qtEckBLe-VN-I>9ys}PVK0r;0!jIG zH_q$;a`3Xv9P_V2ekV1SMzd#SKo<1~Dq2?M{(V;AwhH_2x@mN$=|=cG0<3o^j_0OF z7|WJ-f2G=7sA4NVGU2X5`o*D2T7(MbmZ2(oipooE{R?9!{WxX!%ofhsrPAxoIk!Kr z>I$a{Zq=%KaLrDCIL^gmA3z{2z%Wkr)b$QHcNUA^QwydWMJmxymO0QS22?mo%4(Md zgME(zE}ub--3*wGjV`3eBMCQG-@Gel1NKZDGuqobN|mAt0{@ZC9goI|BSmGBTUZ(`Xt z^e2LiMg?6E?G*yw(~K8lO(c4)RY7UWxrXzW^iCg-P41dUiE(i+gDmmAoB?XOB}+Ln z_}rApiR$sqNaT4frw69Wh4W?v(27IlK$Toy<1o)GeF+sGzYVeJ`F)3`&2WDi^_v67 zg;@ehwl3=t+}(DJtOYO!s`jHyo-}t@X|U*9^sIfaZfh;YLqEFmZ^E;$_XK}%eq;>0 zl?+}*kh)5jGA}3daJ*v1knbW0GusR1+_xD`MFPZc3qqYMXd>6*5?%O5pC7UVs!E-` zuMHc6igdeFQ`plm+3HhP)+3I&?5bt|V8;#1epCsKnz0%7m9AyBmz06r90n~9o;K30 z=fo|*`Qq%dG#23bVV9Jar*zRcV~6fat9_w;x-quAwv@BkX0{9e@y0NB(>l3#>82H6 z^US2<`=M@6zX=Pz>kb8Yt4wmeEo%TZ=?h+KP2e3U9?^Nm+OTx5+mVGDvgFee%}~~M zK+uHmj44TVs}!A}0W-A92LWE%2=wIma(>jYx;eVB*%a>^WqC7IVN9{o?iw{e4c=CG zC#i=cRJZ#v3 zF^9V+7u?W=xCY%2dvV_0dCP%5)SH*Xm|c#rXhwEl*^{Ar{NVoK*H6f5qCSy`+|85e zjGaKqB)p7zKNKI)iWe6A9qkl=rTjs@W1Crh(3G57qdT0w2ig^{*xerzm&U>YY{+fZbkQ#;^<$JniUifmAuEd^_M(&?sTrd(a*cD! zF*;`m80MrZ^> zaF{}rDhEFLeH#`~rM`o903FLO?qw#_Wyb5}13|0agjSTVkSI6Uls)xAFZifu@N~PM zQ%o?$k)jbY0u|45WTLAirUg3Zi1E&=G#LnSa89F3t3>R?RPcmkF}EL-R!OF_r1ZN` z?x-uHH+4FEy>KrOD-$KHg3$-Xl{Cf0;UD4*@eb~G{CK-DXe3xpEEls?SCj^p z$Uix(-j|9f^{z0iUKXcZQen}*`Vhqq$T?^)Ab2i|joV;V-qw5reCqbh(8N)c%!aB< zVs+l#_)*qH_iSZ_32E~}>=wUO$G_~k0h@ch`a6Wa zsk;<)^y=)cPpHt@%~bwLBy;>TNrTf50BAHUOtt#9JRq1ro{w80^sm-~fT>a$QC;<| zZIN%&Uq>8`Js_E((_1sewXz3VlX|-n8XCfScO`eL|H&2|BPZhDn}UAf_6s}|!XpmUr90v|nCutzMjb9|&}#Y7fj_)$alC zM~~D6!dYxhQof{R;-Vp>XCh1AL@d-+)KOI&5uKupy8PryjMhTpCZnSIQ9^Aq+7=Mb zCYCRvm4;H=Q8nZWkiWdGspC_Wvggg|7N`iED~Eap)Th$~wsxc(>(KI>{i#-~Dd8iQ zzonqc9DW1w4a*}k`;rxykUk+~N)|*I?@0901R`xy zN{20p@Ls<%`1G1Bx87Vm6Z#CA`QR(x@t8Wc?tpaunyV^A*-9K9@P>hAWW9Ev)E$gb z<(t?Te6GcJX2&0% z403pe>e)>m-^qlJU^kYIH)AutgOnq!J>FoMXhA-aEx-((7|(*snUyxa+5$wx8FNxS zKuVAVWArlK#kDzEM zqR?&aXIdyvxq~wF?iYPho*(h?k zD(SBpRDZ}z$A})*Qh!9&pZZRyNixD!8)B5{SK$PkVET(yd<8kImQ3ILe%jhx8Ga-1 zE}^k+Eo^?c4Y-t2_qXiVwW6i9o2qosBDj%DRPNT*UXI0=D9q{jB*22t4HHcd$T&Xi zT=Vte*Gz2E^qg%b7ev04Z&(;=I4IUtVJkg<`N6i7tjUn-lPE(Y4HPyJKcSjFnEzCH zPO(w%LmJ_=D~}PyfA91H4gCaf-qur3_KK}}>#9A}c5w@N;-#cHph=x}^mQ3`oo`Y$ope#)H9(kQK zGyt<7eNPuSAs$S%O>2ElZ{qtDIHJ!_THqTwcc-xfv<@1>IJ;YTv@!g-zDKBKAH<

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

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

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

ZgAeru`Cet$9D*erd?F_?Ay?JY7FS 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 +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build/el_native_vessel.h b/ui/examples/native-hello/build/el_native_vessel.h new file mode 100644 index 0000000..b62c288 --- /dev/null +++ b/ui/examples/native-hello/build/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build/el_native_vessel.o b/ui/examples/native-hello/build/el_native_vessel.o new file mode 100644 index 0000000000000000000000000000000000000000..3a45efdeddeb987e8bcf4a637692f5e8b1a8e60c GIT binary patch literal 14692 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�GitqWD}eAMe5qv{=@ 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 Y>WscHBP#%pHf2;u?`vVHiApW@zp@^`x&QzG literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build/el_sdl2.o b/ui/examples/native-hello/build/el_sdl2.o new file mode 100644 index 0000000000000000000000000000000000000000..c485e5eb552fe1b61e30a8902ea9b4540b43cc3b GIT binary patch literal 31624 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 literal 0 HcmV?d00001 diff --git a/ui/examples/native-hello/build/el_seed.o b/ui/examples/native-hello/build/el_seed.o new file mode 100644 index 0000000000000000000000000000000000000000..7abc10279e8bcc2c092b2ed3b8a900a2223f216d GIT binary patch literal 50056 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�*zDg`>}`l{@U+=X zoxO?8B>S=p9%r8nc?s^_2>V*@quKSHw7UsDSO_0XqfO+~?0dU|2>c|UqDg4OR>)!% zUR}s#lx^PmU;hGb)YsaoSiH8(XW*?g_{{cY>>1t??iDx>09P>}kAkD%s{a~i?lvH9 zAn0pQ&)d^ow6FdtcCXp>&z~8UP6%YPiZ27WRkkL&hnP}uaIk+-1^ZCwX7(ubkeu;Qnz6Mtb(prQCP|wdlv}}fWwiFTy>eIgLYJoRTF71mPxLrGjSM_I zg;)~CJB%eU8{ygG;#a&MSd7(Ek3t(@YG0#Mx)+-L$feoXvF`i6-l3n~6-OmrIFK#T z_dZ}F3w>9gv4t^Ae5cu3eA0*GlOB!l#C+1hcoNTt-+2??cHO+0eM9_U+pzC;J)epB zHXP>zmzBD*HSrMaoCOzXU2{R{FmzDve?I(VF2ns{^WlT}jlCaGpXM-s&*C>jdgOxA zA<|VI_ZiRz{Ke15yxyNLxS)hBsI=sql*Q0JI8WabEQgtI>wAMAT~j(1i9vm3mHPm`Z#^)5e~`ZNdr;QdzDFSYIA zTP!U2c)YtQ+XGCyOMw3Sy9AOmrR9Eq$H{zkp98kJ9IEV)4LfjoDoTe50aBS)(QG=pKe`E#u}ERp+;ynRE7)mEn6(yFE>AYAg&R z-b#F3WIbZMy*cwlKfeC#nD_#C&teFv!~X238xCz=jJ`UGz4h_K!0RPp+&Lyb4H%*U zV?wMWxXtFQj`sx!f4(i1bu!-l>E^;B=_dG4-*ZW2Zy@}(kBMIcY{|6sk-Ou&d>=ne zAF<~S2ER$bu)X4gtMmAEAIQgtyFRI3hx&fS(A?`L^$UD9dwg8X+Mu+CPJQoP z@oj^Ywf6T>R&774@}g<|0ouMXiI*D$-gDQ{_OE^02QF)EKk3?LzDwHP#657{k3;<% z@Y8r6G2UIk8e##m>gM6~x~GIRlZ$FPQSN|a4zBllTn}82F=zQvhj zm~RG=`B~JJ@714UlJBHje1zV=*2~|^m=DGy%bMqmABp!KO!r$pZJEBsrcvCl+BMxT z`;7TxzWY9%${y&azRgm**a{YRj$L4L5A&j*!&-;KQQ~0kMYj-!qg;1YD3$5Zu9A5i~uKR_RhO#0O*8jl3uq3QZ%Kz;Bi{Yrcm;o1NHU0opG zi`m=%AJqX{p;fgGnC!;tfI498AUeQ*ufAFbjCu(jaCRc2H`DgZ(E;Z(hQ=RGMz!}I z@>jJExHf=)FgoD%Rq%C|%ESMAbwGoM>E!xV1Hsh2!cMQeB%Z6s#z$1a)ViUKb5GJE z75IAnf^ONid2szA*^`cWjdO&SgD2qjCQ<~!fMe6FPBYi-}d z^7C~(=emTU9@p$ z-PkYfdwBJBi*=Xo9~RxXvnRK0UwdZzJ|Dl{P5+0vIJGnl8%H#k?p42ut50mKpwG7N zqkPI{=;K2Acn$t<)+6S(mS1-i{3joDpXlf4(biF>N0GXY9_EL%=Q{9*29^5~f&aIu zGF~!AKXdeR2mMsvbM&!DAKU1oXp*CEi-^G!o%}xJZQ10`Xpi?>D)63I`l#fNyRu+&n5Gv(&SIRM*czkU-FInIP?z3KYQq}e6kvg z)f53o%H?vn2mR1@1XrX%k|Iq_e}S${+{OhL4TY4^8o#2 zji1n^s=xE2!br)VbkCe;>e`+<>ym=(Aj{`j^kB_WU@gQCf7o}{Ehf(ODAs@W9{Omm=YA*FL$;sa zGrzGg(EfRGmOFpRUcc51{r1l{v)7Ca`*=}&RiVY7SwJD!Bc=0`ph$ zEIl~X$ry7+4j;w4oDY1^iDEn_ju^8^(9SpX`Zr5wn3kWqXI|p2v9n z(DXmlajTtA^##*y6<2+`2V}`N3V$A>lv>MN2t*&UVx)bO({}pG~2f(uv zJl{3|p4-E=z+bqfZM_7ZcZYTV9z4VGpDrc;^Ub^w06xgzc%}Q!qd=q#2hvH{cF7|=E!}sw5WGtj#TIL zW1LN-G|tU8jt|}^aF5-@;JrSnvjsS{!o7tvW#Ed=)jYeWO}+}&u3O|&z>Wq!=hfu5 zaPj=C?c2%o{vO4t{aN!uKRBwxS(VzxKJMjx(c-~!t0v~65B!(EYs;fGUto5iVCWpHFC*IOU3fk2@KPL@(YfCkL3@o6>wU>yUOIwj8z1~) z7JqMTPb_^)utWpKN;GGUXYE_RMJreCURREO6T8n{+0!-7>f6IN>x`_<%Wm3S^Tg}l z$@w;o{kR0)8aEH7X^b0j3`rZVTyaLSjs{Qrj+Eg!qmjD@#t_eb-qsv-Hn}>QE7Ns1 zd3v_;(Q6G-j+Bu~rmOdAK%pU}eq+_>qEZJIAoR8Ka=I`QNey%3|Kgqh} z#z?lo;l(?+cVP{FWN6;mgv>WF20Aa}{8lONI9jVcgG=89*TZ;r5EzClJ}&WA5~m4& zkCU#sgzscsQu+gNiTXCQ=oU>Vrx%qz2Ym2 zXy+-xo7KOa$CN&Uc4pDejHI1*#}7*L^O^RD^=$2+@70dxdA*BQ^{!`~k?Z%JOL(~Y z9Binkhy72bZGD-8<6}2#s`6K;O?0^bzKFk%Q*voP)~~B8S^6R%f;{z!&R z=8!>+c>{I4e$$?-)+O3+tXO+!y}}%l#GJN<|Iay`%N%McSRxr`>Snpk&Uaxa5#2RqpAJxRuQ z1>+l>dF{~N=PQWA&BmlnXP_1Zcv+L~J!rDq1#ATPP2qr=-fZrQr! z(&!0&izO|G51@^D&$E7R+r4})bJ*dz%u%mx+dXkE<32YxM(^WZ+m@X)_x6_MbHDWI z<#WHddHLKg+`D}4=8olaZ+mk2+)dPLeSZ1eyD5Ly=oNFnI%UP&ugqF8_sdNy=H7Yk zin(`Ox?*nodsobr?{5q78O7{SiybUx4{6WWGOpR$_I#cGhqUKM=s(+@AEp1A_B{4$ zF`H?ZEr`9_F1xOn9o3$%*Z*Pd`D67zvOV9R|5vofxvO68Qx~dZXv|taRi3 zDwQW*r0?pt5M%4_BrLBD+G?W5IUD_WLuP!ug|$rX!8aDxJAXpNe0Bk2v!!FjR`m@( z*cq?lJr6#rlYj6K)4!pu?bi<)B1f;VOm38Hj!C5)Fg;w^Lc%$TXARH31h(t} zXtmzij@sXP3veg<^Z(!w-f2~Vzl%*yaRcY%{5$wV+ik-Cki;jgGFr1)KZ9~F@_pPQ zcDPSh-(;WsS?QGX{qp?`+u!Bp${T>a$kPu$+!;LYf!2*z?KvsBYRgH!u7{;>VhfJW z;FGpjXYYM~mw_`pXy2leZ^P<(JQ&Mu)b(ZG5|mZBe!h*S^4h`Ic|CRfnYmd%Vorok ztGEMmAJ6{oed+eFpWpFzH|g;7-iMw13C=ps>g}ekwa>fO6ARf}mw)e9f)5?i@u9xP zCv*AbrSn}L-^waH>FV2@B0geH(OunwzX{wPA>ZRaO87JP?s`x%7_3tclD8yx9+uX0 ze_@nMh$Xyin z1Vd*SJT5;I9*pxStrekRQF89@x!_qm*-oE@v#;}*>VWU9q)VsZcSzwqWVj~n49pwl zyG|I>iuuf9g~0LFztSG_tmezr?z@7?9Q)RZ?45s$*o$v<@rx$cPgIP58}VsJgM)PF zqM$rJ&%VyFjeU%DpXObaC4aW|u>}u*FhBD71^FikZ|#9r#h-1s?u1vzx2{g&u2r9L zvskt2vfm|s?z&T79dD$phqVk^ohv#*V>h=v?&tDwZutat#Jhc02lt$`l;;-JC*N$I zbGSb?oF_sbnk(1)CTJP4zN;)(j;CS_Qar>Pom21=vo)n~#{d3&fU=B9a zRaHLeAkWtRQyP1Zr)~LVM5nq8J`MTXrf{bla+W=;e7C33>+Dy@i_~{y^%c-5Ct6__ z9*9<_s62LK#dl%?-6P(VUrMsj)-iFb&U*X4Y~x*HqqN+~<-Qd0o2abvHgzn2U~=Q+ ziP0A(kId-o)P}cB{Da}BSj`osKP7j@_sK?-J?QsA^v)h;U(I3foEYU6PKp}eK9TY0 z(|%+{-ufiZY)A5qR~TE>wR2j|*8J)2DxK3&_8a;+TyvmgcZZwLbgt}S>UsGTPw9N+ z@yKU~OILe|?hfY4{=C-vP5K#aUp4W5!z1uL4N&$j%3cYL+7F`a7b*KT%HDbqWpAeJ zQv?KsKeLA3tb;qOS`IY*r54rgb{kD~cUcjUZ!J9oMB;|{%c^T4?H*f!fXx?`y_kb%~?#JLZN`-wOd>lWo z4{TvuCouM7Jnus56mOp8H`cs$qqE6mKN~MoNA`T~^2_HA%FBWN5{EDU+jTzt7Eo=yZAu+v|&tkx@LgK_Q3mIeiYNW2|RViL$dXbcSIZO z!BcV+rmrMDT(5bY+C!WemjmKN8(M#ykr{qt1#aR8w@&K@H_=)6IeI4X;=B*<$HSo? zq&(*h#xwM9@xSiZTp7-1hj8}wNY1{F+tSx`hr~7x-P7<$?r!1r#sP!d!YYb z?`O+phU|T--@_n3103;3=Wc8>_$NELdFLzIo=Xqeddci(i{XM!KjTbLiYv7GnsB9T-qrKvba!D`?oP)iMkjE{@O=o;{cp_E z#VO!5m9+^tb%DQhe+~KK@2`Nj-s^ZTQpV{@Nt>1q!+!f=s&?HvFDd@zUcz)$vDKFZ;~#ng(o??TEUap z;Luh&+GH5p8CvyrZl?blo4|i%b~9&gKIq0ubLutF`#?7H`zc%Tt%(6_xRaTeE7uGC z)*ro5b(w>lEB&~uZ(b+sS$b`K5dSgG>8SUkm(*|RCh<*?`CPQu`cuA^e^EYZjlr5V z)iv;Mk#fcZl##!)h%Ty}mx6hcdCBv&)-b{goNP|fxht)krMH!?aW@{2ZUgtci}w_K zz2|C=!iQRueoHUZ&#IT!6@LNCWLWspj`a^7PGf2Y7^92Ol@((&0Pcg)SA99w^@V$t z3)UY;*Mwe;^H#h5XiY62M3Rm^Ybf7!5%x)N(te5N>K8Dd70rp`0vFQ@VI8BvV12H# z!OfphH72q-ro2Qya{e3B;1n?~3*i6e;9JwfsW1F;&JIrd3trX-#@qMH@D2JVpQHLy z$xngJKERlS{M6uZABdlzT|%#vpE7H`-I4Ox<(ajgaCT#w#@SuJZ<(fXX4dapR@2}5 zq&6HMa_@`vX+S=FiT-F`Y%ux2M^o9K2c!OY{x6cRTotCn09cZnjzC9qD zOm;lBR^IClWGDFLw)L7`= zJ-&Op=OnFrG=GxjZF<3YPPBLJ2wt)_5AJ0=h<+VxfBZVt_h-Ct1}CLU_YKr9D?`6l zdV0A&X}&Xkss4b!>0A0^-_Au>R?0`vpDGz2Y+aI$5Af6R;W-?ius`A*2uyz?!kvA&)1_yqEPkmHl~!FVj%%bgHQ1dp}f zgG70(!bQ~T7)pF-J_TzTnrm9aI5_SqRr+4>8<1X8O1)Oa7Ily zJojL^%z9UbHLdxn1yeYLziwifP47vb=!@nB@ubi->@$O^wmbl-=mc z&!D{caX-8yepLR8dS=W#|4E1!O0@?)sock=!Ym-fF&zw?24wu8&M=OyWP z24zomLlOw7JvM&-GXI%lrF$=!es5H>%(8(jL+CKd2LVT0#q-H#?xE)@FW8 zJPptHG^9=s@TzHel*7~0a6Ip0T-ty%JV>2ctru#6x&IppZ%Z#|&6o0^>9sNsN`LlJ z_IX$SRLa}@hCUTeiB5IvaO~Jc{u^|_QJ&_C)1X%_VhyeJxU+TP`vPqy>v78Eq!WQD zn^*kvMvV))No|;ZL;c|I;#FjXIYW%f$-IBW*a z=N&BB5{4^$k@J2H^jzNiHH`ln=Pieapk?CQaC#6~E%#6L-Tb0CS9A9Dw9(QJ=fKaC z)tpj3cMz@@s&W1o;JnAdnhwsQ-!H&PeYLTozd1iv;CH#Vu_-56(l|xIFq}oO`HP zCsxs^!sY@$crC#Ie{MNV<5TJkH3OA79Z2oF`K+$}IQ^>#W%-w~e}~ zJw?7BTiLsL=-fuW%C&Z^*jf?0-Nbqv|2Ah>=jCd-2ilE8H}9&~!1_nBG{V_0O1E)i z4W#)sT`wt}HBj*(WH;b9)pdO>M4ZhTEY4qJMY#?2E9b^{;a?n(t`64}2i7-&Ux5Sb z8^ht{w%d4O{Hej(Z7K6W>+_R{$yzb@9?FV7YM*}D7%2Uxq^ELMg+m381Ta0XCjCDt ze$QZd1pT+MS(o&md3lLwoQ@fBtHNt?bvk|prLPRe(%{Ta(p#&?o-By@144z&a5uW~ zi+^8k-(HiAvj^L*Te;Tuw*1qw+1ZHh?Me@v==5on8=^uuU+FLQ3 zoq03)24P|k`8|}?!(F^J8tA!UQg4Q@BBUn{jzzCxnS=Wug{q$ zy*}5SNI7`zLC;Upe@=Ice^Z#0!`EjrrFn6OTd&9l7VOgN(ZNRwQuP0(A;l&*V#Pkli{p+ z7CT}yx>#)tx>gV5!0Uu$e=djzsG3{+9K1?4rRWchJTHiLYD<3J6c^?VzfMo+S-HuiT|WiSnJ^TDqP|4 z&|bzS>VJv)n$NT+mGky37|VqEW6+x#bI z^GG*3(l62yyGnhmRX_WlK7uaP#xK&zWu{^SNZO2vJcg| z8Ji4QuG)vXOLxEhYAt?J4{l6%V`o>)%T>Cm+V0J}@pxU8ef|XR^25omrZW|y%@389P5f5ryrOZ@d;wos zpW!Qwi?>ask8B)Bw>d%g)sm(&5Iv-Ycldiaf0P~*?Oi!=v^lI&M+bT>>?1f^>|0B6 z7SD7sgD|Ylq`=oKCkJ?L1#v^FIO2*_LcoAkk zlZjOg@fpx08g>26V;KLD{3^z%snRbP`lF;z+Pobh{Y?KPC!W9RSa0T~=VW)XKHu(r zn$%14WP6g=8O;A_p6pNZq988^OxXn0=lJPMzcs(&hJ~Z*fM=r59iF-qx$Dv>mVG1r zcM36v=b$6~oak+oO^(-X{-J;J751OU0{>kVy^GMhllvh~LsopO+ghF(FSibK@oi0t z|2&lMQ*x%HQQzUy+1==_=$co@Z>7BM0TK?_Ii|DpZ2fZUJlSshE}@*wndY(5S^ z4FNoji+-GUSlrbV#fyR4?DUKF?Q|!Ebbeh;>rT5vV<@~0{{H*kcersthBoA8+uj|z zR_9m~J@laN)7kp!xE{EZX=51wf~z)@`9An2Z%5gmp|j-=h3_t=@V=jv+ioEjzdPOhrQyXMufp5BelQ_x-5IR<}WRXyHi%T&~J z@ixz5Q~Z8v62qjr!`^Ue0^@YfpukthqhF7!je4io@tvO$k-p8;YjQEbetOBZRj}?v*T4 zFFz9+`RCce^Smg@XN>Znhzi=j%`=Y3c$^nq)h6TnR{C@?zlyk;KqsvFt55cFrZ{N$G_S4jcPZwTluZH#{kCMe>RNs}!Lrd{kg{=~d z?@jJIDkiUtgXrh=t)3;9PQLn`seAH1@l8O-(1t_Tr*^8FFKqmgt?yA!^MQQsvP-8! zC$lGL&-wcIY;m-f{EI(KrpbQ*UiE8uivcA~eg4fw3C)}>XnTmFVda>I_Fh>Bs%`l+ z?)rnl?$ercwLgoa^D2!4pWP|uqS+V;m1lP-k6)@`K#TT&+zQkOAJ9G9UoMmt2-icb|+wN*awsfy#_1xCR`B|Nh z4d=GEfRE0)>7Mm;Zkyrew(0yc7swW{b@ddbV;eBX7iTnq%wgMO4tsZ=Gx39;1U^-^V)c8)Hrn#EAZ-^g1=3M!xBLB{uD__)$@Q2a=*oZUp zjC0jo?$fI0a!U{9a(l0@v;%zFpmo7PeX(-K4p5i0f$JuALR#0^3W6hBp+RGa{iwCd z7ShaC@Xxb@bZiC7M~9fL&_dZ-#?ILah8Oa(6q=sHukY$J`PP9yJ6=AI*EYNnyQsgN zAIhQbektm%JLK#bM{~_{YQLW>a^_uU+e2Agb78dcY}F&(%VKih3-;z5$sn>g%gLZ* z6dRwrN|4D5gLTA(r31<01o4jGOBUw@{LFI&Kl6SdSsd=+NET-WaCokO!@HM7;pt_u zm1oJ~H2Qn6viOI49PY@X$pZFmw@;H?A>XN7@hthPkgGr*f8pzmp&oi?wLcHVv+yta zxHR5RL`UWt@vlrp=c8M8()Z3OJR|d6(lwOPIwhasE@JphbM|+!2d`_M8TT$j-&0O^ z^W>3N%^zNlzu|EpU3@NC9_7d1$ui&cNXE;_a-DydENdLAW!a_mlPRT#vaI)NS$1iI z*7dTia@Dfz(gv;@$g;|LS>{=?{5tV~<`i_H(P?JzJUe(smNma=9VxkQb9WzNAC&JR zB3-@~Y?qC#&Ni#gw>E6uP^Alm8}hn@ad`{Biu0?X{ns>U>mObOXXc5D^FkxBrzePi z>h_%C&&ZRO8wY<3=YLER6K3`=I%u`SPxgwliPLYTZt^zbQ2L49$x%i;qWMX>OY_xk zd>NI#mB0qynWJqD%N(n9>kdCB@r>S@x8BDI(Eb&) ztvUFe(6!Ugr@Ye#_FQ$B(WKHU;Ww7L&_L_AhI+xN+tcE|6Q(@UioPB9?$;UetL0cH z6|;#v$WD^(u=N}?l)aqkzWT|@=t*x=FFL?ltOeQATI_PkAawIMMH&ZS`FG)116;qx zs0+$VW_{Uwy~9g*gU=rRFQ>257>_F^MCtfm08KQW=YwWAX`B=ta&i?^#NASDWG{1(n$ffyVMU%gex66LRL%~(^@-}2LjG@(@g_p%$ z&gZOkpvfZbHZb0th2*=`iBA!_nw>vTnKodHwkChnHTh#M`5p4tMt#P0)PdHDyZ@T# z3X?VDHpEkFLXYQdJ~q(n47DX*MIIyzxj<*UQnIEr#@=)bJiOBB4BfqMdE7m=K9FIb zuCe$I-;7k5NwtyutaiVMt!;w-{p>*cCGb_>LI1Sx@~EwS@llJfkc&2_-^BA8=OTFd zylC%-o_s>zt#rQX>axG0Z1T;^vJX@~=#*A|Ow|^$sC)TDE72(Ld}{KW$~V;Ba!E6ch4rTwTzXRn`3zYzg&@_m%?Rt%D60 zvn$z01|RDu@9LjyTKctDIOyDn)@)(E^&K3o@AOwO%G$_}aT%JcQObvl3q2g}&P!mY?FvKRq-47s_kT z+{!YJJN!JMy1W;ew`9-3D|U`p@4{K<$S+e{Q`zfQ{K1sQ-rK`AhPpONGviAVz#cmhIqakLKw_j2IHF7#*M&W zy;|rJZkl(1)4*MZ)%8CZ)@N?E`gguIeoa`PImhZF+tu~2aP{TC?P4s}J9+funvC~R zrH?V*vNa!EiZ7h9l1-I4(Ef~{Z;Q~f6WWcE9pLm5<79g#;)ND`SklKDC;TJ53n-`e zGW@`&7WnEs{Phl&?3&|%vGFhP1a0mQYy!=FT2F~br+J-88oCfW9N!8)L%Va6dxh}Z z-lw*U1LPAcYUkSLp@$lsKWLzGv{QM%6>W%?(#yXz*%_>V`o8ZN&N#mkJFFHz&cu>v zbfReF^&Yy%#`#Ti;;(&GwA0>EvOnSZ-@p649?$z96>ab?oe|2I*JJV<{kz&gmslGu7nL3h+u$xaYvTiR;_Iv*eEU!R zsH&Jzq`@=T)7P7U`IwxLZ{TP?DF`}vs-rKTAR<^;r zwUND`bg}hgnd`>|CGCfl$75{L#@0FUV(Z7)q>WKkZSdaK^2%}>_&McP$%P;DmN7{iKb#ZKvwplXX=6-P8@v}Us?vG9tBuZ!usf|E=evHKSGpqTNAX;z zKRV~clPTA^Gy-47V_a1mycf?3+Nhlx>)qNo3puB5*HWif&*cnV*oN1WL*~Xq)dq5^ zHnb)^Y$x@QIn&4J&Ze;5Xop*VZruA7>Wy^uSdZ4I9y-$Mjc)9n9o8G~>MfZY?@>K; zq_xLd^nae99=g%$F&|6~>rHX>n&-y9Qa$D z#~ROHGcW#%FU$O4W#_SP8kU{!%C^mmH&a%3C|EyFZtS~L{hZ_aIlr;@H(|XdSMSIZ z;?Jnw0#|QgW8Z(N-U3&zsj>Hou-<8|UUXvoF^}764!7%@3*QgRp6SYB7hL1Zo|%-* zeIl8Izv0SjZtA=Ge9q;c$32d<{l1B%`NZ$(Xm8`)@*lu^(%q}w{4vSteDPxUGnso% zpY`c|4S#xOU%mDU@~dorQstFj?X$3S?NhovOj~O)-qQK`sHlQtEHW@Y9j za#OJu2-o2bZ^d=U4)(a-53c`RlI~Bwdvc%VOgH|kosZ*A%4jS#$H|sU=Qx|2G-u%V zCGUslX--eRZS=5zM(2LtKaXha?eNc|*thi0W7wPa&*+42=$UrZXK!bjuN55p+?(D( z;_$NdDR!sFO?w(1XUk*1L+8J=ul{4;OUDSNZ$mb??rg}{Ci`Bb@6FdHylDM)ZHMr^ z|MY0!>HK^cmnT>a^ig)gupnMJ2MxN>EyLJX^>=fdp8>riJ9AO6C$F`V)~&BOKe-DO zpQvKtPcYn79@^SHA;gwc)FWN>j-g&oXKEDxP8nCPAUO7{Z;t5uBgA&%=O`=+U~4=t zpkMMo2#@ppGq4x?XZmuMf2OZ*;@QrF$WEk>>O0?vT%xvYEYx@MeBaYCax@V=Y>c4I zJAq|5Qr^Z0`s&@`ourM9dcVlH$wy;jH_FSvz<0!WJFm^jDYmrT%asT2IAlO_JeaL_ zgZHT+w*%RFML%u>$>XNH#5OM5u0C$)*%yd; z9isS07Z()DJ94gAx!sgWY3)@z+X zn?>4L@7Ew5leXs6FVgCC_q_AdCx3*=Iyf=!UWp}}s(y?uh#FIpGnUYQ zCnMEm7gM&MuBWb_mxeh#k@7J767sNk_utq*^t>xNiKiqJJ@B4=TNz#FeMKR^{TBMk z-ymC4@N19}@vixrOg`mXhL0FS=~>B`kHM6VRG!f)o80SX|vTbNM0Sz-#eGN1h|!Q5!|bXuUKQ$1>JY4|EyKD z2WapDopFWphY6O=G5DXX&-AZaU&y|KkNX@A_lCS|^Csv1iWztBp6UZrKJc)g(mR5~ zy;pq}=dl!>6`U7rb3QS{M=}CF9u9uBSmRh!I`)Lxmu|Q5ac%iBzMn=n`rSdf{aOD_ z)?hycehuS2*Zf(woAf%e3hhj0p`XT1^pl>4hwn4^^l`Q0{blgoMdBUt4f@f1{-ers zv*}y-MLr|5bC{F0x7f8f+W39iG`k=}KJah9!@bw?P8*y3dmZgFcB%g1ITyxU@GhFT z_4wntpTD*uZXxz?MwtEnctrfc$9>u;(pr|6p7ywmVVuDgoo+ETb$-5aY5nYMrE5=; zdPW<2$L<~%_02@@cw0d{m7hRAoSfwAc_wYrv61nc^j-eEWX~iI{WN}ga1+dmy^uif z|Lsq}r{BgG)G?nDbCG0_y{;vyo5sQ^J&c88tcscw7!Sq5T_d@IzoHQi9{G#P`%=Ue zGk)j^?2gBy&MEP)7(eYxb?|TPQ{J~9+GT3PrwcE&7skS^P<>Y>KgRbrn2VakvqOj{ zFYf{BTS}eqr12=cC7o?^I`m9=6gsc>b0EB~SjdV!3wXxc)$QK@Mw)n7`1(E=|GGIf zR||aaivwrb&H3P6_O0f!^P%NRZy!)DqMYu#JDYUqXLf`1l5|)Oyo_&XQ)4bZ?!RXd z#^sme;tJ{lK)&PVSR^>|mkVb%43r+UKEaD{fOU0?9o z9wD3RHs@0n@2&Q@&`-0uNZU!ix4B+$G)duUoJDJB!8g&&?ysCr!utvoKa+eTL-dv{ ztn+-6jPB@5qkCcx%a^HrlU!h1B;)L3(q&s5T{;1L6zit>c8Alcra$SoA2Y=W2&W-@ zvq`?tPVEum*VMjo`1g4H96WOH_5S(r${cV2*E=Zt>>Ol` z^iM$Z-;;jDRmZWPbs>2dl}^8EI(s$CZ2u^=Rpuju)RDgU!Qp=Y=E71p>C=GG1AIGg zHY*o1?S(XkFsj$yv$Pj~acRA4yOrln`u1})D}F8YQn{SdSU{Fc2c0Os8K}--v*KgJ zI(%>0*RlBo7(3jW$>Xg(vthtvf2lH0vP@nLc@z1m-<{Yj$7z2aJ!ST!yR%q4X!a(y z5A7K*O5PRgBAZsSWOfkaDESrNSh{Q@^0&ZiJ@Ad$zVKPfPmG(%JL9ZQ;%Kivqd4>jjNSV;Ky^C*xd)xNMUrywHAU&izr6pe@ zoO}&;@>Lhem+B>J`0}@5{2KnR&$EUatF=YoL)|Jz{Jwrq0te~|Bu&I zi4V_o($c8+BWI(NmO7n;PLpgjlh!JIDV1guP5F67A$>Cl0KgFm4_rgyH?poIJ`=H1rq;w#}Jxzs+F?&RIZo|3_Z4{s~`qKIF#Jsju^Zf|ay?o@;+hN%txxX$fC%K^_{wP2ZCf-{?Qe z91@>rd~pPLE-AGI^Wr4OHxnG+jJNk?rE&awUR-;K_~!`tZnW{cD~X{Ok&@@cOm)c^-)f*j|$`yS?eahVlKaux@$7M#VZ%uT<+&lz1tlAuM2xuGM4E) zs&epUkvf{AtRC+b^)zo&Z*g@!#$EL^C%&DZXw|u9Vee(KX)gnZ%NAnqCf`F(d`iy7 zAwAS7#Y-uBDfl6yn_A8*%|Rb@uE_Ltk|zD|-?hA7l)O(=-qH-Q^yj=NTr;e1r62sb z+4&RRcdb}xdr9^AxAJ|FRTH}|T4?3EmL~t5=VsuAvu*5)XJrx_3%r;ox90ubX6G)X z4D%=HoMp8$G;enN+Zp6ZR}|0EI5BTi*6Rv;cDf?uckdGueLQcAe>{I1tbpemxGO#n zk0))uG*4?B^ee&};_V{5^ltd=E&TSM>GZiLFc(q zpJGE5H>_tH^K+3kc+~7y%7tSao$UMxf+<{R-~9P}x4L**rZ;N5Zs%*yVoaOh+Xak+ z;K1v}`S3*3!rnzQlX-gooCNRgISYGx<^v15+I=0jMui?X*B2L%p2_SrKRW3~gIgQ1 zNxdH(*peAwRLIOM+L|T&0)MdH;d|^iTQlIV$0kf<=&RU5oima;S`*))_vyUPSco0r z{Abh4yq~OVu;IzCq$_EZQyT4Y z9DSYww$WEKAishJQ^0=;HQjdS6_nnv>%ikPd}QyuYgyMQG=E0)66}K*r(6;CEw+g4(%L@4j#I&ugD(0 z<*|qF&-?LB<|RgtAWz>6QXQqMp7%L=AAH2VzRx#0KpWAa=gcU*Ti4TK7w1!UHF2j3 zdSmx#QSYbT9Btgy)Y!ZGGnFZ=@~G zzdL{P8^C+oUi{0XV_zrz2DZ^{L78q>zz;nOpo_{ecNi@w=jkI_tfGwNV+X@KcST3f ziZAwQBU~Cj=A|L5Q3JptHpSq`=S+KPj0e0kSuq~?kmc{`CQrWO^tnEGJ~ryLe9f2g zA2SbB`;Uv@Z?uJ`va>sz7WTdudWi0#qvTm@v;{IRDowVf1uX}Xhq>CT+9Pi$T$P4lwO&7yZq1}8gu z*nA9}iusuRwnK-Q&bIkxjKRjnad3pAx2Zax;e7Ekj4gER29CFZe(%}|^JD{Q983nF zhm8~TyPI(e$L4EqVr+KP*WK_!P1zrpv9Y+Yw4Z#9O`rZ*1O3(Bmy~){4?JGr-;dE* z8l&I1G5U=gBj~F+V%SJ;r!&Wr*N<+bRnzUVkZ$CMbSwS>oPR+b#!LShY!o(tHg^6L z8u6|*?{w(e`6Rq9o6*7VdeYgaszYp?>Cqn{Cx3;eu1x16z<8t_`&6MH$rDYx$S*!q zb5GlU{^!2+eJ|Mhq(*U3d^@l6p{TFxA@=ruREDqTuAlPGeGq55HK@`C)WP!;n@;(B zzbDbx^J$MUN^r!!1^?Xj=I2}M@l)EQ>l52Uchz+Bo>v*pVB|?p>x_xkPWSVJhk4i) z6U}aRFf>=C?deQfz)!|g(8_p<@%{)rwZFrSdq?HC{~%poGVbK{8+X#G$9+OL?&MdD z`!>dX8+DMI*E8NL-P~?_0lT4(gXih`TW^c{JdKg-i{N;A6{%|+j}Y_UHe`3LpyIz>PU9C>wPotw+Fg*v+5@6)Ty2i$@la)-tVhv3@Gc< z-{s`W+QM#1=e;ntQFEejA>Z@Jqn<~UF8*+Pt~M`{mS-+ZpX(Dk*|t1MaoIG zE&rUV{C6e!ukiU}s`BF`-|c5_$mtwD@YEOoE8ax=Ba$_q;R7G64&Ec)yC^eD^T45r zUA=!&rm%l}rl9i5-(>d=&y1&0rrFu)Dr4U)2+M4k71s++Xvn2y}H`OPko|b-3ZTp3_iP&@eStvtnkDp%jlnZfBzYJXa3heK9Ili-}zU* zO{st6?*;zT`JXj8-}6mu@s>&%=<#|4nBC-sYtB=lr_Ir%SLjpTLpe$3>wjZ@{Af2x!uL>8ZM*{ zqc2)dC`Pq&N;asfMgL9G@hcn+SCq%$ z2hO(D`K4iP%x4!drWc_{hufNMLv-J)_@lRxK2$R6{BpG40}S!sp6OAq@x$en@oQh- z#~$BMcfMeFV{ajZJ=($EF)O|j*gex5iG@n!xf{6Yx)K_{?|Z-*>EMhe9x8-0-ofE~ z!zCWh*a6_c3vU&iu@27o#@^3_aHcpo?>Qkp)5DoG0GttxeP;{KBnM|oV{c0cXR3qq z-4o(@4vy_}PKBNtv!HJ^%r)-|>&|d>e|ADVEv!4Ee_ipm_*A-~?#J++JEyqP!E0@~ zsMN(6e)vn}z11RZn_R*J&G3TuRlk0EqA%~)Kf3K+dtX+%+uoOyzRJJ!<(Kup8{ee< z>BkoSz1|d`DOQaA)L8c%y?Et!+O}s`{AB+CF-nR_va`N3!37VV~ z&3#U};$u5kHxv^;(~d6Dz-A z=k@EpFmQS1G4s(;KK1GIY~uipnOog=ID@_qPhzwu{Sp1*aK=_NAG!3lk99U&#~fsw zcGcNaPn*^59vH<*ZIX@mg(G9}IestmU4(u}^+RL#M)8m@|M-#d7hj^hVn>X&jDzrAb+_w14JXT(#^UMxNn-Ltyn_6BKdy4vz(j~ExCp8A;U-znDE1V2=>%?q z@AA?njrr|EneUWNd{}ASL4_*cQ=f=gq3-jIgq6B{(XXERmN%614eCN+k+s_>p|0K+JG)(hf zbyWNq`Hb({>hCI{gAXSrsgRbv;HbF6!$M9QtNZfKqvCId`PF^-_)+n_VSdmTqt#tU z#dn1H)qQ#DsCaXjU)`5ykBV<0KkW-Rik9d>)*$p{ykft&$Dyvg1~i`G-P%W9?q1~1 zei3ut?9N_PltD+d1!Y!xU#WQ2!$B|HN*TjLXTgH_b;E{r#;LpXU+zK ziG1D=!laz_E$jM5oEtImh4rp3abAVIdlvtjZb9~KKP8ZZiaMN68??^cvg{D!UT)_= zb;5S2(-z>ir~)^)-{aRkL0jshjTyF4eu$5$g4Vp-n4v>Yg)WVb7m~g@c+6MT{WZG# zy6Du|gVpu81bq-L{b-5*Gxf?r^^-Q9BR@%L}-me)Q#{BK{kn#t$2|9*wr@$!U z(+=lOfg$k@;uLlMRDSn1;%i$KmqmP$;(`<-ME-pVy@(eI&l+XNmHrIwC-uno8(Zqk z*2FsdanGITNbJ^i(jzaP5I@HI{snG5umGEmxTZAT&-)2U&nfQ6`{;qSuC{scDq`QP zZN)-?UpM9aTG!TZlzW=<4=Pg>4BO{Q?$r@KhC_aK{AJ-V-{COd;lOyw9_jHokUkI& zjUf)q4~m2JzCDk_gc^r~UhmyYw znKpR3`^Vio*SeXrJFIE4!`Q1Fjjfl>wqM5>Uib0Y@da-rc91nq=lI6n3pKv{8*RTk zJ3imN3tk5N-p9M(@5bKNITCAURJolLZgF;eN&xrNf$GT?ucHU?`{F&3;tGo+u(Xfj?KM4%M-y~aX?)do24rZ|l z--2Mkrxh@HFQTL5^G#tooh6#-{(^6ZFjtR{KkH#4>jsl~x&kKe#FkgW z*$-j1j*nYBOnAg#qHii-@~+tW3Oso?nDCe4)HjAOcZ`qU?P0=C26I#uOx}xA1DNu6 z^KLNVr7rmB-$Izrj*pjlm{S}~?BfbNc~{(d1)jVc%qhT})YyAP2=lO`<5N9M^pxR= z{;Pn=yT6xoy~bVljEr?Kku~Y3^8|zO)mgtH`T`ri&-B`8r`JYCrq^`GCg&3iUw}WZ zJN4*zGQ6yBYl>%%0S4bYD(oJK{vyrb)tBK(e_6feqvOMadPfXY@7MwAefQ{iNKo&P zf$HIl@c5_gv4QH%8=&4b6XS0O^|lXCFFJVuy6l)3-xbumXP|luE9=?3w&-@WpBwtTlRa+225xh0)d+Qiu--MJ(kI}-SMUMHIj9eX)#Yp>4k2F5>EsbiD( zKzBWEI(AMXYufja?)sqN?_fSPDPH1WYJSrmL8XpO-UHp0z^u|;Zx3M(nH(?hFim#> zqe90f?}6@0U{>j_vqPB2O^&B|n5MgcF(TA6$$Ox?5|~xGYkmk5-8II;G~ERZbZS|5 zCGUamdVC<=^-qEU-L&WGWxsU_Hkovn;>W6Vm+|7t$?@JMuM;jRJ-?+|Cjf6;8D468 zp@Vh@^?o}*z1)OK8MXG1?H>pAemYRS`T^=8+Ybfx9v-OPu>;iGI5{o^^}ab!J>r!P zXy0{Cj@tql?E}F$Z(w=7IjHwJ>Pd!V(`o#5KWHd-p}c;IdKGv7Ht8O>(bz;saF&*P z+&B-Uv)##E9#!WM!!;J`u}>2(q&wM>-N)-oQ;q(~86w@0koYjSQSN$`W8Z|hyPc;n zzx@gv^!{u8bJj@DYtSS9jN}iU9jElKQ${d801V|v*r|W9`kblc3;_K!8&qZQbY+Pn zpuLOvX3@obv*?nNY|w95Tlz-8C8gVVR$YBZ0NU6$C+_~8XgHoT7QtPS&@^2S`g5?P zNhj&-nbvzf$g1{gbO*~9fZKx&A^qjc1p7F>pF2=K`eWmP^3eq!KTtk$ecgfbv3ssL zP(C)y`^c9al;}vE={TySbFk`<&V+2^JeK97XYJm1+7xf~_!uGe8$1=auXSh-y2;b# z619zOWwdc+LfV{npnPca<^$ye?+pja2hWqq_qc1Vqxt1A-?vfhJHZde$0y8*PkI1e zXwlhM)*2HN`(ArPzho`#({tQ2kb75V?Sm7_ad8HhdMf*bE1S@hvr3e=Gn{iG>4X!) z^xXUT2ClyGPM*?>oTDrr{=6L$wir+-*kWJ*@hApc57ItH&e)ts01LxLA^YYMuJnY79O?jEIl`ME4hsx>f zn$DP6JT0>D&%kkO#mDQp^KmOQuybb_(bv|HkJocg<2^ndo=VfXTk%exUKgZuAL5sM z`iLN%I}BTWx{aTM&%J{;`}8qEdF~3_=+no^4oLhM#dQ6ayd$^sR(roYWtgLSzjBJj z!Pxx}LvGPrsB~|0xOTZGFzv7IxA*BD}^+7uK{H6UpHc00lzqG#%K|1&JJ>>B} zKIj*B@;&I&X9nrqzxPd_K08R~uD$zx`n({Wd-c})bk3Nh__MEktxsPVq;oIcX42&| z!zO6Y6Q7vY;U2s-e8UkRrLB#_^#2I!a8JF~VS*w5lfGdg{+Dja!)xO4yyKBKpCTS* z?d{}?1zV*o-W{#2cl&peTQ47yjX$`uf;|Cw(Z7DF%r81IUI4zFnM8h;mfFU7yqA`K zqq;8L%lx_{eLNUEW4Z)6px#ritY8aHD)UnVnRn@_%#)|`_(TeAWBj?Aq;4*hdDU}m znauP4n7+fzy_{_==cA8RhyCPpqu%1V?4?rnHPEc{Ec`BK;qM`T7wIPRBctAR3-QB{ z-c6d9`9J;vnI9SIXk;GU>3xbSS3DDVXEs9qB!>R|z!Cg)vH_6!Z$g72`K)iPT}xki zV$39+q%)pr`Q{VjuljUkK287ZiSZVnJ|aj*hBy24QS_5`>}->hVgDY=u;D@9d!MEwW4nC%gaA)u?3X^hK1l!BiSbW- z`msU!koocBq<4dp$5=}$BwU!-Xdz{|7s9Xc94 z>n%O~UWMh6md<~&-Bd<*0S%4xed^?U6+ByChEjGG^CsmqCl+Tl+V?8nCwyjr-;DCT zK-lE={nSjRvA0?0U^=J7KLtPC3-m4FNm-AReXjyo{#|1pey`$^pnQHD?YOde#}zFsdM^f1#t3Xf^yuA{)@rN@x6<*E$%>nVz6=(f^ys;|2E3Wk1qdt zo4-qhXYpj-?=SLx%#-Lh@L>+HZ~A<=G1^!MtvboOnr9mmzUOyjP)@WI4fB#a=?dvH z=y4_G#f$P|i$BK;2cA=&4|GMy&nipIOe#Awt8~RO;G(Cv>u92_DYWO_cl8qdNat?Z z-&C9U>zjz}3HYy0{bWoV)@koP_}=nR;&^p&BV)Ek^>lylXEkq6 z!sZeGDlP;Zu8-DsI{V(2yWEx2{U|EeeJS5(2M^*lkVEda%F2FQUV2z#ltuP+2fy&u zJUyED;OSA}FzMpq)R`Xabq^>0cR`sC&T{QY=7}A^HV*H65AQrGf1AX|Txp+0F^+%S zN;}{qJUEARpV5(L-?wH@bDxH7RR*v=5CPxFEht@u^&arfok1X%Jg??7<9hdJc z<@^fwG1ahd9L3eR?f759ZJonS_xJ02A|9_(__p>36Wkmx>pL|8ZdI{C+LKTAvzM3t z{0wwstXvFWnNK(;*nJcB7(elf#iLK5Z_qgUqG%1>M{}o*&ILpNpSyCRdvX^{5NnqW z?!E+;?)Q8DF2RLA@fGQQummUFSEcyYax7wa2WDpn?ZbbaUr5?qHN1K8t>7Y=#D7}Q z-8tqvs!PV-^LD<@oil)MgR=6hB z-k^9`=Zn78Q@#gt3hi2Yt!&}trANuz?$&vhJ|anfi1a8-$8VzY50XCF-(Nf<*xQCC ziU+s^I;d~D&rNjtZHaw-Xkz0kI|JG%Uq9XNXF3IXe9P%Y)!76+&(zqCXFrwk?7KR+ zzp_bVn8QcMH!~SO>6#%L!-E{V1KnTB`9b5WD>Sa~mBzIJJZ$`+SKgiZR5{^zjBuRP z*n5)WIekCNm(~5Ihof8k{ifA*N4vVmySf)q_aD9Qv?&ugH$Q^rdQW7<;Z5D}j!=u`p#0h5 zDQ{a+#?MRs-q!1058d4u6*QKN(Ubp?=nu7#=!HehuQkd1)ghi5hR!VWlN)>(s{H&$_-|ZS+Ks!MozecZ)7l&D5rgF(MxyuE;OE&zuWH2G26go zgfi-rkJBPHj(I5QxA8PFIvOA8RO4xMTQ{;*!PCUkz#|DyLsRkeQP4EuX-6aBP2GMx zP5E11fTwB4!?%9Wp7AvGesr?NE*a+`ZmcHu23H%@9Px55=(}U$nN2ie<}6#l;=Qb!TnNWZEn||yS{~@^)s+-zFx0A z&Nt7m_(sY)tueS`ZyDogv|K>^sI$>qscZRfnjfD-e$L#ZRy@))d(9IR4c?d!K&difw{D>TMPuD*hd z$)8DEoSe&#R5J}3vp!WB$=H+R)A_OI{z)_&)p@!0EjKmoX@h@DbhjjYGn~GXO``df z?XOxKbsbw6w-DL7A?ix0U*{2?3;!<`PsR1?j7GD=B|8sTe(WL7l`2Det(9fI$*;%n zE{EU44!^PBC*Snn$Rqe2H=qmdtnSqLll_EK^r_@W=X{Lkz)ATJDnIqgFE){Jdq4Q8 zob}=798FaQ8|jSEZXRLygX@qlb2rH*e1)eroyngja2H z&Xsil>>=CJ$U2T+r_tU)-QU?f^Ih9g-WIxuvgpQ5zW?}^|D~P!9)XtIooG zwGY3!v;YRWFb@Xbf^lCV3}9>hUo1we~!bJw(+9Oc%KISTF{k#j_vn;Y+1}Y z{NQ*UUTf&uKg4?Y=8er4Wroq;W{(1UIB+=M+$!I2s$}S)S~nk48z%R{^G4qr;{4Bh z`oUYGiRZ6!7cV%Kf&(9WSxazrGFK>Xf^T(9cXXZ}b={!xi+;Zb+)op)PH=t6_%68W z%cXuUsk(?)UtUIjgT=S?8Rh<<@>Q;U6?q*mB;~~m#H;)iU!HF(PdSxuqmDC4lioR9 zzm%ECuh%;Zp{w-boe#x&Q8?IJ(1Rk|qlhj$=*J)7&X|6!@;q}9um)+5;`R7E=3$c! zjBT<~jbFT}ww^JJ_klKP{Q3sxzQM=BKO{`sEIjhOjC@@dUQHN2wea@|Pj=y#2_r8S zf7J!H|F=27$;L#+L%;9wF~+yQU@zFXh;E42_!u|no0}ekH|Z;l8`n`6n3zHSx`f<5J1HpKej1?a1M_U&}Tc;d1%bMV^7@sjrc@ zmw}VFXWGGZ`*k*NWUty^E{w~{rsIb`HV#ma5FIMWf7J3zU-dXQDGjf{CiMlH)TAZO3roX=Un96(_OaCQrgblrJWl;L0izR+W!{HOen|S z0d~CCcph^6uB}I~IpedR+5>kCHfU)kEGysIiLU||8q201JOG~Hx12FpzkYAiq>@PX zM;E@6E}NeZr~igxXN5Zclb2*ZsrrZ4bS(ha>xD1oguytDnI>vc>R^=oKlBt z{vM#Ptn|bFSES?EJ1`SHfR0(DK1Rl*2gX7x+Y>t`)|)o}hnM`iWZK^sMOo2yxY2fA z=NaIt{R(fu!?FR7AX_FQ^r781D|qP3$KCj1b&RBcNiSUg(K_Zi+!hgkcYz^1+BER_qT9`3+1XQCwx2{8%tHE?gapcHh5TiC`|Y5v$oUT z?~})DoI9|Q7!xcVo#)&C-JxDr2RM`N@zt1;y(6$|=u_D>k2;xB`@yC42=I+p7^k$y z^PeTB%xScCgKeDbTebbqjBCYah1hN6v$MM>rv8lAZ0q2=`$dJ|k?N z)!TWH%^Nun@}&UAS9*fc_j!=b8#xd1nE=M4J;4}L1P{)GJP^S6C@|C}?HxSb^`q7i zjc4GI_rf!pgZ_>(qM>AX9Qm{seDQ;^42#BgE@Myx|Mjjb*8(H9c?#BfN~J3YWAEhr zP(S{)VCU9T_GJ0q7ww(_zR%4(W!rP5r)={+N8u{H^GD&R_Y*d-mg)3P6MPe~)^#Gd z4i&!W-7OqG zJ5T?RZuCTV%CCV8?vdSQ@;8aHlvBMeiT>ekKjKaQOhEsDo92`q$Uz|<__Ype?%{2W zjKjn^xh;UTrnY##VZ>l&Th*CGC#!Fo<3d}eLem`tB?mbOdx zk>~N~?{VnQ#wy0h4+6-kSIod6ZoL9elJ;*zFJghUzu^KAFx`fXgU{&$oz2FR+ID;{L2Exj*%ap5_+Vo>DnB4Oz#g-Co9#CsT|QySZ1#WT%L(l{zIo>; zbQ5sSDS*S78}PsGe9t!QG2;t-Jgwo=(BgiiklmCJ%Wx- z=r7wuW9tRZZoo#{b*#Jli~dvpSRWCe*GH47XL8(*@O~G6&-60aj)kh5eCkVd%(I4z z%JE&NzFc%p=Crsk&r_QGpWY777TDq7F|EPrEcLn74}Ld7R}0gYcl&S|@1ptekeaT! zK1{vu^x+Zg|M%gM*e(it8}+Q19>&9G<@0djvxGin;%)x+lLvii?>OsyfC}CMNNA_> z{@yY6voEF|qXY5QPt>Ke^Qvniux8N??Y&dK%o@O)lQ_Zha^(9#_nP@V@>|c3*B1;P zGHCGQwpicZuRfnAIMJc2oD8y$r_1|6t)1vOs{K5$A)M=MQ3?pcMYYe^Pi4 zmVejD_UMprSdly-yz>h3`uC$g&YbD6fB(4vAK~(1K{@WYVlEfIFXi!A#yHj&t|1=B z9|gQ$8BW1lkOg&y@Y>!)-uv+3=i#|Cx2H_@Z~Ne=YcKtvu~_RWdujjotW2=iYiAE{ zsqQ>=Tg%P`+wNm*t{%`|K7GzvmS$?TcXZf&kNEvpsj?{3$lNeh_QOX~fBL{n;!B;w z(;2(vABT?^-}Cm$oCNO<(OL|5{J>9?H-6Hdt`5@58XKo7yq~aarc~q7sj2xV&rQuJ znkT?%5BYnvV^hwl|+j zqG`D@$NU3j2EO?+M~RpARfs;CCw>Fj*8D?s5v{bRAxj@Z<3n`=^XZm?ZmCO#&XqlC zZNnaY)AX=1Z-O51hoApRk9R=liS*fi=v)T2;S+yvtsIX`9_|C^Gx37yj zpUKS!#&&8fYG>Tq3vS%oZTqZtpRD#WuE2A98MprAzo~oNNS*Pr^@IeDx2C_f&S}OH~LGwEqRpBu~IYzAIT|spS6A?ta*`q4Gp4~?oi=dX|_fVj*mYP?Uo+? z!4Y&Kc$d&;)<3kVFI?w>YrIy|GY_c0l-^zyWqvvepM&$kh~8Sav3t2m-{sf(2rpGX zGcNZBKl}xIvV^T2jM4e~sC(xdjVtQkh_e@Oz@y^5BLN)og=pT-ztcm1bO7J<)Ls3D zmko)I-=uynDbJMcsD1$3W!iplK0uz@0fQfy4gZ_I=UckIn*1s9nNBC)e&wTXjS*g_ z`!=8-x!cJ3^T=zs3~(~V_rHEbGSz$A#KY3HqU)2^UvXQL{w7}V^ZMJhUH+?f>Nt^d zd39_Mf0uEGPd}}#C44D*YyaV6@)hBq(L0s$8OgTOS-%ge{wi>pP@{W}Y>ig?+dM9| z*9{)c!-a37h4@yyTE#E?YiBS#&jPPEo|S)5ww>f^AO4EZ!@IB1zr&n95%0cVxD?^t z&rkNeJDYnp;=OKpZ+Y%a?QULN;(Y)CO-}7>n~cn#h0IUltP`|&4H{%Mr{Qg{Mn6ZL z46TWK92f`x?tBKXMXRE+rc0tBO_v-Gf47roFL`Xw3AC`gAE3obaMO4tIoABDqNMzR z4u1Q=vzqs4jnyPg1 z`+fLHkcHa)d~aP`x|??@rzY@tCqwH`c9v>!sqJZgl?u7$6XcD8{c+u?UQH^ zeaD6Mah`ppyCsXS>-!1zprUu~CoDTpy5}+K`!e5tPmRtMwJ0xX?eN8a5O#KcLjO=# zPKE|YcBV42F_SxjcXqcIB{TBfn7_1C_ueh&{5gHHMYbU0=SzfT3u>%ds4#tC`@0=K zDlA^j<72*!PU zUzYl>zTe5RWF-s#%5Kb#jqS!l8L_=l7dct9J)$M6_q+00zV~0$)XIG28O>KTX6)x4 zwhmw>@}@G9H((sZw%&z(nw{5gT2{YfOQ*5Eth)qxZw@>I-s_-Ul8KvAv3)AKNOurt z<5Bi*-Wj{QW?Je`(drsM=hru9w6=Tv^vzkWn(&)5VcXq&+RAedtj>ME1==dEqcqhb zx+Hb2r>^ze@l|!&SudTl>bQV)YT{BOnGewh{8g=~k+hSxShzA;jZ!n)+J9RbE>n7##QL@uk!bhH&uym54n^M)fk(W%}2bOub2(1Z}E!3 zKo?6^`_eZjoeM7^uif^T^}y3tBg=U_9q6Tee#V9@u;acWexxn`s2vxMg{^`G9s084 z+Rvd~&?ryul*%8@y`k&5%VuZ)0=obiM5eIoy7sa*$=jGVHuM|Wv`6F9a_E#Tg`Tu4 z#d~=g-xHoyAH@85AXVA#&`!5D9Ih28{!-7Y@VD~sM_vG-X^cccJI%TKK6IB z7vjO&s+8>+4-fwrJ-`orU?DC)F34M0-}jHAzJDu#QRs7X^&O0wqfEE4s#NwyDKZje z5S{?$V^ldWqxo!@(L|w1DSuNl(o4M zmEnE}?7qeLL>ISJW)7XLvI{$JknEE8OZSPcGwJ`5cn{jn{PEoPjnSBO=r53eiKF_FA9I~g&1y}R^j|&L=^N5FEB!-O7wZxHQkR{-7p~yP zy&U~Ae}w+N?hB}!^3OB^D=piKaeV2gQmq|l@g6R?bez@Hs&~>4(D!HXwtE?QtM3_? z>7bu~iyiQhk|7V=503lE7rcYh>Ty{N4*TI3t)mK-eDPWXk=+Pi?CA%7mmch&sU@F3 zQ|ZgfJ~X;XS5a?T?Hv%UPJ`c7u8q$JZPC0tOC9-h?O=Xipck^h&7vEm@8N^44t^^J zs9&f5TyYUJmp;fF3ulVY`m>j_B-5bzrt?XN&y2_L?>6J3+7)$PAX!mZedNzxhz`PU zoTWXwtqj_B(9Sa6j<-IAwvsd5Atw5ii+;ir8gAYB=(b7FPqvHRt*!>w>N4<#&z0uS zW<hZOt6Jt$F6>ww0AZ3;M&+g8CTqpp9EI+oQgg zAEQI`U|&QFWfGp7!5sj`{}*O{Abd#6Z)2b56joc8di!orQ>*X` zaWmU-M(!QbGVxyz`Go(Y;3R&?9HfR8shd-yr{TKLB_z9_R4IO%@q zmaUiez!Mi`>eUB@x@S>o=4WT+bk7{%O$5d)U>GlK?bnLk<@)iW%(cMCj)v|h$8@*) zc9fgVE*a4{WMd2I@+({iY{{G1%w_Z^>0`&Z-y!Z-{IB>C@SVwKw0ec><7Q+?dyHNO zj`Bn$QC9+QB>W1#QM7T-D?z&1TTRTXNEdxnPP(KaH&*29--D%@hXNX4@5X1*?};*! zcYO6-Lut2sU53LwC7HjHKlz@~FY{;B4_)&4I~YGVNJkC8CstzPy3Y99*xsXVDp~E< z{htFD(V!=K`*kVvcQG&7<;Ji?2Vy6y{qg~f1Aol{G&V-G>p#I);nWz(l)LZFrTvOD z`NXd0_j9UW`N}Nc>Z{zkh2Sc_Y(!0~e#@1P4#A#X1|PHz_Wh@_aUH9(x=Wli*C(pi z=5;C`K|}qPfZJB;m5)f@J>mCthu_)!-T@vqPLo$_!Y%yP4r^-Fenu-l47|{3+GjQq z9Lt70KMK7&8a=U0acOwZerbpDrQt!HdAG8}+5WVVYJ1tT!ER1$X^OjZ?A+{FcosMs z%bCl!mcXkI3qNE*yt2#lobu4NjSlw`e!I!y^8T4O&Ws+q)YlL0R{xN=e!thr*B_?g zD>501@oj32hTvPE{x}bK>HD~*)_&k8xm->?A)GX_=w(G^(#Tnsbw&$&Ig&j@T2VSf zo1>w#=b;FBsun#QjV>arw8QIzyG?o;?P@H`Vd zt!{Xhe5?KY7CsIesa5j;`-a~7w)#s6!;=;dPin4PJ){X<<*bBwQgw@;;C1#H=k=5F ziVy5JmWS1n2bGb0ZWn$EgXSx!(_f_w%fv-y*qOu_HJ%W(HjtE+x0YtBU9o* z_3^vFL3W+yAZd-+7GIK?>gXt*nBv3azlv}yx4qkS@^{T;PTH5vrTMX~cVbsw-z{(M z?(%N)$PM_)H7`VuFcv;5TujF?pXA%**2cz9r5S7Dd#!8^>GLwz!%oQ0dt0^b@$j&; zFFFp-?=16oTH)KwPRQMNrM0aj4L_&S+LG}wNYi~+q+!!s>tUwC`>se+e0GAJ>2YHv zIO1a#9`ZZId$jR~ayL_(n{#SE%IWk)YAE?OaDVfL8#})TA8jesUCuYM{>GXbd9drv z7a;$=#)KuzMK_*4Aame!`2ue2eENf0(-_?Q{5J>3h1c(4&R@Nj=ns11SYt!M?5u5wAOw2@9A6%4gKAIkMZ3L4WAJWAAyEC zbA5xOVeZZo)@vtYhf$x-17|1Ku*MMEX&sZ|^|^#*qjfi)^JSBk&_y&mDMzzxm?nNB z&C@I|O*A9T(@bf3G$YOEbP_b9ZVQjj(aiE|y%~H(GktfQB^d12k!^Q1I)ZQ62o_gG z`>u}1uMVZVw_@nGF7ushe>n3ncfRI<6nmZIHgtRdcro{*pR z2>qlsb$}Oszs!EV#g7e+AM@4~u*D>2kGprOg^$K-@uhG<&ZcS12B$;#FKvwJ3HGK? zZpPAvOv&miyx!sazGGO^AHurXj%Zj1xXE{D>t^7lbu;D((`q>bm11rc+WkKN0rD@W zEx)z=lPrHv`3m96A1Bx;^wy93`b|*R|1Ny<4GDfP?Gmr}Kck~`AH1k{s(YeiqBC6o zCHjHymp2sZhrmYO5cmd=ZIflkU-X^vtnm|m_7{Nj0|)CxhoAaAZ){*Ln2cdzn#Kmw z{Me9JPGbXUer!-$-q=8z$a7V&UA-j9OwCmr68gLha~>kdep zohOa$wdfFc5gWhy@*kt@CRhGJl`myJP2C?uwlq%o^}rZ!?nfFL`!s~J;_~Q&529E4 zkk%`GNb~KA>)D>Wo0K%84|^xkO|h*edh|pe*%)r^+rw#a`b+0Fw4bDXag@0fnfl=* zr@!DKtv^WK>s;Ac&RrB{>#0Eav1m+0y*5I z@%0?(&UifW`WhMWL|7$Hh#IyUJmv_;wzUEY_v#ubIw?S4~e9vpBDz_LJ zD+c?nqsTLGM{zhmIf^`$Md}LYj}Lvfpw5E%veq$e50vJ;&{=&aUB4^mOHFJL>tFh^ zTN-7QCR*w2$M@(n;Iu@%i@Z0y_2p=Q_?r=h3={JWn1)p8h{74o7#-6sv2^QRMjoXI%P%6Y|o*!n`|s z-aioX(usBssdruy{RbAF7di^KCve6qPYw%c9pe3G+rtp-9qc1-qwTHEpx$R zcW3eb|HzXn(|7-G=5CX|a8R3OJXSo<0QR63gLB7sisyOzSMu}tG4*>^w&yW5r0KOb z*U#~gFP{p^`?z(_tFE457VabRWz2p)WnAVvO-CNy#$Rvcg%f*D`=p8T>_3V;@t&+) zo5H)_FJyh5 zej+}ekK@nB|1Ow6r>xH3$Pd3MS*urme9)w+tol!|P6+Dx3H7Lq)dQ@XRS$fidK4eg zXTD8)RL5E3llh$u1B1I8e4gb4Ge17Y=#qGU&ZYQC?iO6(gD;=`QQ!3VSzhAGnzrl_ zo%~(uDZaH1p>)dwK1NsC@$d2-2p`sdv?svJg6PyS+~{?{`~X^4pzYs*mfpwAekkM1 znCEJ`K1^E77wrF2*zuFs8RDxC5bx!7s%Ur|{Q~R|U$skh8m^?L@adL0|BlW3z)SN! z_3azu<8@e{*TeE>xg_73W6GB+`4()2bskKzy^(=RNB7eF3%fe5zFdB$f z4wm915Bm0dNYuW!1MfL>n(*1ObK$nUyDft;PIJR8+F#x_CG#!Hz`F-iBWHG{ojl~7 zxg@Q6#HB`~eqJ_8>F3|Q|0OH8V`xVEH5D!a#(x5@!ujSq$mjEw&7b*Yx_LtIWIS_pC$j2 z_Nfvt_^FYc%Xuo&9;!WdZ_gFvznpn7W!gW{FSB=XKi*yLmwBzKU*<x*Jn<>Q6A5eV!}G1OMBeUFr>GT6c@nHwUA( zftgeU-?JZAziFpm3ezIeBcNNZ&iWBy$&USXw=ob03$ZPo2h>Z38pPBVXPHz!6?Q zBri5dOB?cYuHyK$yy&Vk`8GIDu^x)-wXw(d_EPu~{@40h#Q^Auu4LVO^Tt0ymsHc7 zedN*kORcHv&digM#kQH?IukmV#OIi@m)CSb^JfLSjqpO&ek=_A*DL>onyx$fy-fXc zL0nf-&s69#G_I#@U%!m6=kIT!+;r-h-g`Zl1@+XqdKU7_+3XYAzE#t@cV<)TW8glh zKVxudtY;P94nBJ)p(}{r;^=Alr#H2>gTw!XCT)IS)GaxF60fC*r%w%jJM`X5d;XjJ zi=4mP=vzRK(E*+pI6SBDtGx1oZ$cKmZ&o^T2Kq_1!cJ&#c>TLMw{kseukN1Tow>92 z4f55#KFHQ^EL@VZeccwWBupRsJtcZ)<%tbrIzNx?B%Mv2rq`v%^q!8^EU5Qj<*wS#n38#&CakqUfcwu!4D^V7l>fmWQb^1$oz)y~*D< zL^xGa)7n_}u{pCR|JxeqyX-Ni|20lDgQwscY~(<&s{|X_);ONPk&Gi}TI1a1d7bbn zcafKNdKj{IvW~Vj(y`bY!-G7!Yf*g;|F`=40tuU)!Uo_7s4Z6Uvp)6n+;wwIO4TSgl~}7!_UK+Juta7uL7J&Lw*yyl5)bCv|e#0 zO?AO@t@N>5^UBA06Ly=;Kfv$t()qIUpQ-FWAX8o-FRb_Rz9I7_wF5h%SK70$r`L9n z)@wUR>$M%ESv%M()LT1}{_2SGX#cA3OV>|5(w@CEz2Zz-uQ-#|E6$`D&cXhikO%Vk zWd~)cs*ME(SV1ZrXA1ijPEH`9`S(2*=ova&rq_T+xmwz$`{_Pxy$e8-g)D; z+1d|qSJ`OEXf?0|hkhO0OdTPfvag!SuRRCYH8BtBdwy9SejCBBN7`~`rB~XL)+=pE z>y@^o8ErYwQj{P5RI>UpFFWAvb(QI|{u457>oxz3=Y?K3UE_I1G-j1YtV%;9Tzn41$CLg2s z)wp>Ud-NA3Z(7V2jrsQD6Zk#r`n%iMx5)A2#gqxp6l)$>&Ab7e545=Zscb*bka#YY zx8HhL5XU@2^P^@z_r(TiaC93>9h%Gi1iPJj&l66%dsuNZWNVz4$~*#U(XoQS{ zzwxVIOKI99o5R^B4XMog-M8$GJHs?=+}rrJwrfsDAIJvUg-jk^AM8)rne*NIc`kBc z_RuWNV_r!yMvd+KBDkt;noDJ6+h{-3_ z5aPy^tlr`1Z}nVOX7yxLkB5J_G{4*c>Z~ixFSjBtw?VX|oYC@=Wtn@KYc&M)v^u^N z=Y4OO_os2*`Gt8q;=C)uyeE_!+&P7LtIG}do5H;BjPqVzn72O8JCD59H{hI%$=drl zD5vv8ha^q*2WejC<)umfN%J}{rXMu+@=uzpE75tDM?67)YCK73x4Aqsj&c3OLcCjJ zysN`Do-#P|ICLJ|OJ3dSkib1}u+c6m%sYFq(N6b07ue`_9PeS1y(eVzZlEB@A4Yw6#QzpwJ&uf*RP3oL!W z5UWpfZu_1Vf6wsWAB?{*@!z}S?`i(K$nG z@4x>Pe^>hNvf~W?82>##{vPSSpNzjp`0vB<_b~r`$xtgl#DA}dzsvmhx8v_bRu0GC zC8SF}lQo)PEYO;WU&~XzSPt<^I)9w}jQ$=@`4wNO#8$wkhCDGg0@}lD-_);hR(j~a&_nsY#9&=;L-&1A^xwhCb*aqD)Ghl#I&QJM-TqWXHu0Ty}$CA z5@38Ccoq5nO3U*m{FR%9R|9a=e|c?~8LVqD57yp;XFnjDfj!OGMZ27BqOivBW@m56 zma@Ioetul)&d^}rD&MY(*k;jL5$O72CHBQ9sNdh!035TkWdFJ|md-XwGN!=PU^PU~_xR@9N!W zcyWdZ_$Hgqr_(WsZ^MK8l9*HAU%l4gkWXpY`G?9HJElq&tA!8yW|7Bkc~ay_@Cx?U z3Z}se_94rM1U(LEO-%0$#BU$p(0La;?fqyCgW^7tZ}p{nh#wg9YL;*L)wDkLpY*Bs zk)@d1SX<~X_4j2;bMu~Nw;rMOsdmcoE}G3}v`+qsvC%{Oq{B$_=T2rw7EV7tb18E6 zKta0rLG!3~=p!HN2GV`mYU1~cW+mnDOguO8ZPOg;qfTZ;k1g=fZz(&Pu;?M2PZrK@ zZiXM+WKZX5d|jG@-J<$52NMko>mYuet3zu7s^cZ{tG@70^4#F&0~$YUeh=s^B1Q`rvm zpLq0#p}okuxXS#|bU?h`tNBq{G;?FrEVmxKNq*xg_`gePx@vUaOIuY&{a67HuNM92 zZ{e3NiEQ3u;j$Q?JR39IuYlh5_qOhlghsMDQ z(!)KKTE|mc_a=IWyGUslYhhDIvB!^fz1A%7eB21)uyXyNDbkNdSm!%g_Pb2MYoljy7M zURUWGy_x7PeKW_}?s-h#%&pqpx9Uh!mv9uHXuU#znpY*ZRRMpv@fZIGygxv=K&xb| zv$2^t>R~SA>rvjY9<@{B`@g-PzPI&_g`Ll_&!s2Wxp6drjeo1s)maEz{3v^1A$6#p zo?t%i-agQJ3Fl@wzBTpdFzVIs`Rc<``jGPN$o=mE+)};Z=KC)Xx1j$D zaeIaS^Lg{|@$&C*^=<5pcIrQa?fBB#>DsMuH$TLN+}+9O&dq+EQ*=sf6Z%Z+cD#?r zT@P^|%GU7xdz06vF3oWKMwFb%@?|XkOF14EjKY35e2Fhpo&1(d578F0GtsY?dV09J zBJ}0!W87S75H_XL!IW*yIyo=(2lPa*rU%`IR#q?8FG8E;yp)aVZZo!J$pQKZ_{7E@uM1Rs(3FMSc0K=?HBtqX^OW# ztb(^oBYzGnkvZYF$l>%fb+(~v4zJJM?|mfLTM(`}B>O`*p#QN=HGj3en8=R5kKw@6 z+Al~tab-21+B19YQ*Us__K_{v0GgY&Kf#=UG4fRKv^9Ljl77(TG`?-lJiyOcH3yCk zHF{ZL9?|S=Ny1rZ=6pK&=a`_MXxs*E9}_JY|1_>>y-w}k>Dukq>NJ-#T4`?Y-yzWa zVIFi5KF;op*Ke@*cX=EPX1sncTI&ycWKRB@1oB55yY=vfQo9>V_H&9o73s6L*#69d>>Vr$H^}neUmH4y<-MX<-Q1<#xe03>!rl& zybO-M8jtw7GQbw&!M!-L_@JD;HOkG+nY~ z!>bop3~0K<<|)8`8u<4EU+1JB0Dkod?p&uo@gch6t`r-)q#xuR)Pan;qz0Dg&#&w*2a%Pa~pbz3YMs?obPj!r< zj#1PxnmR^P$IGscUly+eo@t;Cy?tnOy7lDD?>WPuwJ+77y|mAlvWIXPdU+N1ORwek z4(zcF-?uv2HvFJ?9it=W4b*Xwt7G%YnV(UI;A?Iyxh?0+`u;0`eKmE=<#(Ry*y-x{ zp4D-9{X4cE*;(q|(2nhX>5Z{77&rAhJ~G=s`@(1Dq-T6-P5K((5a0abKh4Q5|JIuH zb@BJCm5Y6|^qhu+PpnBd#^32DJ~b!x{x7acuK@R%QJ?p#_B3C*)St~4@1T>ASMw=}_l)mm z)U+NbEpJ)I9ZwI?R{0N@b4)YdAx(Dy-Q&gy*&mPbmyP0gMAG90FO^9{8|4?RyxeA0 zmz!h#nlU?XZX4L0(uLApzCQVEd^pt}t@i#;#i2J^b$5BK?xP}~?mE!^HsNc!nRJ!$ zV^B|J)PDKJ{sc~1L+vR~k8q*G(Zcm~dK5Y-%s4Vf`dB*fl+m$I#J6!jdjQP`AX*NA z_KudR${Euv{;=x{r5~R4Qff@igBITv#=kQc|EDni`ds{>Fn&=k{&!(~LoU7}jK4J( z|9Tkz-dub;d+2Ga_=9oqA=bZ*7tklIsbTbEa_AM(B!?`fs#$|lOjwD_`USheCS zgSa6suB2?@{^OcjJFus?XK`C1rw(_sNYatF=bf!yqmDh&xsp{JJdrY)i`A8pZ z>6ZjB2pf!%gXY$bxniPta~W}`zr1Xl4^JYz?6TwME}MPj#M+XJ8V>c6#?&~X8H@|POW zZ*Hn=_Qd^!6<2-Ekh#@UYUxMbjPY^TojAAp-Eo}q?4KOhuk-_Fss6!Mzv9Dh)vYxQ zc-v?KePTM%2BY01>O2V?luvX8meCqGMt|U2pOA;SMVycRO6VHMrr}DTrs-4hf@}xj z)1f%}*5a-sjyf!kKDIb)2z>f+9DQwZ*b&x8DodYRTpe-XZ)NFwi>oJ&wpkoJU~!Ga z(Z?1CFIe0P?li>)4|N1>KH%pvck*UfPj6W!ep__YF6V>JVh=I4cFEVOy-4O08BlZR zUN=8bJO+InU&$H_vYah7z0}&lTWQt2l}6o(Us?H6%C9^Fz#I5!;G11dnrNNpuPx2( z*$vy{{ULhC#QKN6OVvfImyU|p26&G(j;HTaBaqQh2To|R`!G${kzP9jz4r1_&3m3t z;;MUyyQGIW#sgn=KXA8V2kGuv!%20EXTarEbifhh*W&3{;Z*DQjp-YG`6XpQC#-bvL*$vZL!wC;nxm%ONsa`CazVes?mmt7lF2Kq?f=pC5^f3=JDa5gvg z-3$PZ>OvP;9(YpyQS0d5F}kUBmgZ>Ja98x5joYLXGzN8?)MS03IdWF`kdHl(2Wxi^ z&derXs`7`AAhTTuh`-{_W!uU*W1;p}kLSIe65hPgcm<63zF*dr!6B(%ID(ISdE5^a z<0ZL<2KT#hC{J(FC;J2ciFAO*4s_k%)XR;$8(z@Aq*?!>dqtaM+|~F8tmpk&!eC#H zz9db3slM*kpQJ5g-0jw%B@^#YcoUos2u8_Z`pWf@>Qh?>bI!>3k=m(#qurc~KB?mZ zqyLMkx*?hJI~(oIcJl+3ls_-|Gnz?;1P7TqoiegXk-5CH7G8%m@YclP^}+tjo!Wmn zkU6~F!Jm|ooNE4TJ`1N)D(GMB^B=i7-g9uHWFCLVxvVu>KIWK)4|7WFn`!BCw^r-E z{ToE;+Z1as+r4ay=U9V@Jl*v@C0bn(e%}|Y(I|a&ypK$}O*qWJ=USN_kvR)obO&z| zcixE1MB=K$xa&t`#uN9JFmB0+Oa*bam(#<$bwp+~ai%}-uCaBbTIZwP&o}lD>sIA0 z>W3{eg3s{r-HL<8havS z;Z*4A;TX+;djOhsFB7*ZZ!b+S@Ac~~!b>=9^6R+Bfci@G{DIzR;al}-Zk0EO#`l-J z{ZbmQ4{Hqxxwbt~Aw2E#(pt#ZfM;hl@$vYvHygQoz!+oIM$zE`H(ozS-P#LdekaoP zW}jp#3VuKMPxEBWk>@&oLN4H)-uG;YZV}}= zl;7DBv)nqo>7vlb_w4x0R`SWm_i=u+v`2mb`Du}P=dTUUX-c-}8{wDuU1kRR#-X{+ z1nout7~P#uwAuBe;zTR$ZP)tYT+!bA9I-qJ_k{L@weBO@voHEu(a4uiiT?7J0b@V; zbU%?#%k@Lg`>#Xait!nBSib{jm%}ryaaLzby?j8AZfVfg`i*j0LpzDK0O$Y6omENw zjyFV?ZuL9d#5dY^p|i`aW=6Y`$P$zt?x7b+`K67-M(s ziPz|B`NCgSSn>uw>Ob8ZJ5TG5j&B~L9)HeS`+Xih(e3x44eC$jH~%x=-@3$gjNOJ+)Em zv65rSr0^Flbmn|7Y0?Xqi$MZc<>@)F-c%QvvlD|w; z5*<$3#(G;`+un9&X5WaoZI9BnFCInPUQvE)TVO9bU2Hlt=8waPPVS@KUXG*JqO&yK zsJ*tfN8f5~@2}8+@80PU*LxUVI1FF%7mNi#y`Hc9c<1>`^3oT7eVqF9`6;(w*v4t_ zG<{9I$nvx5E8$?gc8O%8rt6Q;OYxzd5xyl{*8ko)x5Ar4k4NA1oBs17gKBomX3FQ+ zQ)AnGn(lR|>1tc7vH0FOyMayrnx6D>jIIChH1#9AY<4htKO=rbj?_Qkd8otBi})_n zUQ6=WJ}vC+lAh9>Pu|)`-r>|y;p$MF(UE?4eu{)2r8^cmJ#@ABM*TCWCZqjZqDS&B zZ#X8PE93E#@c{h{zIpBYBlVW>r#_XAl+Le2PAfYnWHze~C)b7LenXi*@ZP1$rPWW! zh^-%EZ#-n>YP$YOzi#Xp2hB8Y%qod(BjsO3x~KCV;QgnAC!dLAVmsfWyL5%dm3@?{ zgpY3*T+yHSJl#n8dz41}3l5g(8QYS9%}_ue*)`!J z=IvHzj8B-)>`?F?>i9)%l5XDzerm@%RZjBF_vwT?=ug=^vf~*K50s6_-3y;@hX!pa z)%x&Y&TaRzbK7BG?@IK3yayfJridQ0a|at;Sg)>%GC!wo)yaOr)xFnw1@9fY^}>R& zIIOdfPi~}N8(m*~z|}dQ`uw~%$ZzM{-%0-YF8>s~1Hok$NeMotvX)IZR2mg8lu zyFYz`@sh!q4vZ@uj5=T_uWVoA1@f*H4EU<1Jk~Mdqv*OQQ>nek8`GIjlV0KWOXQ_} zB%Kj&BZEdq?LYVF4+|zdsXA;NG~K{@o%TwbZBll?-%n+>iuTxIw}>b7&iXYf4=-fl zflaPmO8XA&Qv22g?HfeT(&HwHkTdP;P!cR%R@TwOz)*?SUZQ# z;MpCP;HY0nw|*2|;gdpm5%XlD74JpuQ@h<*;PI5)Yi#i4{JcBG{Vgh&O2zQiw!%Eh zTTYvN#^$cFPd_(2wom1s6us;&H1atcH=fr4)9z=}nT2mh$+<+; z<>RvhVtGp9;H?zCt>!LWjfhkbhn`P`i8&>2ow=*-4m;Q4X3S!LmEoo^`gDM{aG z48MmwDStK$zqR7-4CC;zQcvqFuTSy4W}R%CE^)eXyR&PA`w8@s+M;^9(=hiwYH0Tz zDNV`7dnUIhly%=bSod9vUR)J*?OVn@iMJqscObiq`CZHJLVj2Adjr4A_?^e^E&R^q zcW$glx%`~ghny$Ol=IPL5I{qDR=c|o;`?}l7 z{{!+HjTbWqaCZZ?5r(%9RxYZ_d=gEoEM!(k7B;Oug;*GBkL>RaSAkrBcMXDo0==iuBksAyBl z(uPg`oRn|Rx~j}wz}oKbmU4ZYrmd;lVjAA_n~ihs5iZo*;Lj|neS9Z$1K;<0d}Uvd zb}Mih(^_Od`Fz4>CHaKwG>22l!}K_9tjb(Z*&27&GDl-@`d|*H8-X*Sw{~r*%3R)C zyB^EIxZ)_Nml{B})s?NZyqKjD8SY3PMk@mjO*N5bd1GRiYn_1sa@ zy2*_vZ8PylDQ@4+X_Pt7`WN|>9csR7@}4TVd@D_Qjdh|fy>W#<)bxPO!-2B{yG`&u z|5tv|50-yD`LQ$Hd@s>q;e79&ne=p6LCcGNlJtbk>Eba$I&Ze}%@UieA9( z%!`|SY%WeVjo#%}9@go)mh!!y@BKrX<}7vP!aS{jv=)-@ac6`aC6=7(p@GP+`e znH}M51=(b>9a5}wVFO&7%R?M%U4E_}wsVtvTPT;8HK_u;k~Gnax@CulFg&foxMqL% z3$V;D56_IH5Axob!f$bSeF=6RzBJ(4T(=4RRz^S3mazlk->9sn>oxd8ariHsUm`a@ zYS6eirN;axHtw)C@HqKx|F0_toeYoAuTuEFmVc8$cMfaLCx0dP7=2o87H?`VoaPRn zJPuyvH?J+FIUk$s9rg^{oO_qISAnO#+7!%Db?=qUElb0*)Z3@od!wwmr<_e8+tm7% zvtHSPa}~fpU;Sfw=pUWC{5|~>mf7KWA9W?k#jT?X zMv}(*tJYzYJD9>Wd_ziWbGBDrIo4&BMqe6_Cb)7Y$Ic>?COjBlT6=2WFT(b5erkKz zKJadEH2$imi<5~(E}!nI32C8la_5+%weGbMecSz4l=1uSd25BGo*c4#+xb~t*HrZUWPd!%Q4&OBGIr&C=I zbYd@v=^o!_Mkn?_^hzhvtZw#8{O{7K1DnI8H;tsHl~qQ7J_ zU?;pd6d7im)!cuVw^azo?*z`bcLKG>b1M1lOgCwo&&iKasksMtE9p%4i3RE47oX_{ z*S*wxEa|F4Wvg|jo49Z-$kw1ppG`j9!C>Oy;D(26T$f!@p)$buA-~G|S@LG9m@`yGT}P^zGfW0g@bdPa<_vEbtT-+uTGXAK z%oz^2G~wXOXwL9@PzHTx_d?T7n=`x`q-lPnc+DApLA>@a2?q~LbA~;NugZ_toZ*E4 zP8!|`=M3Lh+H`DKd|tXITyqiXc~<>zZRFe6p*ce<`IVNvBA7D}hSo1?&hUBYmCPAD zezZ%tYtFD$IEr@2P=~Og@NIaER$71uZF<NH~248FeXSXJ@722)ig%7sD;q?nx zpP0{?lR5xv5zuuoemTvhFF!KFh`F2q|Wn^!(llQ7yi@yQk^KM^Lkawrx_Cr@h zcGk+@-xK09Yn{pjYlnf2wq@toZOuCuZfnvRsV##tf#0t+w5x)BL6#3+6?WV7pGQ#> z`P>^2p(A?bsB-5cynD6seZ8cXv` z+AuS}4RvaR>G9YH3Gey&i^-X{J$9)+e2wxE{j2zju~t4uH|`~GF_w*09%NLq zxyiv)doH6sjd7LI3EQeN-vzGxnfqwh(qkFVXs60>mM*@N5nZ-~c$0C$)x?K>bouw$ zd)!q^d@c2Q|Dfdu7tu^MKKm^5_VQ}nb7Sv;?|MHaw6SueSszhe{(JRNv&jr-#7(LGhZ_kVKzC9#MJeB$K^G zrJ3J@4`q(W_tToUx0rJlet%)Mit`==oA#7x4x{_@XN+duwsD)Uld-B*<^BCg`o>1G zy}h>fqdm;--f~~>OL4u1tNiC#_C0s2Lo$TU*Zc{ZX&x=Qc8#spJYqrTY!-Ri*xU8n@G?f#-3odv( z!MCS%mEq4mbo$=o81a_9)-Scz%N-q?FWqu(=EIcP;(aBg4dXkh8=dk1-@fj0AExd> zgxjQ7T-_V__H}El5$*EzOR$C%=v}Y3vQEY;q<`5%`?TQGcJt9tPqX_LpQz2DPCing zuZ%Y}M};@B-&&=&WQ+cC20|p9f4~=)kXO1(em23=Jj2!mprhF;=pXs-HI8_lXX|z71Nhlw zO1eS1JI{vU+tcHV;2^ss?7LG$GuKBw;dPS3OEBVf_CSZsl+5?vdbl*cQ#_>UF1HUe zrff$-B12}6eGW!6Eavf_^DSOczZ>i!Ia|(u({K4SQB$N3PY1a%bcBbEbu$j-vOQt zZtRlpHU*z@$r~bAsVB5Uxn`u{1Kj?6m{K1JKM-pNm}L5bmlk5z?3Z@0@w$-IA;) zaZVGONbf11*;;HxZ0dAiJ zwH*4NQe2o8?q(kL)i-s?cgiB^!tOc-}n}8X4}c81-|ln+j5s|J?gqob&~ef zU!Xm|_q)2RZIgfjKWn}g_9rk7EOLC@N?p=-@xHQ>jQBIzR~F7m(980*YyCia9bPw^ zV=Qg5d#r>9G}YK1@mAEu`gSz(d2OE;?SG8&YOlA!?*j+3!AVoQolohR%AZzcZbHWI z5ssw&yTPpK`V-%3U%tONSQl|P%H|S2g*-QzKB<9D8f&!XFMa)6tAqU1)9lAj!lIuW zKR1W_$VIbC;GF5;2%mU74CX{V=oP)2^AvTlzf*|=qn80}v@v``CE`76$zOr?uO8kmW zzBG=jPsO(>{C&E=OZ?a2#uo8X{4Sr?r53QCePL&Lz&o>?d@1b?+G;XS+rCS?{{0Ve zAF@~5Wv2K-=6-g-=LTJ^#Sn< zG%S=0-~V0~7#oI3HZJTXe{(_Kir?h}5Z~5d3+lJZ@vC6Qx+0k01iBW#(46+gQD#Ls z^D6JJLxz&Ok(j^AN1!_72Qr<7E|X2d+{o^A+CkZ@bcw%DNo58%wU*!2w5R&6@=vR5 z{#+~2TRU^U0O?3uWA)>me@~EaqYXMzFjGUGe?)bg-9%g&->#kmWwSQz=_uLqhF?3B z-L@0_^89K$bN3!$YaWyitiERwcb$&j#{BqTHMU4Kw#ZiSNwwFuZgO;iR(q~QFL1~3 zHtKF+?5I7NZ|O7BqwVFU&z{>kdYkZV-gyJ}DRAc1t+jQ4N9vU5_?DfY-nMlyXN)<2 zl;oq&n<>Xzz$Z2DTQD^>BsxA-GV0vseWy-EZ%s{&(D%r5(Z^Gt+OS~i7e29I>XT0_ znELszFPQqo&IMB+e`&$g$0)bu4-2M#mHf{PUpVzERSTzndGf-kUz)ja>eF)dcl7va^rb(t$1M0rYL@fR=Qbe*3qihv>JoB|S{P{aVr^^jp%B z9;x4`B|S#J!&=gn`W@Pm9|dhYTexfaiTraMn>SXRk!dbttl>W9#qja1Z-}QyBR30swJjc3e(uT5yL+f$Ow>5K z9eC09x>JA6Z(sJY@_vInP&v_GX)lVpbX9)M^jYp5erZ;5raM?TtXjf@9QEZAMYD)$+?HQHJkPATw($7y z+TwZott-fL6!cp7MU@HY75b+bNBp};`>Jes9=7IL^54D&tXH@8`%>g#B{&o5=fmVcw&l+ln6wmmJ-W97UNC&li*_=)(~IfB8{< zTYbNL;RVIVJnZ9M2;vH5CWN{7$N4b1%Mo9@yC<|X_Q}Xkr}_3Szotf5^VL*hE3;R< z|AfpR4q|WdC%wVmbLJuoI}|!bAdiRnA~0WZFc)h7ANeEZW=|Wucr9gC-~(6? zl-)tupSZH`G<nB?`=slF&oa{=2Kfm+s>`m8wqk0P^SI_*; z8-seDqMm16JuJj?o^Et!>YwF@;dhDF#7A|euFyBX=jwZ;!PCAWn0j-4`yZ_Fl4kj5=JvH^Rk!B4;G%ty z_O1PY)LG|zfP0u1CGniuxySoHtt@$oYj?hgZum?u#E1O&9IQR0S^nw0Qce!;&;a4VJhS%(^6w-d@cq&e#UZrVIC~1E0U(zm5W71qb+Vf)jDlCtqk={(v zcgF=bzdn$i%ve1+xBeXFy`Q`u=Q3!jIj7-vK@at7Z>ZjtAFPRtK-4v^u_0tWVp|}BYT%OKwd982odnY>3!|*zTG^-1rcp{$$6CDN~(qXzkPU``cl8+Nk zvo!)1As>nc#Xev8!%})P6TRrq1153EA+mIB(jMib?lvb2Nj~(*J^n5&gBRDIl=J$W zd&au8#qUce&E@a4Emog+lQKI(9MDJd&&kKBeE?chw7k+&(BZStK=Fbf_g!Gm1inP= zz0o>>-kCRFJ3cgxMcOCaqpjp$!BAk&+B2E z{wLkq&pPm|etutm4u*W(?GA^+x>c?)PP*CG)jOW z6&pgmvU}T^H|-^_lL2k9V|VHg~GYAFH~msjC{E zuy?6_$y1NZrdrdB` zuQMpGxjD1?yPb73@afvCU+UebU;5-Ve&20%1pe#-TV4J8#5)V@>4H3=?fugtc~!60 z^(w3#z0aq7zIPYo^R!tM(80%jHHa&mAE+He=k(nUIU97UhHUP^>+rdcUkrPk`;Zy;l1;Yj=y~V?Qb%F>iRba|B5%6{}XRA|BFYN zUwh#PzWeBKSKix-~@kj0)J9pWR(;ryMTC(m`u5Mg9wOZ?tjMKDrn(aj=UoH9A zi$1KH^4Mh4)He2O#ryFl#COjN&#@&X)ATDGEl(9;^qyPCOK7NZeUY=xp3&WK^l{$& zU}k)Oq;$mp|At8SH$={4ZfZ8vc=6=_oi{|VfqQ)~#B9MC^nW{VQ-r=7%DkC3L{5T_ zj89bVA9y3=x5$d@U;Kp08zIBtC&5f`Ts>xPwlp;{@2!xM;H?m7XtX>WzRfY~C5tO~ zC&c0k-UzX{g7-lzuHbDDiz|2+#NrCx1hKgG+_tvAe@tXMV?6QLQ zKy;Q~Zz=Tl7Kl$@#(n6$y#tcu>EjKMB+oekoVpk%|L#bVr;oQkl04k=p5W8i3!FaQ z{LtKCKkcqv-Sc}NUtzA1XJ_tq=P5OIrXKtAV8ogFA0y-J`=9f5etmwc z8+^2f(XZ2J4vBx7eaeoHN#|{X2hsy__4smrewo*Pr)y0Pt(_|G{K>%_T)J4}pT>p6 zCmhVdv;NIN@}-r}&GU7?b8_Eg=|IlVp2!*6lelAUK*rjgjH~Peo)mSpPhcL)8KmTX zd!03!5sq*4$7uJq$^`nFbL$$L^4?bQ@!{JlJ}!J)Wh`UmV#Z4UwuD@x}_UItDR5^R5cxpvC3Au|ixe`Rt7q7q<6N z!Z%iecza_--(!NePxO6B0E2Me8!MzQBhKDfap7)ntOW7N8!Lhfos&0KP72;wAs5b z{cuNT&d`g_<^GM8N$7(MI77r=^2Q2b#pS)R;^PY6SW!H_SN1WMA5i!#%7t&N495n- z?^nI!_~cEM{luwXlQ&i-M6Df{gC~E<8!Lnrm-ohsj|<;eA#OkGC#rux;RCE)@V-F& z#tQM_H?leyI||ywG+~_}rW1G@?JlCu38D}AL|0%Lt$}0o2fpgX2vh=mZ%_j~VEsj37xH{s% z-{RoJ&wpkoJU~!Ga(Z?1CFIe0P`K)Y@d5>p;D;OUSub;=h@^^8k;a7w3-XQ#&AiO^a|2_!65rqF7g#Q|Z{~m<<1${Iy2$u)pIhW@6;ax%a zJwf=UAbfKWJ~n`Jd=Ne{2#*TFrv~A1LHM*FJTVBL8HCRb!siF!3xn`mgYcz6`0YWs zHVD5X2+s+^?+U{23Bn74@J&JZ<{-Q@2(JvnYl86YLHK<^_ya-s-XQ#;Abfui{#X!x zFbMy15Pl>GKN^G|55ivv!Yx7g%R%^SLHL_N_+NtXcY^SDgYXZ6@bf|VCqZ~m5dL`( z{$&t;H4bAF+rK)k$#q`ZWjcL9%$F5W#ZUd8rT?u+dNi}|^8JgXr;4N>QzU(8k@ORa zq@P?Q{gfi<6-CnDQY1ZHB>nUv>1P*7Kd(so)FSB@7fG)vlK!?L>9dNYUs)vmokh~G zEs}nHk@Opjq%SIxzNAQcLy`35Mbd9AlD@7;`g@C{zrRR&Q<3zIMbbAFN&iTZ^vy-m zA1adm$s*~WDU!aWNc!iCq&F8yf4WHeGey#$Et0;iNcz7ON#9;1{d+~ycNIy0p-B3R zMbdv-B>fjf(tlkfy{$<4YemwtMbdv;B>iBK^gs5VK12JyI%jsB&F|12#(zuyTaonW zZGD&TUnD(MB>k8o=|hX8pHL+IFFZrrx!^-yGZ(ZMbf7hNx!&A zdQFk^xAmTW3GD!>hO^h2!D$3X9z33l=K>$_VvVFeB$9FKmA4& zEg}5cNr#WTm+&IOeu@NgIY2;mVfyovBA7v4ztL>F!%T<*f} zC;ZjZ;=0t1!!Epz@Y61QE8&AKyqxeuF5E!4-G!GBe#M0s5&oeI-$?jI7rvhG^)9R$ z)n4{+M$N=8!$GHipC`Q3g++(&y8NQU92YM-w77WD;n4WFtmyE%3yTgPa(P6D_qnj> zaI%AQ_OmR&y8LSI2`*mkJ<)~L-VrXW_WmRt*QNG;*M-&IZ@RGBTjyY^y)V1?YY9K- z!tW&fAs4=q@T)HWEW+P%@oyvS+fqaLT`vA&!nH0umGI-P?0JMc*tDsCXA}Om3%?DX z{i+LB5dWkLpF;RkE_^cKkGk*)gzs_Tp@eU9;bRClx^Rl{d>8Id_!<|E2w&#He}lHu zT)30)87}-s!j&$3knji>{w?7$7tRts%*9gvpZKJlk2Gz0-Ni5Ag3Om)_??7*?83*8 z=VJ~|itxQI+@J797mf(u=fZ!Z>;f0=B<%D5k?@CIyy`EZ{w>6PvH-7A;$I|wtLujs z2>*)bYxOmyx3oD|kvu{4;riUtA@Q%hMx74qzSW>@YRTRy*_0}sI*HtWDwQ}8>Rm;~_+`MMhii-N> zL@cR4y<+96imPv0I?BfcW3>YGoyFZt*E~+x@_gT`ZX1IuDWdv zz*p4oZTdpAYUM%&7cOq7TX{=;borY4`gMxDMS+Ejg<&+Wu5n%C%3CUKUX?3Xb^h6P zQ*N${*50{xUHyurDQCx_iWT(&vUt^Ps?mYD?8=2#&3MP`w_i5*+Gs`H%Ep`P*RDIW zetC4+nl-D|Tv%~)UE}ilB^B#dRV-dpU$?Hl;*Q3ZOIF=+X2tBa6%@U8#+=J9yA}vD zubvArZf;yzzhru}u5sP+`Un!OYls@^8*gb?7p-VqxiF4U$VEc8=bY3(i zIzPG~ni@@u>Y|&X#nFHimGm&dUM^)i=(P@>d&1v`KGAq zybGpGIe(hsFF5CdbE0?5y|(_n>#kaLd;N;~mFuG0r`AkAn+{yKh9);c?23g8X~vR; zD=(~Ab=$g%RX0PE6|2_VSssmFzMKk!oCWC(>(<@6aBcmX+X2$J^7gvrjY}$SUA1Oi z#rP%V(b`pum(fEvL7QlKBeY)`-Jk;VE8;Kp)2fvfH(Yq({0qk~sYoxmt#0`lYuD8+ zURGTm)h%9Jf9tv^SHZ&D&+VZO16XnUxkm+a{aw-eakR#OuBl&p>#CJ&>&IPqPIUS> zs$F;6+Hn`s;w5}GK#uw~`aXT!O{qXIN%h&P0TlBxxx2@7o)#PaxMGb4}Z;sZkSsYz5 zE*dvpRjgZcTYY2@$6YusnmzaGX#CmdEjeTS+2_XpyH=qKlmDGL{_M$1-a||7tgl-$ z4$7TAZUr*d5C@mkfvn;h5FMA=xbn7jv{SKb>ygJLE}qKAFS#pPu{I)F%owRq1PEGl zhW&T#cxwJ%?7aFsHmu@s6!n}GBQdmDoQFUDo#;RQBhe@Q9a*#t$XvL*7^OfbN!#^ zdah@vweS5|@B7~Ck;U(|x_ln1m%6yab)h}SZzXT55B<=AK}w+SA^`vA z+Pz-8O{9C>Id)s2*Xk7O>=@ z%#L&zTpc+WNL{(4q8vL~lgB4K81~(M6nF77$`fsFgE&8WnjJHRel9Om!ZiJw(J~9s z_vp%HCp^s6bClsap!SSZ!!*G@L;8>&=U*bP&nKWk*kgy@voX~B9d@tq zTl0LP!0xx&tbR-&7~kZDISOcyv(hyVyIfp9ysb`)&7NZ|ws=YB!|E+(7-a7ATi6#$ zQS*ll9Y_>

5ZgW@8M@^I}E<4c_*lyRA=GLMlI?v$-W@STwb<~rzx zJ6!pWY=<8syJ}lLk?qDERX0YenKG0obnBohbk|Fu(4k6L^mc&2JcJy!HE`JB+5`s; zWsf}c7J@L8rfQ?+WB8y3slT;=s?X7Eik9Ln_V~?S`{qKZ-)Bzq z*j?+^rJAFVs|<2aUxd)?LWk1^3&Njop6OKnVgrU%OccmPHIVbozT$#xw^P<6ml{UA znMRUBX`Z5b9)o|)sIN+efFGLbIh;zOnI4ko$Q5f>EL+CGTkY8%u{8~xi))>|)t#G5 zIClu=ip3`;rzK_lIen=rHs4?1Jh;@wWw&=y;?6e|y!3Kv7EeDU;7y8_XR5xv< zNRsBRO(aU|4BB7qHuIGWvM+7sbUWrz7Y0;ku~SzS~QS&mPvpp}5j>NKw- z%hqd_R=+G~MS;gDjc8;|Ra=e`dn?Q(is;C(n^DEVL4d=|88ESF>Rl&I%!8Ztmbt&z zvdvL2KAbWXCbr+gZ69{RJ zF1PZfNeUB$UC{k6uwwXEN#zm6$W}n2(B^Q9EttvOFysrpnCdXCVufWz6A;-}7+l$2 z%zjQ7v}m7bOD+#Cl1526VG39x)&iK!UTI9p1Ex}pkbbwwnkKTcL{_%Q$`x5Ik>wRx zevwrOv$sgvzGNc8PV|YyRU&4hv7XBWEQoFcI>58C&LG+g&; z_ZE>1OLiT?R=Hgw19tVf&_qUlp;+d1h;^7{eWa4Y(*x_S7mSu=-HZUhpCn0_S$Fark}96$x@uT$>rXPg&-EAun1uHA^SpyUnEOo za-H3oiy~#}B&*BqLf>+VRJ1-+ZHXPGL@upz$&RBXvB)iS$&rWhQjJW&PIG%jHZD<4 z5(4IvBhQ6fkdyE5<4zZPk<1Qb1%@OITA2J{mSv->d9ZqMqY0yZ&~&dG1-^WTM@(~I ze#5+*qee`5K4|?&k`|CWBP&Rqz!H)tx-}$0w}>!bk~J`77qJJ5U}KOy4r3#6$S@m2 zpVn!y5Dk-XxW$Cn%6rIi8uS{npa#7TYe{j5gO;OiPr+Xmh~`iZOL}0 zJ&y)pwJUS5AI+xNE~Ds6ZE`PbSOV;r(fGd9i9MoW-XMMA9`qGS%ahzrj2;;Y(^jur z=f~)jH!b?C=(W*F!zcQ49Zu&mGz?#Usukl;K4Qg-j5X0~F`hg8=fWseN#ij#E{jv= zG@7764K8l%EAm{Jm0fvqmPciL@z_xC)0MyuSnsfJjTcs%EzyrkWWx}R$39V^Js!)| zJZ$tKrpk7`^c5K=p>W(~{fgpktgsYm`}tPdi&4Yj7e$vuV;R&IPsY|d>|!`=OJE|p zScOQiIeI;9wc%!{e1yY?^>0zJ80ia(Q`XfhYiCL2V`(i%_dt`~UlB0bCtOYPA%ZyZAyvShq1v}74Xi8YCBEnW7+X1>d zDAqE}rWtP1^TB1%jgyA2965d`;aWDLVM?u~3B(S=3AUNC0DY9jQn}rEaItIcId&|0 z4o!1NIsdtYWINeS8SYJXQtC)0oFf;+BUCpc3L8YIJ{fHCHVn(ajn&pNY}1a+R`;c= z!^m>Zio*w^1rM(br6POv@cCiHJt)4k1rCio=I^7+K$~=+%GNEJj=%#a`6D2Lbbe+`Cv19mEBc1!jo3?=a2~pBk+*hjp2oC zG`lSsGbyUU`q9@1BQhEhm&RljdDwDdM07R6B^LT&hGN$a%Y@=o+N#9L2*cx2>^ESD z#eEv7J(WrgIcx3IrsH7#&`PeKp@YJB*e;9Q}q=beP*pfrqpM^8DR~K!Jz91`0V$8q4&Bn#7zN{fvJs)H=Bte$@~IM`z=l%qG6KL{Hh4QpfUP@&WUj5D-f13hCp!$UwDI@M57UQDTY zOrk^*wy?0Or=>hhK$?WCl)^l;>O5E=@~IN_pb@sNjOlgRy=$#DN1>{YzRr;O zBx5gDn)dpoCl71ODu&m()i+dI>9-Wd_zHRurgEg-P1|xR%tfcF(lnAv&lIaK1vR_~ zj1h&kqlc`wW1ztlIMQG45UMDHDV@bM4_q`@)(YB%m2pU~XbMYc+fr3(m9=P{?0u-_ zDu-+EeAvC`p^fx8s6m2t9%~MDKzYvH0p4msD1B+%7`ktYaA|m300u3hsz`$kBZHpr z(bk>s;I;tsmsSC|3A7qfHP`iV@YSA+vEG+TZ(7Lg&mFcT(mk+rpq)M+)kqGnc)|^H z)0Q}7U1M8`7JMw!kfFS&_YW-CKIy)gv0AU2K^f zn-CKpmoR66*gR(eo$6jdu~{q`3O|X%5m&}bP(ksCmoRTGdEpeEA|W0 z8R_{_;`LiNcn!-7F1D;*cOGhY#w8;YCQQ@qhG|sj(58SMO;v*`h-xfR)lfv$*mAJP zh#2vRk+4A523_2|xl|E3{hqOA%_Xrh7i^xtKvxfyK76rBHCn>uBZ$T&jD7H>h|~35ELyHfRYliaZo&sHE~GE!xB(kRpi5KDj_zG z+JU+OBUDMMo-bR`;N2M!9vd_A?yaE&gQan{V5*GlZo~9UoeozsBEJ#Ix!^xeK2+Kv z*EU$SKhA;byk@BPY#Gru*Tjx&XlRM(61uvUP$wEn%DwBb&HAiirI3P##y4t^dZCAe z3|$^Ij-h5dGCjo_Y?*qOl}Seyqi3KR`%nt~D*v!}$}bs@w=nTqy~We!%^j|bLsClB zz?assxXZGA9TG{l6}iAic88BbrK%WEz#poKmcA(Ik0bpLvl_Zgy2HO;w-u`~AHu@= zkL4y|i_J^1^r7q!UC@ON-+v;dowayN!ddb@%C3GU+K@gr9ya~p9UamdCHejOA5!EW zMNV2Nw}SL-Ai5g71-dAi_Pk@}%$*lIf5GW-3lo1ug_n&WouFKMMIg)wETDJPn$i>g4ajrJv)m9 z|JJj>zpp0n@5V`qe=l)_q_BhiDrR53Re0{4#Uyw(5pt$?TkjYj>5l$%=Kb6KXLsILm-TkZk6YHRIkTeR`8hQ$ryA?_{^ie4e><-0`Q_K$ z`}&%k7e?hxpB{eY*ngcGcjA;;0|n8mj+zj+F2gt0zH97-TW0OsbJ>;A-s|t^dHT+k zxyRLbu35R~t~JiRlY6dzHFDJ7-?(!A5eGa~7ls}4O!eeY=jkV;ZcRD$vBtXCe~zEJ z@a+3LR;RsCx#3&)to$!8--&ho-9_O=E!#s4drN=%ZFkw{&h+XZ?)q`}H$|JDAG#Il zc0omg-Zvf%HxBMdxJhvIu5Soj7+fga(QpxPCb)@k_}wFM6dZmJl-_L@^i4rH9R7_Z zaSYrTxKVKQE-^XEkC@7$^pqdnH+t=u-itpSj`E=NQ{d<|^f)*wdotWMkM+xQ9{5!K&Pm$lt`~6 zdBX3ifRaX*29`?T9d171^Z~uR0LRno5vuYH48B@2HzvlMwo<-DLIcoD_3DWq^XzQ< z7V_}nB|2*XB3N_s?K9cT!Fz=6nK|S$^8J%dcDrXLwo$ggUoAZK6f@=94KuMw!@4cr zJVU;ua%k)Lf4#p@yP|eVZI^~cVrrMvR`F*-2Qi?Jn1oq+W#FxAL&bKZD4Bq7N)RCn zF3LcyU$H#lL(fbhN~aq|-F$re0@=6#aUjD{KqL7t#5XjMfki@eLbjmM_d>RvCBy(^ z{}S}Q8AcJa9Cd`GQpSr%MLU;Qp$S7cI&t{XBE)~R(8|#1@LNFIMqE(D@j{C|UKlg+ zTNu3}H$|Cya^)qtoFB$xY3(gh+@H zrt$a(@KWXqQ}{d~JoANVF|MO#p)g_jBl^}uFPTCM&lDm*S7?}ze*vDSfa ziabK=_uyNDLSc;EB82}E5mI`o(CRJ~qN7|GY&Q#$e2>sF?-3$UBMjpk(MO*XTE}zv zcBxeuqF;f|UK3jCeqo4w1NG<@nyp)imXAb8->0bmFT#)@w2(5~xhj*U^_etLF-p_g zMr+0h{4h_&Sk2HkPScXd<41xgYsSXOnutD8GjvVYM8jNdRPsDcw9L~CnfPU3+d@r? zjn_oXV$B$Rh89wBh9)u-G|iu&iLNBgFn)z5qOvqY(*?+DqZZP;Q4?WSEu_n;iRx@E zWU5UQ-EK|G@@S&WqiO9VHzQB4CR)9k7Ut7LvQN`WAjkh5W!|od#(TBV9eBjt{HSJ# zX~U0Xy{Z{gUemnkpqet(6j=s7g7T zWeiJvvWma3z+9I2bX78a<4gYdBv(p&$|@y&pHBYt?Jr4unkywfag`FE=t{YmC4HYx z@#vd%lK5m-$^@2)ESIoMVwucxDa&On@rf_~!U8EQSFk*rx1qUdVD2ODD?$mM)fVmiW{de_?^mEWIp!Ed4ACS#Dvum1PmjVwM-NyqM)S zmY1-^XUzBu3;cy;faPT@FK2lL%k3<$WVwUoUs;y0yo%-3EU#f%%JOe4uVuNDWf{xs zSYFTa2A1V4Z)ABB%bQtNu>3pATUg#IWlTT{91{4XO?I#_5h^=am}rSG%Fr+l*M;iW z_-;Jb zY!0-HR+s*S)Q4IAF2>=)XB|s=GONfq#=J-zCIT$mSoX8bY*pb6EF)ebR(}clNl*qg z5Y#|W13?W0H4xN5Py;~?1T_%UKu`lg4Fok1)Id-JK@9{o5Y#|W13?W0H4xN5Py;~? z1T_%UKu`lg4Fok1)Id-JK@9{o5Y#|W13?W0H4xN5Py;~?1T_%UKu`lg4Fok1)Id-J zK@9{o5Y#|W13?W0H4xN5Py_!DX+XoZ(zmMm_p17(N6YVBJv#nJX+{jnOYnD{Z=9yS z>zo}=GDiMe(4jb^?`$dFZwpm;ql}jTAN6mmQ~2;dRgW-HHHqSz2ZK_YMzV*Q(wpEe zjaB)7Dz76<%;)lQxn65I{2Jy&=Ephy99i!$v6y8t%T$)AM{CBJ`b%1*uKQql0sHInFU$|Ke4k}MOY>r-UwKcF zwfaui%%upiONNJupI9zEV`O~wJ#YG8e9Gr=?QD_M_fWp+dnliwbb}f23B{zmOfRbI z{G)ajf_Nj_S=fKsE)tOA;oGUYjxfv}!-{=F{s7$&}A zdGwj;IuGW>?61r9%zAv+LE$yb&$0Z3sbvefe}W&YUAL8-tSC&We_}9a7G0XAMD*hCfCvo}|mglhC!18*Q z@3Xvs$J0$LuVA^8Wh2XWmS1vxK4dn{SLIJ*IgjN+mI*A^vpk>W(=5CBI(}n5ae>M& znq?x(G?qCmFJifq_P>wuRi2)YE)UKxjK}$!|BzpmjAxd$r$a#Me~3em zqVk5g>X?z-Ca=#dHX;`NMLa{{33PhczZ|~54Km)L^OITTpKwUpjqKC*AM#J*{LNCI zhvwh#AM%fr=?*Pluh7k$f04Wohvwfe^>t|ajhsQ5Ojkup8u54U1NX~cA{ycO{{jBn z^t&FZQ$G#%KP>Y%{87~Zw%q^Q(_b4&w=zuJ$nsW}ce1>X<-;uNSw6+`IhHT8e2wLs zEZ=4MG0QJle#`Plmf~cphZ#=&jglND#<2`zc@#^%UdJ*Y$MOW0ku0aPoW*ivd0`?} z`kVD)MKQ;_gym%{uVi@@%fGR_j^&Lk|IYF@mUpnco8^5hA7uFm%g0zg!SX4V&nT(= zpKfPWQbR+(KSpa``m2)1A%ESjKiGe~jCW}K%-``h|Bipy@A$L#A@xZZ5nlxKcoBS} zj{~}YPzE&+)Id-JK@9{o5Y#|W13?W0H4xN5Py;~?1T_%UKu`lg4Fok1)Id-JK@9{o z5Y#|W13?W0H4xN5Py;~?1T_%UKu`lg4Fok1)Id-JK@9{o5Y#|W13?W0H4xN5Py;~? z{O{9%8Aav8^%{h@1CqXh)&J-lYWxN5k1 zxF)zZxGuPUI76r~OoB7R#lS6w%Yd`N`Qf(1RlwE2?SX5D>xMf3XBq=N!kq#)7j6k$ z23#(jAFgDKFx~{$2-gW`7%PmYz^#V!j}@ld;cDUDfIDKGFfE45hr1qbH{3UHQR9Uv z3$6@q4_xnfG3vMpV$_*%>)|Tkdf<*ZLX1v^^TJ&XR}0q$*9$j(q8PmxE_0#?@j^Zh z*9kW%Oc)aIC4|MY#Oco2WN~L-XwUImtUjM3&vme`&EfO7eRg=SUYF!{<#_FW`#Qfh zXOm0|?{i#R9WI+?t=(5xK+!BY`I~(HO%|KgivaSld%bQiofOy$ay-QpfvLc~#Xbyu zcE3(Om&faH`Ev*PAfy^lm?hWha8ho*L7%)rtJkKyY$)2prw*5X<}UQ}sdrd)e15NM z2p4%Y&!&E~aOUGR4JY>qrq4K!Bh%8@6~T@KPa9X{j@>4t9f z#7=h}`Q*)s5m2sDnQ9nddjZ-2Rb zTZKheg~y*yT7cfsOtZait1ZXsLm?yl9WJ_r5nj|AtWeYl&#l%G@sKg;P>*7>qZQ)T z=zO7@dVo#^g?@Vxls7yF#y6R`T?Fp(lGz0klr%`MB{UnPpQ2 z=c-$TGbi0@NZ0UWEu?C?P} zsvb5Z!(QZH=CC_)HxJ5=LJphcu%73l!BTHsy>6*3&%T0gtnWO#*G)bo*J5fqq=-~& zF$Qr=M`%D8pAU_&s?cxEcG?H;f^8jchYJJGL3RBd?+iI&(8PhcF~@=i<8xyyaQKTY zTVgN>ktcV+p>yup%A?Tl_S(@DY3P?e1-`s>vh7GhV?#^DjLV_K4&O@XA}u#*nE%K; zF!$nWsai?PqkT zFfLgx{-vd_U9l>0?VrU{+PZVrE=x>WDxTGFQ53*-isO2A{A{<^Cw7Fm zZFcb#0&Ab8JQ`6clL$y9w&Ox@p~V(|vB$2o@o|W%1LBuGC+mQ-8g;?V`URJdGKq3| z!6m4jFj)OQmAYmW`e&X$A8LDOlrUK_XLp5Q(zUoy37@!nl--JnR@^emFR;KFV->q_ zHR3&^9Ydo%DZ9)pw!B*?Ho5_>`iFLM=I>b+p3|+sRFdLSC@S^=RrSErbS%G?jx{e*f9Zc=j7E{sJnZ6% zF=&?C=?V%kQe8Dh_rC@HoxP}$sKQz8-za%N9@|TC?OW!GJ@9Y1f&3d}=KVX7i&1sM z_A%0&e0z-SfL&PH*}b@BEntiI7?prY>9XY7#4n*Xw|I1n*N(LkW-!q>#wUJ7^a5B3 zR@i%vi|pcuP`}3_{x((^c&Van$FD>E;=!>d(JKds*8OAUHQ;t*rA3{!W2|dXC+{I% zXCu|Mt`W6|nrKc7jDwQki~EebyJH;LVK+&?at zbS~<~(Ioo#IJ!TL<2c_v$fNp6lp|Vj*1U_-4iJ-y`{C6f3+RynRl7)?Fqf|z4=wq{ zHREOF9J$5fW-MQ=Uaz&-A}YsG6~7$kv3jAUU&r}G^>|#8yu$+aq2@_yC4j)UHj#vE@x_+?5?;4Hi<;*AtvB z@dGkvRY=`{7*Wsl9$~m zn|FmgcFAKelq*a&yHmVz1Szfah)wolpZMSiGJt!KVaGnIQA-OQyISS(WfW`7w_;)D z7dt1yP`_~^R_y3gROMSI;)?cCyv~>C*!UtHTiU3qa@}q9ihUDdmSTyA=&vAp8|_et zj&_Qo8cOd(y3%hZic!k^xoTpLxML!58WCa8`-BWg)GX?5UMipnjh>PWc`yXQzW*+=1Tgn?=immR5=g`)OITGqPU zTP-f1*o{cdP4dRzi2WdY4$TiA9tpdOPClhf>L~;(0P)q4g$|o#ORTtJ67KrHDOuNN zbZq#Xj*Z=PERpxT6Fzdj#=`p1Nl^19(SStt2t-B2?n%DF>;kmGSKwJHQ&)XVsVhFB zW80?`jaC4Cle{jw7q`gfDD;W%5j!ADtU?S7ji1PP&}qd{v^2Q=D3j>xp^!Sn!i~nX z=NFG2<*?as&l(Wgixim4WKEk8Q-o&cVW~_-eKrY`&%h*XRgf)h6W1S=E2@vm7rT%0 z&@vQ5Puo#`=u*6K)K-VhpD#L(Lc4%%k(2Mhx}a(j<}|<8T`caIgcfX(QEMl;T^8~B zB%ZBQ%lL z@zMAK@#q8>7HHU?5^qlMh@D5s;p6@zd}3FaUo;_SrQw+Oe1exz=wz^1d2wl~DP$6)#S>oK@!vGZ7TfiEfF+Ala?YP|Oy z%dPlfgtuWU6EiAe4Yu-qdiX2HQjH%wmX^8VbwsYbev~kZrWmAXs)e0u!op@jtoZa; z%p&@X{r$0OgriYb|v8#ryE?`$WT42e#r+Qh*5k zR}y!ehpVNsto@vmUBhN0rpCZSwfho`yBGMRk`BWamZord)fyXID$BlHX zxt@-dwG@p8mino~$FL_6yZ0fA(RCjkTPkEcjA5NqX$<=S;br$yNZBJ4qQZe* z0HNLgpwP~%>DW|_I74IDLsKz^HBB8H!`_&hD|)8pi_4GK$FTC_<-l~y@oEgKI)3;V z);&cZ!#g8xQ%8(pHB;pncF*x>oExC*>U)u51ueM zfZcV1Pdo}#16cR*Y5;rg1fysy5Mu1eF>Bzs{1GF|Wm6pDx+%`#rqaDr2JM&HDZUZY z?ITm@DlMXE%E&?M{V5o<+D{lcXnlMF?nN_cbNjQTao9sWf1*9-FaIQr*_d^X^LRF4 zl^{Mp0qYs7#Ul><2}8}BG@$fhKtlVJ9jN1eYRrL$=~yK@Q`^5U0H6zV~ zU0_jt zvca4uUNAdFml@{QXJ(%mK!$-|=#onCTo2Q#s-Qe})l%_w@)*F8D)H8d=y)e$xOzZV z7)SE`3claLw@g;J0jb4kn(%CeUa|K?no|S7Bb2; z(=e!fDckC|bZq#Pj*VZ?aiCZFd=7n&;%&yJX;_9JgXgEojU3TBjSRuprjfbTF^%lp z_mE-ZP8zpbD(F~+$7dMjxJK8VgsR+p5{8i5PC~&qBFtcuCfQvl(H5&kkNCt%Y8fuN zr_oNmMLdo8r9V@J%736^Ei@;LRQjhUQMq58L{{i`h*j4^4Fn6DpHC_hJ0oE{-AMQU z0C^j&_+2=A0n7nAtevVzjO+IxM(>@Z(#F5iZD@R+EXu;u#x^JU=dap9b)m+)h%9OXz+%K*^*=x_EXAqbV zuz&8vjC&IzpbbT0$AUBiQ&&dQZ9EdRhM(w0TTP`kW4PVsvwA6Vm}^b|4PDlRi;%SqIt=47O~ z#hjDpP`ij#W<8}vTzw*qJaVKKyH3pa4_{P1b0SZY7IED)r|3ATK-5N(DXY(&dm_E! z*^_wr`h^PmfikMap2M&mql)RW&bLp;n(F{E64f~C z>!O%#_tCMvj?5=jojud#j#0~WeaGl!L@ueMWbM<@YV;kWhUsKWeTBf>J%aL)N5v271WHl;!<6YbvGKCI8khu@!#x50hWlo21*peWR`! zgZoAw&w$~BO^AvaL;FU1W~hCm*Jk)d?+h{4D&9F2(_UGW+Be!2#rsCDL{Uqky(aNa zlv7+W-6ejUj*dn9igHpfMLF`BtlAyCeuhtMKMgf}jD`-ii&TBu7I6T6z5A%&bv4tm z1V_}?<1h5u#kJAyf&zM4RUQo;(8g>sLCq)=r{~TdrxtRpH3?h`;f5m85-o8@Lm9QRKFOArk$e8XF{hkq@sgrv;7r$ zdGVVkE&%R z?J}r6Dp4|1+N&0|FurLfwyWMmNiA}hxI-S>?wzPvoY_us&n&1)diBU-ACB09QTB4x zEZP1rIMvR=L+V1mcw&~E*2UAa=y4uwhUbvJMh>i1Z-i=iOiRmE@#8GA(qEi~q4+mM zX!$4JH4vqXaM+F?Gd1L>&0$eB+vUi{0#4jL8$IZAYF=d@s|HHDbWhKwCHQmrLEyI8 z2GYKM(T>awDZ={LdQ;dKJXkYZX-4|X}9o-7=7REs+g?~ z+)A~~!FnVHQ$jQL>%@&XYmh1HWG;QrQ(i6d*zp`)1qgRP1VmxB^oWUKym*!wi^8*rM zv=+UyY3uKrxgx~s%eVOCOd)QWO-q#fXBSv=*bDb^wlwj?JF~GTho5k+or7vLVXXv< zs#+Q%b#x5KV;Afr2_Kx1Y1FX+M8LBh(%>UD^|Wp zP^f0khFJORS7x+3c56OfT2QYSG{<5oMo&7wnClX~^U(iZpmKIdJ^gAN+#ju)kI}y0 zh;n;xp~gyg>z?^!L_I#=Lso4gB5c2bl6C!!R4O+s#hdf-q>dhZyo=!45(@79kaDT~ zfQ~&~bUe^a$EKI@)|c8*9hk3_c=-Yvph^}{&$)I1@2Hk9fQsr-GjR*fYWGswe!NkI z+3O|bK>Ma`3kLU1-&g=0seRKu^X0nVB0ik2`lP=1`~7^nnx+M6L-mCPKGC_rC@N{4 zK7b7aR1KF5gk&{#PRd{9dRhPg*sa)&Z=IZsP*!AfD)K6v^&KeaRr!x zoIdeD+@N{pr`_mB;y&bR4(_?Zrqhq4XfF3%&l!xMECs;s7GH zUPe(meluY#{3%YpKyckc+$)*B;#vwUl_qjK&hes^8@77Q!UBg2`!M36g=)P}zfg4_ z@g$Nq1wwEIO$(K^EN)&XGyM=Doz-+j?RV3~J6ySL@yo)kR~+aXl8%6_0n8^2N9Da@P7WUM)L+<*cq) z1ZBNJjiMh%^|WC3A~NKg5z;0%9?JHTz8apFTEmf^{&X+G8#|vNN}~+ymVsT*Q6Mb( z%NAp+R{V7_Jq+=Q_ZB%t-y)Ag{Jh8~{fMg9#y<^Z|L^zn^@naSmmx*D}6etkz2di`9DR z@-wJGmz+T@`C9w~GBs2}^V_tbsoX&W741<`H*Goto0(Xq;HiXo{tRJ4rMnl)2Ueo| zjKL-Kt}|#{Y&`?L9-B$f*nTpnKrTBIyU915iMv;gy&O?_CW=%0 zU4!<`_A|GNN8r!fjNNCr#V-6iG%Ztck4vWDP=7o!W#a&^3(%hN2WRHtg%f&tfasGm zU3k5OYED}+y(p~{8+EwBKa+Agag^INHcUC2F#n&0M+oS$;(@cUaFbC=PUR%Tdza`vIZV$N&nVY)tvZ**fvu2c{34@&Nqoq6EWtC{)GHue?C)ws(Mh>B))cTQbk0KSBv4A?OZjkQF%MPWi;G zNf^_7;tqt=|BJ4x{3&_djrNE-?Sg+}w-JQwu!AT85nqXz1L%d72 z1}%gelW8&U6MGPLpp#N{zC*`>f77wHi;flVQa*T@67S%M&yw*t%_sU0zvn&phbZlI zB&)vrUa~YIe4;zqDZWqM>h@w&wqz+Xd7DP`YnRe+){;zbD?EmCdQv7HN_LB<5vAb` zs#{e%)eU>O(he0JOE-)65ZL-LRi$n}RRtC~4;9}nRXf%{BS!Uy6r+1D%Aq;#=4HrH zPPzl#6mZ~8%DwF^8Rla@v^U5bU-y*QGM+Qo5;!&}O;&qRgzG6dJ$3Oi}L%9!OEI3I2p! zYCfVn2_rYK!s{pyH7jrvpnfqfN4&VyF1nZIip!P_-hkVdNvr4nWlqtu3_HW@ z>E##&Q}D*b3YG7!6x+M%anSD#I9b=t(vvk@tk z!qvkkypUR8^%jf3*=pBTl%MS@^ybn{d-!a$2zprhev%Duw!wZ%M%#R5MKPX_h&X+-G83;CG6Z=_?( ztu#8QcKgIi+AxLbh!-@TM(h?jmDM1g96#P!S?JFZ|3*M}IZa`o;B5QfX^^0u*9TY1 z?aqI$%oWeAq}Awwl^6t6X1^k4$J12Yz>{=ruSY5)nd&vEG_*XFN~1}As!&g2D_6>` zxb3NaJoKlR)hy!4RP>f=N?Rk3^|ES>^{59%w(pUWr`gYtm3sKnmr7^%Al-CQ_o8tL|7kIPNH@`!f32&PAe)F``}#@i@W?XPTHtN z^&))eIm-Uk*iiaz_26@qYtnMXEou4U(KP)z$~$TDIZAh$GF(4R!|G-5S@1Qh_2(#M ztI-0~v)~(7({q$3SC4p(vTrqQyNWMSjV3aRx1WYX_beP5>3bPUD<7tOmrf5;eoDun z18Wzi{O9R%g^R5x`MQVbOZSQ2(lKn*Ow{Cq;p^9+AyDC>Hr*v2S%blDAGNR^d91=w zzC(g{SX$S(bMdk~_S)A_7wACLw#%utbU#C@jQ7^q#1Ct5|7W8@v_b#N8Zb%t7e&(txh!Vp;-qD}`ws!Rdo z6)@$uqq41rI?b6`W9T+i7z2j#G5%&_nW=YFc#Wy_RDXIi^guPM)@sM4*M+oZ{kLmY~ixp@Gm2tu3T7v`?!KsSfGVlEYh!&4%<=L%kuZ zs9XyVkBX`>;t+0@e<^e$^gpw?!(cXtdmF!sfHnyru6jm=*4j{1I7W(S!3weB2pJmjf-%m*Jw3bTxLM4 z9ouQFF=mCAiM+$)*xjuAw+wY}|k`t|FvFi;QeW4o_wb&S(B=;C2YqSH#=o(`*-SdPx!_=vLC@ikR zSY_(d`n1eit;D!^`{>fpI<3Uiu2rG@?$H~&wAQf=+V)ZHT12%ms}Xmn($H+|)5;8e zhN>|Uz44WX6tvO@3M^_$Sh1@FfUqEX%wwu~RnkKZ2uGIo#T1WSc>K+#z z8I2~7V{~lo=!6PwTb<_b)NCclwN1;EjregO#Mp!MaM?QMn{K+M}$Yq za;0Z@bU5xcPNSlt%#l&iQ8R!c3@Y|ij%4Rz?jJ;_3^DAT59P0dItGgP5j z`VeS8(Tkyr*vKYab$XjAz1>h{L_aF3G1Y42+SKUo(WRQd$EcbD zE`a_dM8^Wc;bOBq$@1E9eT$eD|;o%vP(P-bSqbyQpa)xT+ zQZ3OXM!KHTQ2~P!Lzev8fU*4u7;Kf(g#6wzmwAA>kh$+v6@D>uZ@*&BoN=lTz;nFSC}gk6#vBBjQ4UV{)wnJmDjvf z@k(ZqqPUQ`eud&HX4^`|FEh8me5U;RnLX)>PsVjq{xxeAr!)7jQ@o8iBDfBXfCA7k!gevTP?@3Q_~%oQ6If5Y6t zY`}e@{7UR9d?Irt^Kr~m^Hlf@=6Z+X`OIy8#fi*;ZHiNwotG-kV6F-%&Sf@Vq4;9v zfvXi?%N$;+xRN=J`99_Yu3}DPj%iilY0OQ`8<=e`sc;W-*nY*AGH+zQUJrjmg;z6o zyruY`%=Mj$o0(&~6u-vY&ioN`O1BFCj=Ak)#U}Jyy55aFijQL6)~h&*x$SGkr!%L2 zt2l+Z=x4NH|XJ~E8fTKU!?dQ z=B^~gpEEbCQ2Z-%<|@Tu7^mp^yO@t>Zdk3tW0>>TC{AFGI8SjJ^VAKBH!yF@Q*2{S zaVp--+_X*crOeJt6qhhZT&nmw<}&76^zgr^@Vl7%m}{9c0xJA5=2qt2%u!`3`~~KS z>lDAs9EvyVsK4|wZ)7%L8A|#Ze}f7?mbtuK@hQw3D-|zfj=-CAlwSsOFY|@W^>?fA zZOrBOC@y2Jd{}V}bM&K%pJOgz#;RG?FZnSQ{vC59-Y}%{Lop9g{Tr~mMf@k`_V*Mo zWS;uI;`5l>n2VTWx>Wd1=IVbdzJoah?^{v+4a{|YieF*w#}747_&dxMM#Wz+mzWg) z#vB=<_{c@7{uQ$o|A{#!M)6$ctT~DknA@1sm?L6UxP`gubj5|tk&6^x%`DDPT+W<& zrsBJqBhFI%IP>B}#VyPM<~Nx)E>Yp#%*jcLzhQ1=Ho#7x`{luJQINhSFqbnQ!`#3; zjk%RMhPjXVOy=+nD!v` zW)Jf#%=OIg>gkz3XC7ewp4ohX%I{a^WacqvDE;L#Pi8Jt1g24-7@ z;+L44D;0M#XWgdw3qAcEibbNjzJWUxhcl<&rT7%)#dj;7$6Uj_gxPbS3QuE>eL!&* zb1m~G=I}>V_$AC6cPYMsdGS+4twait_v@b*XH8H=$+{OGI zvzW-&e>RuTJd-(rc{%e&W-D`m`7g{>%y%(2G4E#XV(wrTVXFMEna#`-v5!RcPhgH> z-pHKH9AI`ZS2174+{FA8a~Jc6%;HE@zDVWkXZ{m&0`pSljm&oD0P{u6Rm|ngP0S6< zUCbTKVv;KV0JE7nbQNDe^PiYEGRK3DFbNT0&StJ+zJ|Gt`3dIGqf~hxG50f1Tdndl z<8wtC-*cI(m~UckV1AW&ln|xURs3$|O6DJ!YnjKUsrZe|$1}Gw$1ry?Co%UjuV)@$ zb})~hp~~OJJe7GT@mTCHCaCaQ<~rs^=4Y5+W!CTt4_*Jq%!$lnuy0BEr8AqE1I#mZ zUZT<`F_$y1VLrgTkvTI-#dk4BBrCp#`F7^pnCqAy1fMV=AiB1z_^&gEU8(qc<{0K< z*QogE%x5tNnDdzHm`j-3nD1lmV}6x5EFg+@sPevH4ltjJeO(GKVP3~v#(Ww0h)Ga% zlS+RNa|-jT%pT@%nM;_%*QxYv%rVRn$|nH!i(_4v#!%;r5R{9}^J$%vsEttt$RC%ni(sFh{(k!ruZPaf}c}%%3wiF#p8d&phT_ z6+h}_mHq_g80P8B9_B^NbepQ{neAS1~`y+`+t$IpS3n zzmxb_>_;n0GLn znQzoN!zAP1Imn|V*E2_DDBi~$!~B-c>s9zy%yG|Jkhz3;H**>D%ghzb?=n|0f15@m->CER zitpC>1;vl*Ja)9K|8C|E<`ZbqGB+_N62oP@uGY(VEFwAZ zwPFkNwp7I)J$|y{zcAZYDBj5&xlHjtn5V8(j2{S)`P1ulI2Db|WgjYjg}J9&@h8mr z|5l7|i)H%7A1OW#H;C%r_<`bR=6+^uGt2O8oPQ>BG>1EwyE*@D%qA`$+d!(m9DYA@ z6qo-Ta~J2|&K$|y!<@n#g0%(Jx1RH-wFYs1m#Y6v=5(BQ({U(ejnT)cBp8O+_xHje)ta}gRt@@jcAt_)Tld zkA8=P!XqA2+{`?l+xzRx2i{lVA2WyVRs213*5it`5WZf!;xOhK=2Mxs?NQMcvzR|& z9?$$EbL6`!ehAiHq`&d3pJSN2IDQm!J#!p$I;T%zF5&PE%-cBJ!Q9MzF>@T3M{k)^ z`BfbLcjg}EhnchZ`k!Vl;_+rLb1HKib3V7{e>2y&sq6iUx$+go1I*Ffp2v??^@-?H z;ZvBSnP)K%aCr-ur`D?YXEA5xtLN*f%q_fsdoJ_%TorC*wsHCb<`ll(OPM!*uktTt zuIBP@WNyZH&D0<6U^a931I&%A?*`@y=I5BZnD;X~nY)-fs#X49FsCpNFmJp^g^vzZ z`rCM~;>pYdcPWl!?&SC}%#C-e@C4?R8pUgv(^-F3=1d+>+{`VkuPd0Vneo%@vVDcV zpz^zqdEh0*4a{+@zrD=KJRZ|$Xw*K<%pdCU-&X1ASCc5bo7=~4%p1Q`;YW>8;SJ0u zGcW!|g~u_6^8HO^?&SCu=2#B*F_&`q)y(E^RepCcS8)E1F}EF1;jc0aPT#{^#o<3Q z*KmHcS3%e3;qa4~V>o>rb1%nV%^b$%TbUC$eWA{*uN}+{%(pO?GvCJ?%lGq1$zf>e zs{g#q;oWDd>+57TGvlWy)%}~O;(y27Hbce#g*mQNJ?}S;Q}MU)^Yuy0wF^}I6PeTF z70+W1=lDyRr(UJ9Q3U4uH zyb9mWJefI``84K=$twO?%ni(InTwe7nDhDlqpf=UD3yLEbDvMuAHOLn>lc9GPW7o{ zE?J;>A9Mc_#s6l`oT>P0=DLN7$4pT9WzAQ7Jac%Q;swlYc+N@Xtz~Y(b8KQa^EOUj z!d#AZCxurt*I{8nyqh^@x#G8&4`eC+j@iZ>b_DC2(?>IJ!#aoZTggoO;=~s+N3Br2 zgLx|USt$H|W+(O~i1#sfVV{Y(n>i&_@o&s!*yp0~spwbKK4@Qocp-B)_C1NuWzJfv z82|i)jGusgI10a(xo4(g`aKNFFEUo~v&^yc6u-^f$oKbK=8f4ZT*J^n>9<=IhcVZl zrrLKTvxocp9Oiy*uS=LSk5=t%9dqDV#W~Doj(;(8I)`7Qhcn;G+;@yh{{V9n^DgE( zj{hQa9GCwda|H8O%z;bQ_5H#e`nckWN2=?u;QF1!+`$~nyphXKVvb>6%Pg3!%xzr0 zhq;RRFU(~e|61k-F7Hm}*sZF5k1}@_D}F|g?^oQ$+_**YyUg2|KhxtEs_-B5_{)`k zX)l=We@d!iGqZW6;8Jd`}Q~+tNs5e*Q7#jm1~WtBzJPj%rGvw3^U^{ z+g$csn#ILiig}G{A|TLbv_<%;T*|thT=Y(<@pO0AG=o4uTfm(Y2j~CTs>C! z`xPIoBlxu9QLl@9#p+W2>Td|Hs~8^`ar-n^yr4vI7saW1AF;pU&E+KjEXDq%B0pa7 zsM^Axr+CCk;V)4fyHW5fis!#6_#?$luafkK6jyvy_{SCd*9a~PTNCxi()ogGDo$M> z_%_8Q4J5xVif1hmeyZXl@byIfGfiE|mhTTyU{;^|tRHxS|(NdBj+^m>2xPYW-X{t#1B-tX87!B;3AwM=j=#T6AdQanq?ODn~TN6GWz z701C)MemoSc$@O?QCz!3^3PE`?RmkYExc0jB*n8-eum=7PYeGs#k*n1qvtJFyyRKI zC4^y*+phMt*A%hnToli~wyM82)!Dy;ZfT6fZ`a%Clw!5ec-C=H%L@gcpRo}hS^-e2#lc)?`J zZ@A)3F#J;a^AtBwezD?1n*Vf*e@>piKyjxslKy4I{m%&h9mV;5rGJzvo_2-Ef2;Ul zdBLX?H!Ump3XDB!-`ZbFe%C5Kc3kj{it8wDsW|5s;m0Ws{VKSh;<)bxdlZ-aM{u6v zalZ>LQat}V!4E2~gLb0&nx*)R?kDFej??#Dr1%hirlgi$A_b-q2L{7qP=iGNicsh{dt*9hOJ_=xh`5=QzMJ&#OOew^}C6=y2WR6Iv< zNO2#X-zO>_r}!borMka;+)AGz_4lme_KH_2uB`YK#hVnrsW?;ddx{UHOMaz_t7!SY zQe2|__ZP)I6_>?2L*s9{;#!JBif>hX`W9)Aj*4TI-$!v1ecnBayA?=%`xV!jD&>1n zaY*eC3lzub{Q0!vQsqCVxPi9+TE*2>{td-Fv2UgJ{g-0emlNKvc%AZpR6Jsal;=0a zr#s90U5Ry;>f?aox{B8*ZmM{?;*N^*72mD+P$$XHqc~RiIg0x$4k~V%Ddion_&|o> zsfx!b{}IItw0`iD@m$`DN%B4=idX3OQ=1jXDc-I4jN-$J``{pv>g$B!`GW+PsW15- z=`XmZ;x+obhKgHj`gV$ksC*B_a}*C%Tv6xOG{q$?rM`R?ZZ3F&;*i?oW+*;cManZz z@dCxqDqg4Gf2~$ry}ro5rg;8H!CMtqQM^lW>^;K&Qt_-C1Rqwsc&W6{DaFT@2rkz^ z>g$+Ss$7y*+D6SI{evabPTE9hB`Xb>^QM_P+ z;71kb==}AR;&GY6U!nN)6v3}4uJVB3_Z4r``R+5tYqUNODehk^@~0H<93!~GjZ$Bg z#|gep@jCquJ8*Kr(Q4kKE?fY{h6V-iQ*>}cT)M4 zid$=WHYy&qM(Xc<#rimz3?L~&ik{c$mf-Y-`1NzK10Fz98$;{RF+E^pyPgFnpRXBqq{27b!GFB^E9 zfxk5HNx|i<^3;%d#KxTkSFrf0g6;S58F+?ZJN*)aztO;-8S+OB{y78JY{K>qJh^Lc%y;eG4KZl{@B2K4E&XW|6^cy zHK6&*exDNt{?ouQx4Gpj82D-f*EDcF1K(`m76$HO;9dqEWMGeha|}G%z{LiB$iOoV z{Fs3k8hDw3R~vY(f!{FjHo^AzD>d-H4Sd4Dmo??~vC~&I@GS;zXW(vv?f!7Lfl~}T z+`t(IzSqD$1CKTEYy&Sd@OlG(Xy7jmeAK|F4P05y$?f;m-NGOf$uf2-@xMxJlVjr3_RDsPZ@ZLfma#$B?E6XFkF1N=8q2z{HcKt82FHZj~n<; z17F_E{k%#Bu4dpH41AM;Z!vHS19vcRR|DT|;8X+S_fTW{A>j8iW9Tj~KD&q+h%g9Y z2*OZ=VF(_CQ3&Y>UW6=!Y=j(yT!ecO@(}V73J?krdTob5gwcm%>d2qO`!evpRvrS^p)$c{nyf7RZPAn*U1Hvj*z zeG5?+J_J94Iquqlb|*qdgiZ*Z5$;0hf)Iz$4IvSs2SQJTyAgULv`6TG5RZ_6&>f){ z!e9jFxOoQH8aqo7Uxv^i#=}31+qF-8N{T1elb9dx_jiN;oNW5(UXz!=zn7f~A8z;! zUXz)M-`4L=UVn|`eG^mTFOoO13~! zKTkpr2k4*Nt8aYrPzNPtU~>2Pu8B5fU}~RU{cLE++rl1pX(t}7JRbOgb^5affqXy| zlGp5fUl27KCeUwNJ7B<@F~*76wYh^IB4=Qhj4uc$%wUS&B~O~=Auf_+7t9j1UC4!H zGkgVpIBORd^>CWUj+>lpaT^z=u=_I0Yn(wApKW2Hx;P6H=$D?^)mbFTd@{TGV3%h> z$J;un5edlaTsYrC+c#Vk8ab$k^8#W+zHJUj`Y z503L@aG~+nlb$V6hyOT8vrhoa@i5^aQo;KUw<-X)D0yTOp7?l^ID5>IsQ7ww#MMn4 zC~gm|L~wayWP-zAq=hqm^h9Z{$|>k2pJXoN8PhUM zfKL^BF5paqp!oc7G-0O!!5aSXOb~V>3<0=Ra8xA!Ho5qs($;WOEO%5$=wOiF9gE^j zMk6zw96mXi>PF8=AmL(!Cuw2NhYEDAG=WcN&DLJHxpm@gM2jD>! z7Y+K`@`TAlJ^l4%OT_x2dCZnF8IaS5ap2>J;2|eSCtm?Yg%(pii|3NJX2>eEJq>~6 z&y}aZXM|J(N8qNBJ1+xx&w(I49lk>~#y)ZocRT+jg19FoFEWJSEsEbJ$gU*dQh|tY zUqY6=grD*2|Er zb`^>iIt0lT0A*t(43fVPXF3`sy=j(jY&@H3E|=Ft%;72wx3m)2 zAqerwIR&{?g`*lka*3|)BowJn2}0y80$wB#b$eL`0bUWP(}rClglU#*gfN99sj!rj zCWI#dTH>>L$UA84W5Ok-J|@C6mw$yYnJPCOZm)dgUBXR{PILO7aLe0nA>8CFa!Fy3 z$UlXfq(00HqIj!t6D?O0ZmLmH^jhMAg2mXUcDQlHp(dt1%a24@C`b+^0NuW@F_Lq5 z?ZN0)f0i+xG#Yg11%YBe-i8r+fg-;j+F5Qs)_e6!CeDaG>h9O-JTC9QG0JuJE)gJ# z)x&7gv}a|}0Fkg54iRlC++wMlG4X&!p4L3%EtAFw#o?oj|MEz|0H)^P<|Y>;o-`*& zcLFT9VKMXH&0qI2Hl%wU-g=H?VpF}#ildp4y&4;re7aw~dGYNVz%g_)JPh6D3*eHNvIc_J7 z0Qqwoi^i_CD49jj_p_kBh7IK{ET|>2p_{9(U~y4g`|-R#vKB2oow4}Tq{1*1*P1rO zTXM;n$6A~v$f_>^A1ynXq(nMj)j`&mb5OL}g~AG$GbuJuqK_hXid#>jIQlD>ryvcQ z2l2fH(3`xZlM|xV4}VFW(+Xf5fF+6?6%#Mug-3Ka=fisD!dNTe=u{-gZJ|W5H7EU$ zGfB&R+PlQ(Zj9Q498;?9prG!l6qhm{xFyA)2jro=>I5ZpzP&X`tAE+efcI%tM4+j2BNhb70PIP5qPEZSh&PjVH|X~D6Q zh3z*^ifz47B6=a2;zZ?Fn%kbEM6u>rz&6U*<#E_)+?Je(B<_j{sujl)V`zvtEI5{! z!+t|Piq&pI)*H6ka3mwm&%R@s301NHn`!FMN!>M!zmTZapf_OPQ>dw&* zCO3^Itq6WT9b(c7;bd%)iS{VU!CffMG>T~0Aja9MIV>FK`BWihPHP4>DJ;gSd<>e@ zv5~PBNLD3sgTD4{o3|hl-ZN1dqtS}Ud8D;zamX^=-ld>J^Gk~5LJ{}t+#qZy*tr!J z6~JLKOer9+Pg{=Vvm0EAlCRhdUb7(LFDlFk74b%kx$Lgg=8nzD#TL;|enxrtVxbCP z_U5j^4Pj&3rsg2?qVB-#5nl2Ur}Ug!a~6s({a&8zIhEUCy(A1jnmVD_0$G@#8gy*T z!{0{S&uxTCBaf?+t6Bh=$$EgqScGkTyc3HNV-v3jTn>(jwMP~SGFx&?Ue%6z&=qLA z%mkg7^WoJU`VD3TE9QYA6-oix&ZY#5gCTE$5DGB+=VN3M)7B3{Yz-ySm|Sub4YsW{ zIpa{HV6qJJJOwBa2AYFXjA`0QAoaw;X55qGBgNC9W^E!rU@{g{Mk7R-Wi zn_^?r0w{L@Yeu$hlve;7SsNakKwj&k5_O+q3$kB(WUij%Wd^sQcmxfz?bNC#+KM6E z?>c|3WKd-iUPoj8T#dkDYQ@Oky=Af$3$AU>Z4&&*tI5BN5bMYx1HvSv+YwFKeLs zoF|IY2R&h@7kcsmtp40R{?Q(F2T${_c%gR?)g3)i@}Q@aQVGBF7KGKy30=a54|+<% zj^-(%uI4>zI=5}*)M>;%4t3vfBJT-^4|Y~VG%l7cHKVy`x)o4bSk+d7_=C5yw7kMw z*x?)AB9Z-7)4Kh{TU7EE&qH6O2#aTWwe-hW27Xl>oaTPS!+zpDVT-@SFql#0cq7Z= zFH!D!c9i_Zs|-1X*Qgu=h!D@W!4NyIw%o!)jIq6*QwOzA1#H0X;UNp2?ol)@&f%2< zHH@B1LWo+bT9`oja>*~tNF1CyWW;AY6R-hPhvfocVxY%mg&n{PR^ux1 zY=7d+u@D`LNLpECNHX8$|DDBoY%`DjzzYo+zTi1dZpqmRyp6;cJi~z3Pd?zSID3Lu zATHnqII`oj6iEOJKvXfwvfFkS z#q!|pB8oG2C0P#JuIRfN@ zS`gk}9ISgnwySjVs_uY1Zo^z2mM*2#t%)+CC!rzfe9n!@*ShP#%Xt{Vhh1!Xv5&pf zC$)9>C7#l)11z%OWaRL}|lfWvagJ9`HPLkS!9VFWV97&VuCJG&H z-konKW@3jNqNGe>8_WNY({$_LOpndn9@a^cgs8ey1+yq@86Ho*(LMC9sF)g8-|E8P z&P^iN{)_!M#?vn)-jBZ6FC`I2ro9T0eQ=mJ05!8;N_W&&db5J^;b5{i3-_}MX?sl9=JJA}JZ!WANJ<26 zARy=CFePXBi%EdUKn1=rIQg_8&l-YI>I{z`=0{&)T7GV**fXX%r3n_YGMZ&23L21F ziFlr@v|Q`%g;KJM(gGMFbmt-yCBpFFD2z}VL)X%1z)?zcwZbr@`XXBnb~)I1L7!oZ z8~$*4dH;dUEX?H~h4RPH#ymuw6&a^xWO(_kJ4E+(((o_-q@!|i0<5<_u%$s^F^SW9 z0wlDmBC9$S`hUJlY3zivJR7EHx@X7h-A6%nugh8JScT{L@lQmXITJB>f9|mcC6^Rz4qJW#Kik zHfG6vDijiZB1>A6B1OzpJ0qS88f^F^6Y^3Cvefd3TzH3!zc3cE(J)XHzliNVf*| z7NBzRH;-GZAWxc+xBqlY2wOqw)tV|CM~SGKgisXn*=H$CDF|k#@JR|IG#%O!1R2b)zY{XW?&cTjQ<(kTJ1 zgh3A^Vu)>vd=rb}IlTP~SeS1P>_h5wQTWS#o9YahXc{qy5Ega*^!xWz$6OoCiO+DGFd`~1BMI6i* zzC;2~p&aa0)w;Ug06X3gR&8Q*%gyWxD@nHZ5`_~BGZQm&Lon~0H$h|}BTEpyq)@=@ zGNLjpEed7qjy@dB>5~SFb58i#=aEl=fiXWb)f<4J4}&Eh(_f$u2B56}U1^1|T0pCu zret28XjwzUD4Dvpt&V!qd8LuoaW}~I9)e^nX11l4Hp6|Tu zh-cEZFyo0)N``6_9oP^pNJ9t0ppr|W;mlokC5$xA>|GavE+FT;WOM;B>HDhIUqCK| z4k@=6E>YHT)A9n@I?^1s?)G+8k4j?P$}aq(lGIMJk-H>OYtMM`B2dOz z7|oX^soS?p6UIxzPJKz`mCfU&$+BC9yWYmnNOV8{CV_+BSrXi#p8! z7nims1$QNf7Y0%uY4QjUXCqIDS{Fw!+U<)X?yDqYp-R*G$8#3`@;XX$0$-DYqOI3d zz)CN|Vl1kxs%c27^IIAYvq>k{V+|}*9={m;@_oKMI`_t$15=itlrlP;v5Cl-kIOts zDJf&q{Jjc8E!yfaEexM|bk5D^qXPJ7^xkw;@DLGo1QBlmR!2G5!-_5(XwrNtjgo5$ z8zP;>)zd&S_c`PxW0t1_Pg(^7z9JmeU_e3jlXBt28_Y32QfEZe3!PQtR5fF{zQ{hS zL|cbo@if>CsIK(9l4-OXL8ig9EN5|~wfXdrv@ZH2pa^_9PRxUSb1W2CMszMkD@q0{ zUHAI+@WV==HDy6;2gQ60%Q-Tzq@`8lT044!Q-cXV(#j(p{mlCG1Hx=wfib~Lhi|}m z8JH#L^pOzVyt0^tk<^z@n^ykDK?F$QvU(QDh@Dby&Ppyyp~Ev5fe%Q8LOMb*7e+8P zEkUYlJ)h)f>j{f=XFMou=LL|_zHwG;a@|k5feVP=1eUoJb}*sNQW6^-wNQWe1=Z z(Cr@7iLzk~^-$+jn3W5~*TdKL9ruvr`Ztbkuu-x_Et{{M61x??4`fU5sRNT>D(CYy zo5j^6EZ5VKN90u`R65Tc&L{C5C5#%ffGSzhkJS$haRQD>pn$h?nundhAMlOK z#q?7Q>qKEuI7hq0K{Ouvc`%nR)mX{t`!(7NLYt$T`Buq%C_;KJ9cd|rI}le04LvuR z@24p_+Y_`V0&g~7rJ=g9?t2Oa-OHe$XWA5cFeW2=IOvfbOra>FY(w~pkz7|m_our; z)|UVT!`FpSO#3>m_AfRMfNCH9+45zkCv!*&_7X;#G(2G;=vsGX`ga+qbJh5 z36-dQ%BV2>?UD2IA}lYk4m-amiuk}t+efltGf^mKJ z$FrnKq2?6TLl#cCa-=QBtDG|#>GS$BvI5e+T(gwx1w4rA$ZoN!KFSqbw(C+Z7G<-&E=f!ZE9F! zw@Gp}mMmZBt|*h$!2BF~J&R5kN$CcunLr8}*5^=Vx{_EM4bIoT!o~Uxv`E=jY3??c zwQw2cQCYG`aE?0mrGcgJO0wG6Mbu`tU!3X!#R#{CT?V^@y3#v(h11?0+qH+DB)X^r zQ@giM(TUK-uzp#gS9YO~=E0@xt7)`=D zA??7sh7K$rMXsxT78!+kp6t`1Kc(<}MmiKt%1X%{?=@3nH^j=e>lJmWaMS3tbuexm zrzH1CNQNztE|wK`NXK@o1K)#<$k{G2rRoK(umf%D_&B%&U+eN_CKPuF`3zP8zWU3B z8Pgl+fN2p^YGEi!)-+wpd8MnAnZ67hA4X-7#fM5lN6{UEQL2J1Uc}8bpGQ5ElA;ml zxpaWtfp4euO$+9ANQW(e%&Yif5oIIIvYjb4&fQ=`^9JP(0Ca+A+3?{U zIw|@U_p!(~PC&22s$Eo=$+K6KOcK1~z4j@Lvsy=Ych&N_+Cp?W-ea?iN-3V`ymlC396Uo;wTAvrUiSVE@31JrR7_FBf8um zZvdCpFD%7Y+Gxq`XXFCyf0g<=e4hy=)*>`mB{jZZ*F+EQOZ80ZmSX;f+6Clv z^3~r{yMQ3ob$9n~t69?hlao@Dx+e9BEQjOHmKOE=vPu0@aaYUHXx1zoe$R^~rV7?0 zb3On5pXZUJ8pOq4P6`}ZFFm?;g>8rKM0t3&ZqYKvB(-cENopHOY9B#r)jX0EnSHCs z>{~@<-zqZu)-CDMBnBp~j9GJBH~C?Vp1Ijj$ik+rW_&A_w!3XxGntig{307~dWneZ zXs}`8BMBz*9y6-hq8UyP_(0}GW|?$xJG{@vWf*|4u@n64T)IyMs&2FCtDGa-G$mmk!VC}d_k@0F5d?(5w@<}|_`@|$QJQDJgloxzjI197(V#8G{v%V*FXnz#u0&>ZT-HCtkNag3<1C^GzfyH`gMaCK@IpR1%7>NjF^HPcIWmR{qQjHozMTq@h@6v*8i11{FZ zyRb_<;^|i7x}@-;magfjRbKOOagToB1Hbx*luXH>^ArQ}FRx;yqF-zvJ1>77N5ABu zXA~^1W>U5rO9pbp<{;p7i+}4iOcEn|)TOQe?ftg7o+rusWZu~b<}4BTSw22$Z}R^6+#jJlB&dECW5U< z-vc`xITDq3q+gYyQ4mI%*gO`b@of#N1QMVl>5N>zeKUsActdnr5l$wT1b}rwSj7=Y z50{}~(HU`}tRZW06l;0;7LUb3c^LD)LZ6Lm3Jx}1>v3@HdograXWeYb_oZ8}5-uIG zmI7kuNz;yvbx>HCi>zHRoAhZbjyu*8cRtf{YJ0$iJ7LT%r<}$tUi&fB!J*v83DYy8d+Olyzkej=FOFneLT7x+C%5YhSDqUlO-&&aKB&R^Qxx(zvg; z9N2WUhks4Iv*o=LUK(4k`0g*JzxB$jjmbCtJ!^d4fCdki_Wh&h^Y4BATj#oip2~XQ zv+;$8XCHjx$&;bC%dW_|yK2+=pPZew>bCd+kM-?WVMxRHq$Y7cw|Vu!?C!e;epgVh z{&N+-YxDZc&)ruvwdaz-1$FAR{q$coLg~wQW%cQv7F=KFz_G+VtLpR{nV!7!*wr`Y z51g^4QOjA2rp>QYVervko=EWBbaR=6p071p*!ts!(;iw_{%nOii{{qr`A6yg(YtmI zyyLsl}3m?Ih22_a^xcH(LJp z?pLn=^Ziu=-)okWbNR95?@i9zbI*<^zk7D#p+Ck~n-KR*e%q!gPn_AXKJS~U@ipf^ zyy){c`2Uf9+lS!aY5+ z8!x}V;_A2G+4<&_4b>n2qy86X)@R&Yc;~~_7M)3JR4!rlZ5{7gIeyKaH*yMVj_-ci zW$*uY+eZVB?YL^r-s&-dYrQ?XU5R~t2A z%*d)Ws^(tzV%^j|D@RV8c6Hhfr7gE?Igt2p$)9xtXJf3ZgH7ZaNP z^~B@>-S6L?QBwYs_b2T>FlqLV)FxwErCd?&jZc5BIP>4RBM!YguY9Z4BP&$xce+)( z|J=VME3vQdjr?z3Y=3rs+}*RYZ>c`LV%?a}j@>c2TK1Lo@9+5Q+qZqzz%BY_EyXj!zzFC=h|OFb!+``yy=NGN1A>#JN~z}#|9n#@aDno z|N5arX3isxvSM@kZ<`$Ka$Vi$H*Ei6{L;#AbiVev*Y>w;RsN1w2b8<{#1FfzO*hv*XX}pKo-{zK=FG{AcpqgSn55 z>%DT?KTllk+g|VUEk}K`8&8gX^~;lkwk_Rx^8VTz`c=x>-~FB5akFdneEW~?UnNdk zUAN8^ulIds+@@x~t^9Ig;fkf*9;}s8^7wrpR{wLs+?#7==KucixTklFFU{_>B-H!L zby;71UG;EUqrkP*V-5`N^mON4o^zipIoWlSe^T!$Gur&O#oZn6pD=2BQk9qAsPM~I z3r3VbJbB?W3;T5OwL4j_V&73^w)%c5^J&wyjdN};chA9D)mr8D{bSp0-HxnmxN33h z#(z!Sw`usNjXSse>5e@Wo5jAi=#wpH+uT>dbM1;7pF4HDcsN(U;Zj1 ztMiWX%ab22{`B^&xqCNW`|dZDmknuheR{1f3FEJ;J#h2SOV1Q1m438iz&{q$eY^Iz z!QnH1KC__p#24bO%I?^@-Bm9>xa!cnyQV#PVr>49ivDiTE^cx8ylaowS(HD}cg>w2 zb)NO&q>@*L_}>2^YjfGXpL~*cdD4(_=k8l~-}nc=d~0UU8BJIIZ~3v`p4!xGXs!GK z{<%jszOeenFUIwmb*B8F2@fo5d93fbWwrme`Clx3;LV)o&%L_ltsyu4FuY5LAF^)G zo%ef%PL0pjuJPh`6H;zlctgd5k6xLR(Cn&}zLqD>71T}-?4J~Ws^!^#wmi9b;@pC^ z7t8-*{CwE9))U+AHP9$xBP> zb=~V(@Ikw(!M$%cUhvV?&;Qk~&F#Z~oSZys?B03hQhy5V3ly}gaWd(Of-W_`pP#fo z^Ra@z?_7H9=>gNH4wyfC`OnYfe!e;D(86+ABfI=#tEc9NzrB-Iz0rr?J&^zGr2Koo zy}s}0JAQe#b>oijRH--WaG-v-R;ydhzjjVaoux<149K0b>CaK?=AJm5@^|^Se=12_ zc4S%k%z zmHp?B(`R_LR+(G;kN3CqoB00D@h7()D);NI>XUn(Ej(3r{-M0;EAob~XuN9mBctjh z)gD%|Y2u*AKdH6!fw;YUmLI6lWYLbp$Gwkb?DUp9(|h`%XGbsYIHcFUp}$xC>d6Bq zwl^!Ad8AD1@4h_Ms!W|;ITaI}8vpW&}odiKxpLq1)y m;PC_V0%h(`D5}u-r?toWcl>E+)1zyy{MVN+n583l%cQ7}pm(1X&t z9HX@NfZYT){oDJTb$)YxNZsze)cD(L z?X}lld%gDK_c+(TVcr|-GMOm+tB<}J3Cdp>V{*RYxepKXr6|fpXGMqc-xH&kMu&n` zP0rUQuhZ({if|p_>FT+v-3niyydLlA!Zk>!>mk0b)}?Q!I7d;qMs76me2^;o9uU!!?Xg-1T>(uTNfIPo4gSt7!FaIDaaq z$r=Mvg!dy+*I>V? zOFLSpE?xA>r5($!Uh~S9mX&Qw&V2Rht2$4ARXpzTG}8#?zWFWD&(?qL-T(CTpP&8O zrhWf)@@Kwv#d{yW@A34C3OJkO@^|6YoI?nsgU#uWMs36omm{c?(3hNtiunc^qc zrO2~3#V!j{_{>ege=bFDTT=QxDusS#3jd#^#L-t%_-sp|Z%pC;t`z-mOrgIg1%C|q zT7LCH3jH@z{PQI#dV6)scwd|14_Bq&>r?FBn8N?dDe_;Cl5gu%`W>6X=kAoe^u-jr z{5eI=qp*7x-6j8xPNAQefQm_7lwz0jQ{;b5 z3ZI{(*zwsEKBbg=cv^})J5%@{mLkud6#L$lf`3O!zxz`9U6SH&*QVHa6nbb#CQ0D# zlzxY$j7yuRTKVVs6ghvB!vDz>zxsGep4^|}R}ZJycS{QW?38#sE~VeP6gjgg{i6Pw z`uRo*{n!+JKAJ+mDMg=ONzwnd6#7r4=;1$8^zi$Xc$t<$|Na#IA5M{Ta*CXFDfksB zc3F}_-=3n+CsO3hr0{<-h5x-N`nfx$-*=?&*EvqD__`!TZ>!-`YyJ6eDgACrk#l)U zylhSB_m3&^pPQne52oa$U#5)rBOi7alJIffBu}pry-@^#VP*q=@ffCnbPmB6gkgKp?_V9-QSC)7;q`mE`rjF&Sx?7gF0k*8_F1PPk-r<0jj^$zx%Wf#I>AXset&R_VZ zrbX>5+pp+Y)!n{w;hW|x?Ofizu%&frd(_l)#WDuTU{-cFHARlJCZX5Y>K2W#`>Lka ztGl~txV(LJ)9Q}yB~43PTHBY}npNn{AUoF>P11f<_q7Z%D5+)9qNWuqJFn?LOvhw( z$MQv;t6h_}B^^r_dE_ElLw_q;R<MbRDc|S&D%j z((6XzR5aj6TG(xvT-(4iZzYAfT7nTt*AfHY`ZwW_SBrlp-Ni}0Dw zW!{;}@?aIm-uiLCt?qD2R8QA*xIsJ8va(!v6=ju)m$a{0(Xu=?gj0E}=~(HVS1(_* zD((?#;xdR>%MwryL05lohx1JEpJ&Co2TMh>T&hTmK9CQIv1HbuLISVyG{xl z6mN|oe~+bcN;3{q7Oa(sp-H<{dD+X$R22zBH(})J!k1ki0l%_+8O5q9brqVc9Fw?K zMm-KTE*X3zFqRV1O;+U$HND*<7m@c?cE*N{ohFD@<@*6yvAxVwHPS%hbY#4>Y$@Mk zpfiy{YFO3L-EmE9YFk0zqo1y;U3OLkyF3L+0kK3Ljjnb49nQ zCpiVv8*iH%I?lZsuLTAfF&FQPv{!apnXky+fu1jiK(fT znW-&Wc9obwy{PkQ)UUo5bySq;WNlr^6y}yti@V$LfEBTbix)4wdexF>)wQfwmPK^k zxf;Lf?!20OD21)${}H0BLa)JWZ5XfH4};-#ztfAll#xTe2sgwM?aQx;u3k=+3Pky$ zsAKt=q{@{|Ez7UPHn9~~d2J&*mS|;e7K_DhJWE*itcqG%JMpPi%RM2^c$G$qZ%6cW zhE&yt6!LHSr(?D4T)e2|+DNvriiq9FdAy8;wd95oBUPMeZG!7JwXb2})zk!Xd8gFt z7N?cLD^T6qY=tv*Q&R`_pE)vC6Z<%=1Za$U4|^~w%(R92ZDA?$>83c{Z_Mj>ilEN@FXKx3Z(I^KF4vcF8J^iV1W0v<@Vtmhq8aY5 zfm7Tz<{kLQly$U<+r>NrHc$<7ykjWl9(ajT{lxzZ6xGt&I+evul<`%i;ZEK_7xRn9 zTj5mcH?)iEV%b7lT`N+=kn|Hufookf!chv2^(2Zz%XF~@Lhtc4Q`W6$^-uVJWiO~_ zLbCkbT=*C}#u3r)!RenQ`JS(z1{~K9clB!X0Y`sG^n95<6WwX}?QOh(9WmBDT-?O3 zKKhZb7kr0ne`qwGdhz?3gX^P-9*+B~E6a1ge}1SQuKKrWUVPn^z~?-E$B$ydK>x>! zWBL*Pd6a+;@Pq{ZveP(t;9oX@Z}#|86Zj<_KP`b@;qhlB@M}Cim%!&e{=5W!qsL#I zz;E*S%M$ooJia-B@A3F03H)syzaoL(;qhw{_&Ys5pTPHd{Kf?SevjXj!0+<-TN3!k zJ-#P_FM9lK3H(zYzaxRa__eVe?@Zt?^Z335zS-mNPvDn${H_Fkg~vahz_0Q6VgjG{ z_@@&1O&-55fxpG$pG)9x^Y};ySo{nAyu;&1B=C27{OAO}&*LW~@Vh)do4`Nr@uw#6 zMUS7Bz(3{jXC?6aJpQ}{{yC4oIDwDOj_r6^0zbmzn-loa9={}kU*Y+$NZ==U`ZWoB z*5mUD{HY$lF@c}v@tYF(vpoKm1U~2SJqdi!+ieMa(A$m#{yfj;PM`NOxWzHfGvfdB z8T^R`f4{-cF!)^tKiA+NH+Y|FU2W0e^{i3&_mshZAt1T#Gx!|_|D3^p-QX{NAolY* zF0m)Xm6sWOOcCRo4W1*|^1mep&xBY0x5D5#LN5PXWAGf2mjC4qo+H@uzl{dZErRmD zO$P6$Ojmb{!87%h|MeLB5oK%?-DdE5MkxH-Vem%=B-c9)Uh{1D*Jtqd{>J?VZ||$@ zGWcN&W$;HE{62%{snPPk=M4VXGM0jSqBV--5B?EdS3APs z!!yvT7;W$;ILi1x6Ab>P2A?(fi3WeF!DkJAn!!&p__GYY!QgWSKiS~VGx(DX{$hiF znZaLX@FyF5v%#NY@JkH-RD)k(@Gm#`H3mP$;PVE5n!#^0_*WSGCWC*a!QW!=ryG2a z!A~{#+YEl1!S68mR~h`B27iXZ_ZfWHyQunpgMYQ5-(~QxG5E&~{!D`}8vI!X|CGVc zH28f6|5}58&fw2B_~@ZH+sXf5XYeBo{v3lJZSdzB`~-uaW$;;p&l&uw20z>2rx|>s z!JlRDa|}Lb@UJ)c^9=qC27j@^ztP|?Gk7~sH5>eShJJ~`pKtIh4E_RxUt{nW8hqa1 z-(>I`4gSprzscY)GWc5z-fvM{ZI8iUV(4!(`1uCE!{8Sf{GA5B(BS(FK7Jb@uDsvi z-)iW08T_RN|G2?lX7ELW{|AGA%HZE-@cRs2&(4K^&l&tX0#et9EB^nF20y~!FE{wn z2H#}x6AZrD;IjtbV(_OLe5=7vGx#=xKg-}38GO#*+YSCagI{d$7aRN)27j5sFERLL zgYPi-B?f<`!LKm*s|Bg?+~7AEe5b+RV(=>rzQ^FZ4E{EQUup0= z41Sft-)ZpO2H$7!R~!8O27isg?=tw+2LHIhuQB+d!C!0ePZ|6>4St`&uQmAR4E|jP zAN{c6|L->V5e9G9;iC=ydP6_K;PVEbHTZP~f2zT+H~47=zro%8T>~L{(gi1n8EKd_?r#>af83b;EM+T zaf5%#;6GvT`wae*2LGJFf6CyaM=JjRX@ehO@SidG(FXrngP&mVJqDjO_$>y1s=;qH z_-O`ztHGaT@SijIoWcLI!JlXFpEvl64gNNRzs%sb8GN(BZ#VcQ2LDBaUt#e7V(@DW z{!0d*H~8BPext#E+2A)B{8tSA7K8t)!S@*a*9`tPgTKSzcNqLP4E|1o|E9tB8T_3F zf4{-^8vHJUzsuksH~4QEe9_>)ZSYSS{M`n>&*1Md_~#7%I|d*5a}urp`wV`B!QX4} zqYZwi!A~&w`wTv7@ZUA~Qw{!m20zW_>BhtsKIYC_}vD7i^2cc;Cl@ICk8(; zTmRGjYrnH?e|=P#S08QOzxLkS`s$k=+XwFAx@gCk&%b_Pbi>OE*}933i8lPXy6x08 z#&r(99k|uNUj<%o;BNqLHSk@)I}LmfaKC|f0@t1F`gZ)k58Pnj2Z0+6{0MNXfp-J1 zH}GS?TMhhk;GG8kC2+ri`+@6Dsr3IOaD#z=1>9)h-vPH8_z%GA4g4(dRs%l|yt52% z8uJ(6egoINf&Nbo{cjp`C~$*;4+CyA@JoPO4LlZjy@8Jg-fH0EfOi`BMBsh{HvreY zywd;4zzqhT0^Dfe(}7zJd-f7@-f%^^I2wXR%(*GNQ8w`8_aHE0W z4BTqq`M~Q9{8r$t27VjxP6J;K+;8Ak;JVW){VxV?Fz}VYjRsy0+-l&J!0Qcs4e(Y2 zzY}<;fv*GZH}HDkx>r>Ce;;szfjZ{V%K zb+4@S|9RjB18)azH1O@ftp@%o@OlG(19+=}?*iUw;Cq1k4ZIV$?(|Ck-v@3m@PojO z27Uy%)xf)f*BkgT;H?J!Iq*&c{}Q<0!2Q5=Q!D*H3EW`dUja87_;MiNO5^ zZUC-(Ri*!vfg21w1-Q|`rvtYd_zd9n20jybtASq&ywkwv0{0uZ5xDM*O8;*JZZPl# zz>NleGjOYc=L4@d@LPem8u)F%I}Lm}aKC|Df$OGM`d>(ZC-DZZ+^t!0QeCG2pER{siz&1Ahj% z-@sde>t0jo|MS2N2Hporr61c&@zXEPF@b7?I4g3e- z^#*M ziNO5^ZUC;ES?T{|;06Ov0d6$#>A>(ZC-DZZ+^t!0QeCG2pER{siz& z1Ahj%-@sde>&~h4|9RjB18)azH1O@ftp@%o@OlG(19+=}?*iUw;Cq1k4ZIV$?%YcM z-v@3m@PojO27Uy%)xf)f*T=Bg^3*J9U-Qx^zxzb~o~Vzum+Er^{gb1@lkAIfb3+S`CZtl!nCVGvNao^m5JZ)Sb!*T=5ko|GSkd5;5o9l+p zd}dT`VE@{kZr>N=|Ha_oG5gnk+wm+O84Z-+>)JPu;P@~%@K1Ho=Hl_uz~C{_K=Jt8 zz$HgTn2T4xg%af=vfmEANL!8@#=E-gxdp++ zV{!w6V|MAW6 zr~X{zjAauG;?F*NTyz4jik^_WN&P$B zFgC8D`DLJKo>lmxXwYw_j`rvSfj3LPqWejp%g-wON_3Oq zHQDpxDw>A^4ZKRCIR##l^_1Meb>hWUbiA*;Im+aB|3EaSMY6%Js5vU|-JQ+Fsqmee z8+eLwtQ$^S%Iz`VT~R(N{HtiDLo+=$@c+VCDX$1L#VGb8a6bemy5bDqM>bQ~srF~n z{_NbqH$r>L-JyLh6SoKVHMQqjoP`e|NAa}Wz)j#v;z?P4UQ#_}z8$x@#8u<_0A*(v z^zd2rHzyic$9Ri#as#gu9aqs^AJZ|eOyQ%Vn;Q)j=jH~sA(zuh(aQqv^)ZgTQCFCZ zee1B1^Bd}Z?%KlhLgWM%? z*}6iP;7gI`(%is%>08(0+kJg=U15dlo2YNf4e(tC?}v494a<9MSYbz?jOfviRudj3z3hUXRnHTACS+ht=JTs!b;r@lF(Tf`&NqYiMUNJTqwD%;pg3H1;abmC`Ey)u|A=h! zi9PgHIx@FkaCSoTiFM@YXm2!?@$Q#R;Ze|>HMqg?+7F-o@ZpcT4RKvKQ+&&pLf($g z71QzY{N?0<&%hUT78%b;VQy1htb<{(4?3;lY4|?Crxb`xZJ>yt&pkJ4_$;o5UU1cu%MtPo)p6_7(R_;~a zy;Eg!tjga}d1_q#zRJp@;D4wxIa1}Hs7!twl+CBc{`DbjuCn}~UiSVNxlHxQq}=G{ z`RRt1vi#rCE`pY_d_#F~kg*kK7+T7jmmKYd(5n7;aOA>4bfNq!A9pe+*Mf7jXF#iZ zbfLUAsQHNed*-+}9v!{%G5B&EHXl+Dta(fMaPNiabuM}(AD3ncmmByDw->Sg7tht_%IfZN6VcUki629{+cB zR#!V;L|5Od($!xVBWfGZDb?#j`OU$^*iX~iRjj*A!QOIS| zf$|h%uEcN{?|+ZqJ!SaN47+iDpDhqp`e1^_oNQ$5E_V+#G?OMyRYgfcVP%=3PJ5T#${T8!Gc+ zMaS{lpfdNKHzuAZnu%BCi0_2GW>1A?mZ4EhbFY8A!YW~8AhLPO4q zY4R1C$k1r7#&c*fO;3g9zeZPdn1zOC@M4<23eE2fO%58KKa6RL6`CguO*1q+dl}Pk zHlp>neBc*`CJ)VmK$ES|{MgX+KyyK$$yI0`G&FtC%nmfo6`FeuO%a;20!_X`bEl!n zK{GYb^i*iRY-pOHIXTeuRcJnMX!6j!G|&_)G@mv!J)*|0>>qwmw6^S>kzns74JP%^-7SAKgS&rtgp93qFC&5SXWc)@k zsWqBg^C>>HMq}N@{%g0^UVmmU64s}mS`cl1hW1hPi`~T|p87)+UGbTxUl_dE$y`5f z`@d0+&!E2N*JLj)V`oCfuOwuA9ym?L*)`Ls0_o$L z7we;^n%6DxB6o29G@_uh@tyDrvY-A}@_Mh~rL(W~;p_{!j(;&;(P8eKB=o(~@RD5X zja(zrcqPuD*1g@G5pBxkM{xfE+n`^a4QafY9*;w}LvVS6>+^KztxQ++xITjmbVY-U zM#lXzwsPC7$7N$2KFqn!&^6?{2YhZtcm2<5uN!Ep&)c%J=d7Uj)x@jDn8_VhC{E4y zMo+E%vDR2ol3W3_B%D|IXCV_wqC8}Ha|Cbv*w6ebUafyPzyhCL<;_J@b%KU zvukf@YOZ%y6kWx-Yhl0(=lVuo)`;UdyXcLEMPoD`l_$P1t83Z7pw@jimCwHB#Ajc{ z;pV(Sz09u>M_XM`m()dzr#n=AQlF8ooPQivkj@6@g!8Y;UPAF(!Y4J~YQEGS zvRD_-tr7QQ1oy#Lb%MW#E*%|u^Yp#0ZbWY2`A~=M9PaSkz@GvRy=lIW?+1lFh|g($ zzIXRYybQefx=`zF_1}X$YyXRW$+30l%qkTj{-a!j9Pzv!_U6?(3Hr$RdnOutHk$RA8&@tW{Q6I^9wyKu%E&zmSyfh?M0|20 zv*~!Z4_X&!pue3lJKusX1034EM`NB7$76PIaADv-*7H2skw+tZp5zZb4qzD-%{MK@pMMi%<<6X!ShUk;n={7!xw{H8J|SA0f$RoOy1mfs$p zh^f3^uSUd)`i(qX53GLc=-2smef{?D(zaHd$d2WHe7ycaYZx=e9zVv2*ej2j@y(ST zHEz}<_0%aA%HyY8HGX`+okJkYma;4%PKNHML#+HLA&ZM)`U_*uBik0_2yQ>2%PRqV9EcS(*8g>|gsPVx+z*M#A1naT4-~%TdgQS;cRRY zaA92JjrlHin@1MzCb!5=j;1+Yi-hu_g<}1o+xH`L z(0_Q|eE-@w&ZXPHD}tBeXEymm&s+q&?&)uof5=zR!8XT_bu9f}M_Frt%2=!C!QhLE zr~gv!B(7%$eG*3>qrLL8&v&l>yz*C^|Ehc|?!RvP4{39%;qhMJJ)TEqL@u7U4$t+% zcZ&S*3H2*}qvL$e`!O;9@iBaa&s!Pl9UXYr7Wo?KN8^;A9Z6aFW~AwRF@5WP{yStN z=tZ}lHLsJEJ~ONlq??dmtL97PzGI@^{&7+7o+G2)y`%7F8{dBJRNVP|)LWd6pN&J# z8C;L#dN$WlT=75VhntGJ7ej2TfB8a`e@)>S!5kerL(jU4;eHg2Pz znt3KU#y2O%NL*Bnaf{|B#;h@}H)H&N&v@U~eBtwq^KB0+Z{{-bUMJQ|-S(r?bW*Q% zR{gg9$ZY0<9p!Zv@m1D~^1ojjX?;@mg=tY?b45R{ZNGdJJKD9%Yn*=OY#XdktX2HH z66}xv^lI*9Y`yeBtkq3W>+h;qv12Aa zu<4NTD&CMMjs^CVE(SDL*>Ud}R(Pvy$Xp~{1U`Ehdol)|RZe#?P#HgZsOUaL4(>_4 zs0^<;NEv=wLxzyA%lVWszFl-$KQfLydRP>6$v89Yonu{wb-H3I!&)NJKDVMr;!A$x z^j0swG5*qbSV8-D`BIhMh`prVZY#??oB2D$Rb?&dau_my1OJxH--TX06>CQkCk@Cy zo4=bVYdk%ar~SRhv+0P!PvChl9bYykX%{Y}u+_M*q@#~Dy&Lda(c_XZC-26T`xUa7Hq=Is0 zCgSXbobi3}fxq%eMK|%fzAA427TsizDx7zax_L@APUMXtxC}p$#!IkFr}Qc{+#Zxz5W)BmHKGf9cTdn|M~Syf-ZCQF&K-j82aAm_?7Y zmpz8iOglBE(K-^EtDtdy@?yFAZ_)LpV++5P{0HHOAF8SA_#Q?%KjMdTz zY;7rC&paQWj|Y0z>tkip+1RdFKWzKY=u7MAAnT8S<=2M@hm2Y1ehrVJf8IuG_0zEN zY$NYZ+Dww2PQk}m^ObF?@yPC*rc~_C8t_@@B~J<3OgXp2_l5r3@spcaSaXmz)p~Gf z{3PZNZ0eplF#0aa#}oR_860Qr^Ur6Dim9Hk?#EX1)NP2=^ z=NUQd@B!@L>_p!cz5ShX@}`puQw~yZ4@%Y{d8fDPo}=^=aw|SL@QH*Efdx7j>Tqi_bvw`KC zZoM3Qmp#EabX>wGGh8`H9v^7VH#CQNnqB(_Z@$pbyaJzh_JEJti_dA`8V$`C44>y{ zf2-o;G<@K+*a!L!MJ~#$PY0H&ZaQ^S<2w1kAbYN{;s;K8l;1EHx##!<>#sCDH!zEL z57t4SRc@ipA+!-*?M|1yw0D~k?>(iXOM>k5y&Jh+h5XRyTzIU;0qwvG;9p|Kvc!z# zd(??;IPG+nsdZoSd@tT#{lMMtO~y5P(;h2z-B4ux=VHaKsaKp`xwi7je-HM+mzJa3 zmj{^s)DON;rrjFqplyS;A;6yYt%i1k{8D)s+RLClKG@LHI)6rf$?!tp1MQojeNH|> z{!=W=@0_nHu9@HNRh>TvRh{$IW4y02Z%BV-U-htXnp>)Tl{U^-!D~+3fDH7hGm&1} zyLkS57z2Huf?V(8ugH8gZ@k6nJG=PQynb!YR? zSUXqw-0Gn|_g;8EvqATnFWQE`#m`O;T_f zbbER1^>K8dcnj@53M@Nk^&C9+L2B(Y-yz*5^7df8&U;4@8|X5Qi;9ew){cu)5^<3a z=e;9xcNaTvo1ckJIWZbB>BLNQ)``+B=fG9?x_}>Djjs>*@wvOBzJ?Q@yfrF3QC~l0 zFMIqw^_eLeYh8}B$kCj|aK`u>Z0%wX-z`CVE^>vlouG&QONZL(IP|c8?S-5*#Am*P zbF|kU#{9(@wc`Jn=x)w^@8-GCyLm43?!ZfVri9ODbQZ+8oUM*w45vkRBgft3@Vmj^ zO%A`C^Rc_hr*{`On7!@Ab%jfi(fg9qv+j{3^Vc8n_x)>Mj~~TnA%BaF?$LcBv(|{n zOXWPpy!gLrGxqesX4fZd_9${5jLp8}bn<^-Gw$W@MaCDi*?WiB>=1lv9C0;lXk6WF z;>zDcQEt3MHuLk3a%Wa+1MX{tH4=VUhM}LS{Fv8T0Gc{tK=F}j9>w{4-S#h$4_v+G zEYHKQt9VZd8u3;&MvgD4zH5`K36j5XWM)eJ^pScNknOMu!(N**F$Q z%C3raAB%6VW!IaB`uYr~w*|!FA#u)3#G>xg5r@6Rq0ZtexhWZkjKR%SI-k~gwa%xt z=CSd&OwLQJNn8$2tk>K2RI69q-ZrjZK zvEzioz4%pCE~?RY>x=YV6-UW_<%{H^(-Z#Nlh7qRYU)z=b*?dbl>UM)-Tf`@jeYPS zbvW%s#_>1lusEr(;vjT5Ceinhb2WeFC0*?a_aB(cmACWK6=mw&{isN=*1-6aa&maD z*u{?Mlry<6u}FF|@L5Jr<$HO54?o&>a$)L0=wKJ}RPCec>zLDqGpEJR-DGV(o>4m{ zKPBCmm~2k?QBm`kd=B00CI9Uq&fMJ6d_*BLLUYTo?JIv~V~Mi&!|G4e5Tuw!10FZ_>;IX}H{c(@fpz{Q-_v@Tt zH0!{T@%22xtnZ6DXCT+~LB~5dmk76n@=Wb_;iLWjA6ZM_OYUsolHUy8d?$Z8hYIIU zJ=)hzrSIv{z)ux}#CUOfEUVk^v%Yd?Q{a%xje*!*C>p`~D-oBoYMo7i7v1}{p3ypO zMw}ZX+TTTAPA>XB4Vg-(+6i{-!rl|c$x{|rW=~u0w2yZj`cJ@;5x<73>uoz z)V&>8wqWmHJ}-RM&9eSoeBFn z;xFP0uBc}n{h7zBz=b(tD{{K~)LzC|{<`h&U@Y?#2a@mXXy83DAIh;F+#JSp+)h4l z(v|e%<}eQnSDKZW!)W8=120~WiLagq_s?$U(PxK|@0}^~O+&tuf_!tL0hfQ~Vy8Jq zzPVmLa8>eo*vU6HAs=m=eBj0FcF6~>IM?q#hyj~_<|ChCV58`~&pg0=f)ESweVhm4 z`#ASiKPMO2If%0>7fZwGS8+PW>|LA36vpC*RdWw|vOPZM~jja_jni@BVS{)u+h`b`QWaQB^$IN29B1KDsZgoMm}S zpB8p8%e&Z(=%5iDG%~(9+)s+=JIlK)C1->9+ZwV>uzA=|SYFJz72fD_689RB$vuaw zZ^eXfxRl#KgMNAk@rxtSMHw$0O9%8FYN?+4_4_$a!K~^ z4pg}v{ReyBYi(p?FVjYc#I)~TG>tfz`^YP?DRaAU=`8teJU?VdjXmVcnnp?cK3=j|yyLi2Raj)A_N zz7upse9p?3q=Ao{U!6VZ+tNB2;U^h4@a(y*bAD-kCFqv8dlR{_OzZKPVbLWPC!bP$ zXdGJ-b_3VT86NLChiASkb7Y8T*GAu`>pPn??wzbBj`PQ{ZzAw0T zv7UddXQpX8+{@deI)8TZuI`Kwx03V$9~z2 z4OK?(kCv#Nw<{F1__X_NE1 zxCnlE{`5@)%|k(t5xGV&q4#za6FTFRKk2yuJv*i|56v<3Ia6izosr(CyBE5^e+Iml z@TWD4>c+_TxVC`{=bp)U4RIl!S%0RuNPVLhC!hBjCm%99`G}t@7?-WT6gWBO?O$7Y zt}m?lonMY(OwuF#)bBc@cUO$NKD`2}=RoE+=Oz30Z7-Muphf33sz zJ%zt;4SMW{R(k!F@TRS3%6g`Jl+#=MJe}2Bc`V8)8jF0$=h+ai!A8pA8s{wXyZy$c z_KflYwcCvDpzpHvAz#?@@^ZiGFQG%%uh(0P;vGLa-21^k_{*1^PN0!IUt{b;`%HPK z&d;p}8f)m&o#im5AFEGqvt7P_r)&C`E;@{i#>XF$zGMUG=FK(r5@bxzcR?06U%_Vw zd@A~^%yZIb*bBbJ**e|_Rz1f+uEx)ImCuL#{nEW{oUspQji`4eqa#`C#%Jsw{}0AZ z-9lpTz5Mw+<=X3eSIKAZACPX`vm}*!cGlODIVy}zbAjymap}md*=o<@zcu43$0Pp3 za~bY?UE*PL+P!fs@(su%3X_pv<9LPlqst{L;}5=m0km3EY3-}nl3n#~xXN00I^U;G z_=gPNBj9;1e@80@oNqasKcrYz93M}eJKMz<am5#^-nGi&%NOIK1+W+KNB+x{Z_@gnRAtem}pzFf`*=j2dMr=JqzQ~lad z4=&U@el_G?D7i0;_%32B_v{yt+xK&h>X7Xd$XWT$Vfq@u#)rODawC6}WDnx#W74~F z9zJ`e#!aj!K6G{+a=m1YI79n0w7QS$^oO4aKbvnPXEetHI~^*vD2tzBXb=0fkV8d} z4cvO*Dkpo$iw7tBD`@kNM)oU=>}S7->?{6`vcK*iWuHWBDAy`4?dQD?eKX}OuXm>_ z&577RxT>xUr%wp;ytI>~-LBD>{eoqrslgJzYeG ze-1o-z0Un}-ll2%w}?OVPw0IVe4?MS^9N$?0r=Ij>9f#=HJjZtNRH@p`JacqL#Qj& zGydVqk?`iP34h7<6Z;nCAe$sc*GOUdaG^W6Q&?AAPtUGl6$FK&*k zy}#qU`iR26(I%^N4%e1^F@F90(v)wn#NT_Rd)AfnGsEJt&LM>F;-){wb8(y>-m=C zhG^i4_eTRy-VhD^$3G#<@%eK~6Z7ZvPtKpS=al?8dr!-s^TgErIZsZ{pYtCx^5;~o zA;@JJ=@WZNe{RmCto5^G_$)HGb@_&2?mNjvU6ofh)Vus#JTyPnos%BPmGelQ3&F=d z*PPKi{$5|4t=z!3wVi%v;pbn(j+xhS-n8%CH!JBlf=02h z2Y=0S4$~a_x%@ru!=Ep?IUM=v=URsklUBN>!Bl)lL{KMIS z(A#9(_ia;vgm--k8Bp;a9*l@m1~6O89BE;(->U*@Lb~;6o<^i#mO;` zbq5i{zann3#Ls9SKbk{B94d$D3|0L(ok>3AuE$4|Bdg9z!}zhOTg!ObNdCl}C;zF+ zJK&UewyR%1ziQkEBlF!}<~@7WHj#G(o4g}=v4wj#(b+_K8d==9xJrK?fS=PLb#Clj z|C${1aWBtzB#$4r#X)?w&ll8hKga1{Xnj_(9`p*I${IMa9x(b- zEZxg``EBw$jl=uXhvZKl4mm08%cUdDU9*Ul4+c8tpRw($>_(1}-Cie|<%{^p!P@O2 zucxb}C)!ob7lMP`kkiG7{7?CYtKYqO^PIb* zyl;PzIro!)!<_qC#f|dA|JIzVzR+>7$^XUte#3LA^Sje^t@(WuemP`*uXzq$$yH%K zeO7kS+Me9za=(vPeJGMA#}5}c3@u_G4EF5y!Kg>erup||Ku+B%z?&OK9hl;m9v@o zUf)eWLH&nuDbJAG%HuL~x%Me;Pr>~BZa=Qsc3h^dTVwnCQ~x0TE`MnbjN&zi@;x|> z?=;5e`gi%B{{4F7i+aC8x_Ghmh}J;Rx%G(FLdfrO4eOB&$XV2xiRz{MMEzRTPo@5} zILA2qPd)g2^DXj>@=d8<{=}HzQ(n8kC*yOP&K$zrk(`62YquWo`f@&^ar=Dl>dBXG zZRhcBJwe&cOZ|4%!GFa(bB>p}O)|&&i`UG$*8wj#cS~O7a9?ldZs#we zlTV7yUxUrVy6k80FWUq9!(5DiFMig;>}8~DZ`1iMo`=JC6tPc!>l^&+O^UN$V_U(+ z!O!j%to=86W&UW{SM=I%;Af$JiLZC(c;Jb{o77LU$7>BI->>Xjs?W~#`)1I0^++e; zt9&b;a*!eepf$t`F?rk z@PPeh^v)0RINz6?A>YLPT@w1UH0$)99&JJ%l@C3OFSvNnnFD_K2Vl(^u03P5V_KK! zGcqt^tl4MD>mlq>)aSpdPvunn<}qJJ*S*5+2CnGnyWUsyzE0)Lfqir88fFXjaBEb~ z#LnW~AbkT~-w#_dk?(oSPnZjN?!m25nEPfHujjn*dd|mIWZZWtb-hvVS#ONb%XB?l z@T>Vg0OQs5M&=%UCm=7F`?7tE5&6gW(~tVjWRG<3$@S6aX%?T0@pplLnZ-B9_&dN? z@yW;d+rb}e>3d@QZQ#d&ck=UW$!*d#W8pnI_pXCvsq5oDjJ^TQyACr-$}jP^5l&|Z&!r7{L zWa0hzy=)^HM#3k`MFsbs8*!)K7>V^dnzNtWz{{1lz|B>E!25XLB&?Ogy}|H(H?1E; zuC8aSo(FRIe-^o{O=~2{;_(yT6+OKX-wHT=uTW!n593sB6<#tn z|1Oq~c!~S;?a2+hCQ%>cuaYa!AWz`0x!IMAV+;2nbBI6t4ZCoz6Z9&1ipNx5uqV)GKl;`~YDrj29>^s`w3YIsg=koglHtX;+ydPS$Qw~cI))B2e?Q~Ltd zIeluL^gge9vbK$E<$iY)UW$Q9aSRM;r!|1)Zl`nclg{bK@$x#;JT1SU6Z^fBF$-Vq zXDWMup?PCnoZELr&7%thrF-rw%*)PKPO9r_(R__>s$Yl8WxDQFUKEbsi*dNKGVymD zg*)8iPRMkX`4>kQR*Aprl>=X>uh0j+&Gi-f?3jYSx3L~ukf(T0dW_3cpT&2it1{pG zn8H6=y1tn1qe;5rn1bf5GQaFt$M1$DU2beab6J@#AJeV#bfus#^p3vc@yH&2E#t

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

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

V#CnROdjw>|K=hmQ;o^fsrFR4K{ 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%0Dql(E@{_9NZebPwINLT+b(!yD+_0(P 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

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

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 '} + case $link in #( + /*) app_path=$link ;; #( + # Make relative path absolute, based on the path of the symlink: + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@" + +exec "$JAVACMD" "$@" diff --git a/ui/examples/native-hello-android/local.properties.template b/ui/examples/native-hello-android/local.properties.template new file mode 100644 index 0000000..b9f1e6d --- /dev/null +++ b/ui/examples/native-hello-android/local.properties.template @@ -0,0 +1,7 @@ +# local.properties — machine-local SDK/NDK paths. DO NOT commit. Copy to local.properties. +# +# Generate: Android Studio creates this automatically, or set manually: +# sdk.dir=/Users//Library/Android/sdk +# ndk.dir=/Users//Library/Android/sdk/ndk/ + +sdk.dir=/Users/will/Library/Android/sdk diff --git a/ui/examples/native-hello-android/settings.gradle b/ui/examples/native-hello-android/settings.gradle new file mode 100644 index 0000000..d9f3ff7 --- /dev/null +++ b/ui/examples/native-hello-android/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "native-hello-android" +include ':app' diff --git a/ui/examples/native-hello-ios/.gitignore b/ui/examples/native-hello-ios/.gitignore new file mode 100644 index 0000000..22a50dd --- /dev/null +++ b/ui/examples/native-hello-ios/.gitignore @@ -0,0 +1,4 @@ +build/ +*.xcuserstate +xcuserdata/ +NativeHello.xcodeproj/xcuserdata/ diff --git a/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj b/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d089ff0 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello.xcodeproj/project.pbxproj @@ -0,0 +1,377 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 4C6D9832BE495DD69A8573D9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FFDE02477855A55BC5F02CE /* main.m */; }; + 396FD073065B58F39358D798 /* el_uikit.m in Sources */ = {isa = PBXBuildFile; fileRef = 606F96EC1EDC54CD99E7D722 /* el_uikit.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 92BBC3E2AA055FCE840BF779 /* el_seed.c in Sources */ = {isa = PBXBuildFile; fileRef = 618FBE18EAA850D9B493A20F /* el_seed.c */; }; + 136C7763096C5037A69077AD /* el_runtime.c in Sources */ = {isa = PBXBuildFile; fileRef = A8CCF8316D83597CBB332064 /* el_runtime.c */; }; + 25136DF2A63F507EA5C7EF99 /* el_native_vessel.c in Sources */ = {isa = PBXBuildFile; fileRef = 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */; }; + C05D581DBD4E52F6B7F7303E /* native_hello.c in Sources */ = {isa = PBXBuildFile; fileRef = EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */; }; + C3D9A9987F0050EB859D9218 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF42D2E0FA0655149E573F7C /* UIKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8FFDE02477855A55BC5F02CE /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 606F96EC1EDC54CD99E7D722 /* el_uikit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = el_uikit.m; sourceTree = ""; }; + 618FBE18EAA850D9B493A20F /* el_seed.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_seed.c; sourceTree = ""; }; + A8CCF8316D83597CBB332064 /* el_runtime.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_runtime.c; sourceTree = ""; }; + 530AA788858357FBA923217E /* el_runtime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = el_runtime.h; sourceTree = ""; }; + E1A7B02609E95D1686523B80 /* el_native_target.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = el_native_target.h; sourceTree = ""; }; + 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = el_native_vessel.c; sourceTree = ""; }; + EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = native_hello.c; sourceTree = ""; }; + 08B6475D8C7952F48EF46B10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FF42D2E0FA0655149E573F7C /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + BDC23A5F3FCD5D1098144064 /* NativeHello.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeHello.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5673C72A848C5FFF94E7CF8F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C3D9A9987F0050EB859D9218 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05039DD2316A5B41A35E094E /* root */ = { + isa = PBXGroup; + children = ( + EBC5644CBC315AFDA6A6A59E /* NativeHello */, + D1E4BC2FC3925E6FAFCD38F6 /* Frameworks */, + 177F2C27230B527E80CEAED5 /* Products */, + ); + sourceTree = ""; + }; + 177F2C27230B527E80CEAED5 /* Products */ = { + isa = PBXGroup; + children = ( + BDC23A5F3FCD5D1098144064 /* NativeHello.app */, + ); + name = Products; + sourceTree = ""; + }; + EBC5644CBC315AFDA6A6A59E /* NativeHello */ = { + isa = PBXGroup; + children = ( + 8FFDE02477855A55BC5F02CE /* main.m */, + 606F96EC1EDC54CD99E7D722 /* el_uikit.m */, + 618FBE18EAA850D9B493A20F /* el_seed.c */, + A8CCF8316D83597CBB332064 /* el_runtime.c */, + 530AA788858357FBA923217E /* el_runtime.h */, + E1A7B02609E95D1686523B80 /* el_native_target.h */, + 3373ABFFE7FB5EFA9343AD02 /* el_native_vessel.c */, + EDA408C749EB5F3CA1E5EEC2 /* native_hello.c */, + 08B6475D8C7952F48EF46B10 /* Info.plist */, + ); + path = NativeHello; + sourceTree = ""; + }; + D1E4BC2FC3925E6FAFCD38F6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + FF42D2E0FA0655149E573F7C /* UIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F1275C1CDFD05A40B9D6C717 /* NativeHello */ = { + isa = PBXNativeTarget; + buildConfigurationList = 07AFC970D6B05512B39EDE6A /* Build configuration list for PBXNativeTarget "NativeHello" */; + buildPhases = ( + DF29C8DEE5CB5F1C83967F1F /* Sources */, + 5673C72A848C5FFF94E7CF8F /* Frameworks */, + D6D57F9604C45AAEA747645E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NativeHello; + productName = NativeHello; + productReference = BDC23A5F3FCD5D1098144064 /* NativeHello.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D8A823993D3254C7B6541F91 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1500; + TargetAttributes = { + F1275C1CDFD05A40B9D6C717 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 3DAB235BBB945E0CBB3EF554 /* Build configuration list for PBXProject "NativeHello" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05039DD2316A5B41A35E094E; + productRefGroup = 177F2C27230B527E80CEAED5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F1275C1CDFD05A40B9D6C717 /* NativeHello */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D6D57F9604C45AAEA747645E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DF29C8DEE5CB5F1C83967F1F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C6D9832BE495DD69A8573D9 /* main.m in Sources */, + 396FD073065B58F39358D798 /* el_uikit.m in Sources */, + 92BBC3E2AA055FCE840BF779 /* el_seed.c in Sources */, + 136C7763096C5037A69077AD /* el_runtime.c in Sources */, + 25136DF2A63F507EA5C7EF99 /* el_native_vessel.c in Sources */, + C05D581DBD4E52F6B7F7303E /* native_hello.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 071FC6E313815E66BB76FC93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_EXCEPTION = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + }; + name = Debug; + }; + 64E467FC48E757C0BDE1DFF6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_EXCEPTION = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5DAFA921569F54D081B65492 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "EL_TARGET_IOS=1", + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/NativeHello", + "$(inherited)", + ); + INFOPLIST_FILE = NativeHello/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CFLAGS = ( + "-DEL_TARGET_IOS", + "$(inherited)", + ); + OTHER_LDFLAGS = ( + "-ldl", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = ai.neurontechnologies.el.nativehello; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 81898B3044815511B8C31D64 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "EL_TARGET_IOS=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/NativeHello", + "$(inherited)", + ); + INFOPLIST_FILE = NativeHello/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CFLAGS = ( + "-DEL_TARGET_IOS", + "$(inherited)", + ); + OTHER_LDFLAGS = ( + "-ldl", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = ai.neurontechnologies.el.nativehello; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3DAB235BBB945E0CBB3EF554 /* Build configuration list for PBXProject "NativeHello" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 071FC6E313815E66BB76FC93 /* Debug */, + 64E467FC48E757C0BDE1DFF6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 07AFC970D6B05512B39EDE6A /* Build configuration list for PBXNativeTarget "NativeHello" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5DAFA921569F54D081B65492 /* Debug */, + 81898B3044815511B8C31D64 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = D8A823993D3254C7B6541F91 /* Project object */; +} diff --git a/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme b/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme new file mode 100644 index 0000000..6eefb67 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello.xcodeproj/xcshareddata/xcschemes/NativeHello.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/examples/native-hello-ios/NativeHello/Info.plist b/ui/examples/native-hello-ios/NativeHello/Info.plist new file mode 100644 index 0000000..8ab6a41 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ui/examples/native-hello-ios/NativeHello/el_native_target.h b/ui/examples/native-hello-ios/NativeHello/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c b/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c new file mode 100644 index 0000000..0682965 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int el_vessel_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello-ios/NativeHello/el_runtime.c b/ui/examples/native-hello-ios/NativeHello/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _tl_arena.count = 0; +} + +/* ── Scoped arena for CLI use ─────────────────────────────────────────────── * + * CLI programs never call el_request_start/end, so all strdup allocations are + * permanent. el_arena_push/pop let the compiler free intermediate strings + * after each compilation unit. + * + * el_arena_push() — activates the arena if not already active, saves the + * current arena count as a mark, and returns it as an el_val_t Int. + * el_arena_pop(mark) — frees all strings allocated since the push mark and + * resets the count. If count reaches 0, deactivates the arena. + */ +#define EL_ARENA_SCOPE_DEPTH 32 +static _Thread_local size_t _tl_arena_scope[EL_ARENA_SCOPE_DEPTH]; +static _Thread_local int _tl_arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_tl_arena_active) { + _tl_arena_active = 1; + } + if (_tl_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) { + _tl_arena_scope[_tl_arena_scope_depth++] = _tl_arena.count; + } + return (el_val_t)(int64_t)_tl_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _tl_arena.count) save = 0; + for (size_t i = save; i < _tl_arena.count; i++) { + if (_tl_arena.ptrs[i]) { + free(_tl_arena.ptrs[i]); + _tl_arena.ptrs[i] = NULL; + } + } + _tl_arena.count = save; + if (_tl_arena_scope_depth > 0) _tl_arena_scope_depth--; + if (save == 0) _tl_arena_active = 0; + return 0; +} + +/* Persistent allocation — bypasses the arena (state_set, engram internals). */ +static char* el_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +el_val_t print(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) fputs(str, stdout); + return 0; +} + +el_val_t readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +/* __read_n — read exactly n bytes from stdin. + * Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read + * exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and + * returns the buffer as an El String. Returns "" on EOF or I/O error. + * + * Used by the El LSP server to read JSON-RPC message bodies after parsing + * the Content-Length header. readline() cannot be used for the body because + * it stops at the first \n and LSP JSON bodies are not newline-terminated. */ +el_val_t __read_n(el_val_t nv) { + int64_t n = EL_INT(nv); + if (n <= 0) return el_wrap_str(el_strdup("")); + char* buf = malloc((size_t)n + 1); + if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)n, stdin); + buf[got] = '\0'; + if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); } + /* Track in arena so the allocation is freed when the request ends. */ + el_arena_track(buf); + return el_wrap_str(buf); +} + +/* __print_raw — write a string to stdout without any modification. + * Unlike println/print (which call puts/fputs and may add newlines or flush + * in platform-specific ways), this uses fwrite with the exact byte count so + * that embedded \r\n pairs in LSP Content-Length headers survive intact. */ +void __print_raw(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return; + size_t len = strlen(s); + fwrite(s, 1, len, stdout); + fflush(stdout); +} + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)atoll(s); +} + +/* native_str_to_int — El compiler-generated alias for str_to_int. + * Converts a string el_val_t to its integer representation. */ +el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); } + +el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * the loop is observable. + */ + +/* ── JSON error helper (used by HTTP, PQ, crypto stubs) ─────────────────── */ + +/* JSON-escape an arbitrary C string into an allocated buffer. */ +static char* json_escape_alloc(const char* s) { + if (!s) return el_strdup(""); + JsonBuf b; jb_init(&b); + for (const char* p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': jb_puts(&b, "\\\""); break; + case '\\': jb_puts(&b, "\\\\"); break; + case '\n': jb_puts(&b, "\\n"); break; + case '\r': jb_puts(&b, "\\r"); break; + case '\t': jb_puts(&b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(&b, tmp); + } else jb_putc(&b, (char)c); + } + } + return b.buf; +} + +static el_val_t http_error_json(const char* msg) { + char* esc = json_escape_alloc(msg ? msg : "unknown error"); + char* buf = el_strbuf(strlen(esc) + 16); + sprintf(buf, "{\"error\":\"%s\"}", esc); + free(esc); + return el_wrap_str(buf); +} + +#ifdef HAVE_CURL +/* ── HTTP client write-callback buffer ───────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* HTTP timeout (ms) — read once from EL_HTTP_TIMEOUT_MS, default 60000. + * Applied via CURLOPT_TIMEOUT_MS on every libcurl request. */ +static long _el_http_timeout_ms = -1; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* http_post_json_with_headers — POST with Content-Type: application/json plus + * any additional headers supplied as an El map. Combines http_post_json and + * http_post_with_headers: the Content-Type header is always prepended so + * callers do not have to include it in their map. */ +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + /* Append caller-supplied headers from the map */ + ElMap* m = as_map(headers_map); + if (m) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve(el_val_t port, el_val_t handler) { + /* If `handler` looks like a string name, register it as the active handler. */ + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; } + /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6. + * This makes `localhost` work in browsers that resolve it to ::1 first. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler_v2(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler4); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler_v2(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + return 0; + } + /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + return 1; +} + +// stdout_to_file / stdout_restore — redirect process stdout to a file and +// restore it. Used by the compiler's JS post-processing pipeline to capture +// codegen output before piping through terser / obfuscator. +#include +static int _el_saved_stdout_fd = -1; + +el_val_t stdout_to_file(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return (el_val_t)(int64_t)-1; + fflush(stdout); + _el_saved_stdout_fd = dup(STDOUT_FILENO); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return (el_val_t)(int64_t)-1; + dup2(fd, STDOUT_FILENO); + close(fd); + return (el_val_t)(int64_t)0; +} + +el_val_t stdout_restore(void) { + if (_el_saved_stdout_fd >= 0) { + fflush(stdout); + dup2(_el_saved_stdout_fd, STDOUT_FILENO); + close(_el_saved_stdout_fd); + _el_saved_stdout_fd = -1; + } + return (el_val_t)(int64_t)0; +} + +// exec_command — run a shell command, return exit code (0 = success). +// Used by elb and other El tooling to invoke subprocesses. +el_val_t exec_command(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + return lst; +} + +/* fs_list_json — return directory entries as a JSON array of strings. + * Returns "[]" for missing or non-directory paths. Excludes "." and "..". */ +el_val_t fs_list_json(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return EL_STR("[]"); + DIR* d = opendir(path); + if (!d) return EL_STR("[]"); + /* Collect entries first so we can build the JSON in one pass. */ + char** names = NULL; + size_t count = 0, cap = 0; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + if (count >= cap) { + cap = cap ? cap * 2 : 16; + names = realloc(names, cap * sizeof(char*)); + if (!names) { closedir(d); return EL_STR("[]"); } + } + names[count++] = strdup(e->d_name); + } + closedir(d); + /* Build JSON array. */ + size_t sz = 3; /* "[]" + NUL */ + for (size_t i = 0; i < count; i++) sz += strlen(names[i]) * 2 + 6; /* conservative */ + char* buf = malloc(sz); + if (!buf) { for (size_t i = 0; i < count; i++) free(names[i]); free(names); return EL_STR("[]"); } + size_t pos = 0; + buf[pos++] = '['; + for (size_t i = 0; i < count; i++) { + if (i > 0) buf[pos++] = ','; + buf[pos++] = '"'; + for (const char* p = names[i]; *p; p++) { + if (*p == '"' || *p == '\\') buf[pos++] = '\\'; + else if (*p == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; continue; } + else if (*p == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; continue; } + buf[pos++] = *p; + } + buf[pos++] = '"'; + free(names[i]); + } + free(names); + buf[pos++] = ']'; + buf[pos] = '\0'; + return el_wrap_str(buf); +} + +/* fs_exists — true iff stat(path) succeeds. Symlinks are followed. */ +el_val_t fs_exists(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* ── html_raw ──────────────────────────────────────────────────────────────── + * Identity passthrough for raw HTML template interpolation. + * El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is + * without any escaping. The caller is responsible for safety. + */ +el_val_t html_raw(el_val_t s) { + return s; +} + +/* ── html_escape ───────────────────────────────────────────────────────────── + * Escape < > " ' & for safe HTML text interpolation. + * El's {expr} in HTML templates compiles to html_escape(expr). + */ +el_val_t html_escape(el_val_t sv) { + const char* src = EL_CSTR(sv); + if (!src) return EL_STR(""); + size_t len = strlen(src); + /* Worst case: every byte → 6 chars (") */ + char* out = (char*)malloc(len * 6 + 1); + if (!out) return sv; + el_arena_track(out); + char* p = out; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + switch (c) { + case '&': memcpy(p, "&", 5); p += 5; break; + case '<': memcpy(p, "<", 4); p += 4; break; + case '>': memcpy(p, ">", 4); p += 4; break; + case '"': memcpy(p, """, 6); p += 6; break; + case '\'': memcpy(p, "'", 5); p += 5; break; + default: *p++ = (char)c; break; + } + } + *p = '\0'; + return el_wrap_str(out); +} + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input, allowlist_json) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); break; + case '\n': jb_puts(b, "\\n"); break; + case '\r': jb_puts(b, "\\r"); break; + case '\t': jb_puts(b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; + snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(b, tmp); + } else { + jb_putc(b, (char)c); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + /* raw_val is the JSON value as-is (already encoded by the caller). + * If it looks like a plain (non-JSON) string, wrap it as a JSON string. + * Convention: callers pass pre-encoded values like "\"bob\"" for strings, + * "42" for numbers, "true"/"false" for booleans. */ + const char* raw_val = EL_CSTR(value); + if (!k) k = ""; + if (!raw_val) raw_val = "null"; + if (!json || !*json) { + /* Build a fresh object */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(parsed); +} + +/* json_escape_string — escape a string value for embedding in JSON. + * Returns the escaped content WITHOUT surrounding quotes. + * "say \"hello\"" -> "say \\\"hello\\\"" */ +el_val_t json_escape_string(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + /* Worst case: every char needs a 2-char escape. */ + char* out = malloc(n * 2 + 1); + if (!out) return el_wrap_str(el_strdup("")); + size_t j = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '"') { out[j++] = '\\'; out[j++] = '"'; } + else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; } + else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; } + else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; } + else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; } + else { out[j++] = (char)c; } + } + out[j] = '\0'; + el_val_t result = el_wrap_str(el_strdup(out)); + free(out); + return result; +} + +/* json_build_object — build a JSON object from a flat key-value list. + * kvs is [key0, val0, key1, val1, ...]. Values are raw JSON (pass + * strings as "\"value\"" or use json_escape_string). */ +el_val_t json_build_object(el_val_t kvs) { + el_val_t list = kvs; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + int first = 1; + for (int64_t i = 0; i + 1 < n; i += 2) { + el_val_t k = el_list_get(list, (el_val_t)i); + el_val_t v = el_list_get(list, (el_val_t)(i + 1)); + const char* ks = EL_CSTR(k); + const char* vs = EL_CSTR(v); + if (!ks || !vs) continue; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '"'); + jb_puts(&b, ks); + jb_puts(&b, "\":\""); + /* escape the value string */ + size_t vn = strlen(vs); + for (size_t j = 0; j < vn; j++) { + unsigned char c = (unsigned char)vs[j]; + if (c == '"') { jb_putc(&b, '\\'); jb_putc(&b, '"'); } + else if (c == '\\') { jb_putc(&b, '\\'); jb_putc(&b, '\\'); } + else if (c == '\n') { jb_putc(&b, '\\'); jb_putc(&b, 'n'); } + else if (c == '\r') { jb_putc(&b, '\\'); jb_putc(&b, 'r'); } + else if (c == '\t') { jb_putc(&b, '\\'); jb_putc(&b, 't'); } + else { jb_putc(&b, (char)c); } + } + jb_putc(&b, '"'); + } + jb_putc(&b, '}'); + return el_wrap_str(b.buf); +} + +/* json_build_array — build a JSON array from a list of raw JSON values. + * items is ["\"alpha\"", "\"beta\"", "42", "true", ...]. */ +el_val_t json_build_array(el_val_t items) { + el_val_t list = items; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < n; i++) { + el_val_t v = el_list_get(list, (el_val_t)i); + const char* vs = EL_CSTR(v); + if (!vs) continue; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, vs); + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + if (!fmt_str || *fmt_str == '\0' || strcmp(fmt_str, "ISO") == 0) { + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* Return a JSON string so callers can use json_get to extract fields. */ + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"year\":%d,\"month\":%d,\"day\":%d,\"hour\":%d,\"minute\":%d,\"second\":%d,\"ms\":%d}", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +el_val_t now(void) { + return el_now_instant(); +} + +/* now_ns — return current Unix time as nanoseconds (Int). + * Thin wrapper over el_now_instant for use in test timing. */ +el_val_t now_ns(void) { + return el_now_instant(); +} + +/* unix_seconds(n) — Instant from a Unix-epoch second count. + * unix_millis(n) — Instant from a Unix-epoch millisecond count. */ +el_val_t unix_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + if (i > 0) jb_putc(&b, ','); + jb_putc(&b, '"'); + jb_emit_escaped(&b, _state_entries[i].key); + jb_putc(&b, '"'); + } + jb_putc(&b, ']'); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(b.buf); +} + +/* Returns 1 (true) if the key is present in the state store, else 0 (false). */ +el_val_t state_has(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + int found = (e != NULL) ? 1 : 0; + pthread_mutex_unlock(&_state_mu); + return (el_val_t)found; +} + +/* Returns the value for key, or default_val if the key is absent. */ +el_val_t state_get_or(el_val_t key, el_val_t default_val) { + const char* k = EL_CSTR(key); + if (!k) return default_val; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + char* copy = el_strdup(e->value); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(copy); + } + pthread_mutex_unlock(&_state_mu); + return default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + double v = el_to_float(f); + /* Normalize NaN to "nan" regardless of sign — platform-independent. */ + if (isnan(v)) { + snprintf(buf, sizeof(buf), "nan"); + } else { + snprintf(buf, sizeof(buf), "%g", v); + } + return el_wrap_str(el_strdup(buf)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code) { + exit((int)code); + return 0; /* unreachable */ +} + +/* getpid_now — current process id. Named with the _now suffix to avoid + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +el_val_t getpid_now(void) { + return (el_val_t)getpid(); +} + +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + +/* ── args() — command-line argument access ────────────────────────────────── + * Compiled El programs call args() to get a list of CLI arguments. + * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* Use plain strdup (not el_strdup) so arena doesn't track these pointers. + * The BFS loop manually frees them below — arena would double-free them. */ + frontier[fc] = strdup(sid); frontier_h[fc] = 0; fc++; + visited[vc++] = strdup(sid); + + int first = 1; + while (fc > 0) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + if (vc < 1024) visited[vc++] = strdup(peer); + if (fc < 1024 && h + 1 < depth) { frontier[fc] = strdup(peer); frontier_h[fc] = h + 1; fc++; } + } + free(cur); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(buf)); +} + +#ifdef HAVE_CURL +/* ── DHARMA network ───────────────────────────────────────────────────────── + * Real implementation. Peers are addressed by `dharma_id` — either bare + * (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * and returns a JSON-string el_val_t result. Iteration is capped at 10. + */ + +#ifdef HAVE_CURL +static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5"; +static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages"; +static const char* LLM_VERSION = "2023-06-01"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5"))); + return lst; +} +#endif /* HAVE_CURL */ + +/* ── Native VM builtin aliases ────────────────────────────────────────────── + * El source files use native_* names (El VM builtins). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + return trace_span_end(h); +} + +#endif /* HAVE_CURL — OTLP */ + +/* ── Threading seed primitives ─────────────────────────────────────────────── + * __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid + * __thread_join(tid) -> String join thread, return result string + * __mutex_new() -> Int allocate a mutex, return handle + * __mutex_lock(m) lock mutex m + * __mutex_unlock(m) unlock mutex m + * + * Every El fn compiles to a global C symbol. __thread_create uses dlsym to + * look up the function by name and run it in a pthread. This means any El fn + * with signature (String) -> String is directly threadable. + */ + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + ElFn1 fn; + el_val_t arg; + el_val_t result; +} ElThreadArg; + +#define EL_THREAD_MAX 256 + +typedef struct { + pthread_t tid; + ElThreadArg* arg; + int alive; +} ElThread; + +static ElThread _threads[EL_THREAD_MAX]; +static int _thread_count = 0; +static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +static void* el_thread_runner(void* raw) { + ElThreadArg* a = (ElThreadArg*)raw; + a->result = a->fn(a->arg); + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) { + const char* sym = EL_CSTR(fn_name_v); + if (!sym || !*sym) return EL_INT(-1); + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym); + return EL_INT(-1); + } + ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg)); + if (!a) return EL_INT(-1); + a->fn = (ElFn1)p; + a->arg = arg_v; + a->result = EL_STR(""); + + pthread_mutex_lock(&_thread_alloc_mu); + if (_thread_count >= EL_THREAD_MAX) { + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + fprintf(stderr, "[__thread_create] thread table full\n"); + return EL_INT(-1); + } + int slot = _thread_count++; + _threads[slot].arg = a; + _threads[slot].alive = 1; + pthread_mutex_unlock(&_thread_alloc_mu); + + if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) { + pthread_mutex_lock(&_thread_alloc_mu); + _thread_count--; + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + return EL_INT(-1); + } + return EL_INT(slot); +} + +el_val_t __thread_join(el_val_t tid_v) { + int slot = (int)(int64_t)tid_v; + if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR(""); + pthread_join(_threads[slot].tid, NULL); + el_val_t result = _threads[slot].arg->result; + free(_threads[slot].arg); + _threads[slot].alive = 0; + return result; +} + +/* Mutex table */ + +#define EL_MUTEX_MAX 64 + +typedef struct { + pthread_mutex_t mu; + int allocated; +} ElMutexEntry; + +static ElMutexEntry _mutexes[EL_MUTEX_MAX]; +static int _mutex_count = 0; +static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_mutex_alloc_mu); + if (_mutex_count >= EL_MUTEX_MAX) { + pthread_mutex_unlock(&_mutex_alloc_mu); + fprintf(stderr, "[__mutex_new] mutex table full\n"); + return EL_INT(-1); + } + int slot = _mutex_count++; + pthread_mutex_init(&_mutexes[slot].mu, NULL); + _mutexes[slot].allocated = 1; + pthread_mutex_unlock(&_mutex_alloc_mu); + return EL_INT(slot); +} + +void __mutex_lock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_lock(&_mutexes[slot].mu); +} + +void __mutex_unlock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_unlock(&_mutexes[slot].mu); +} + +/* ── Channels ─────────────────────────────────────────────────────────────── * + * Buffered MPMC channel backed by a mutex + condvar + circular buffer. + * channel_new(capacity) -> Int (handle) + * channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded) + * channel_recv(ch) -> String — blocks until a message is available + * channel_try_recv(ch) -> String — non-blocking, returns "" if empty + * channel_close(ch) — signal no more sends; recv drains remaining + * + * Bounded channels (cap > 0): circular buffer, sender blocks when full. + * Unbounded channels (cap == 0): dynamic array, sender never blocks. + */ +#define EL_CHANNEL_MAX 64 +#define EL_CHANNEL_BUF 1024 + +typedef struct { + char** buf; + int cap; /* 0 = unbounded (grows dynamically) */ + int head, tail, count; + int dyn_cap; /* allocated slots for unbounded mode */ + int closed; + pthread_mutex_t mu; + pthread_cond_t not_empty; + pthread_cond_t not_full; +} ElChannel; + +static ElChannel _channels[EL_CHANNEL_MAX]; +static int _channel_count = 0; +static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __channel_new(el_val_t capacity_v) { + int cap = (int)(int64_t)capacity_v; + if (cap < 0) cap = 0; + + pthread_mutex_lock(&_channel_alloc_mu); + if (_channel_count >= EL_CHANNEL_MAX) { + pthread_mutex_unlock(&_channel_alloc_mu); + fprintf(stderr, "[__channel_new] channel table full\n"); + return EL_INT(-1); + } + int slot = _channel_count++; + pthread_mutex_unlock(&_channel_alloc_mu); + + ElChannel* ch = &_channels[slot]; + memset(ch, 0, sizeof(*ch)); + ch->cap = cap; + ch->closed = 0; + ch->head = 0; + ch->tail = 0; + ch->count = 0; + + if (cap > 0) { + /* Bounded: fixed circular buffer. */ + ch->buf = (char**)malloc((size_t)cap * sizeof(char*)); + ch->dyn_cap = cap; + } else { + /* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */ + ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*)); + ch->dyn_cap = EL_CHANNEL_BUF; + } + if (!ch->buf) { + fprintf(stderr, "[__channel_new] out of memory\n"); + return EL_INT(-1); + } + + pthread_mutex_init(&ch->mu, NULL); + pthread_cond_init(&ch->not_empty, NULL); + pthread_cond_init(&ch->not_full, NULL); + + return EL_INT(slot); +} + +void __channel_send(el_val_t ch_v, el_val_t msg_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + const char* msg = EL_CSTR(msg_v); + if (!msg) msg = ""; + char* copy = strdup(msg); /* channel owns the string */ + + pthread_mutex_lock(&ch->mu); + + if (ch->closed) { + /* Send on closed channel is a no-op (drop the message). */ + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + + if (ch->cap > 0) { + /* Bounded: block while full. */ + while (ch->count >= ch->cap && !ch->closed) { + pthread_cond_wait(&ch->not_full, &ch->mu); + } + if (ch->closed) { + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->cap; + ch->count++; + } else { + /* Unbounded: grow the buffer if needed. */ + if (ch->count >= ch->dyn_cap) { + int new_cap = ch->dyn_cap * 2; + char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*)); + if (!grown) { + pthread_mutex_unlock(&ch->mu); + free(copy); + fprintf(stderr, "[__channel_send] out of memory growing channel\n"); + return; + } + /* The circular buffer may have wrapped. Linearise it first. + * In unbounded mode head is always 0 (we append at tail, drain + * from head), so a simple memmove isn't needed — but if the + * buffer did wrap (tail < head after growth), we need to fix up. + * Simplest safe path: if tail wrapped, move the head..old_cap + * segment to new_cap..new_cap+(old_cap-head). */ + if (ch->tail < ch->head) { + /* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */ + int front = ch->dyn_cap - ch->head; + memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*)); + ch->head = ch->dyn_cap; + } + ch->buf = grown; + ch->dyn_cap = new_cap; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->dyn_cap; + ch->count++; + } + + pthread_cond_signal(&ch->not_empty); + pthread_mutex_unlock(&ch->mu); +} + +el_val_t __channel_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + /* Block until there is a message or the channel is closed and drained. */ + while (ch->count == 0 && !ch->closed) { + pthread_cond_wait(&ch->not_empty, &ch->mu); + } + + if (ch->count == 0) { + /* Closed and empty — signal EOF. */ + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + /* Hand the string to the arena so it is freed after the request. */ + el_arena_track(msg); + return EL_STR(msg); +} + +el_val_t __channel_try_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + if (ch->count == 0) { + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + el_arena_track(msg); + return EL_STR(msg); +} + +void __channel_close(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + ch->closed = 1; + /* Wake all blocked recvers and senders so they can observe the close. */ + pthread_cond_broadcast(&ch->not_empty); + pthread_cond_broadcast(&ch->not_full); + pthread_mutex_unlock(&ch->mu); +} + +/* ── DHARMA runtime additions ──────────────────────────────────────────────── + * + * Functions required by the dharma registry service. Added here so the + * released el_runtime.c includes them without requiring dharma to bundle + * its own stubs. + * + * Functions added: + * list_len — alias for el_list_len (used in handlers.el) + * list_get — alias for el_list_get (used in handlers.el) + * json_array_push — append a pre-encoded JSON element to a JSON array string + * now_millis — milliseconds since Unix epoch (alias for time_now) + * unix_timestamp_ms — same as now_millis (alias) + * time_now_ms — same as now_millis (alias) + * log_info — stderr structured log at INFO level + * log_warn — stderr structured log at WARN level + * config — reads a config value from the environment + * http_patch — HTTP PATCH with JSON Content-Type + * http_post_engram — HTTP POST with optional X-API-Key header + * http_get_engram — HTTP GET with optional X-API-Key header + * str_to_bytes — encode a string as a JSON array of byte values + * bytes_to_str — decode a JSON array of byte values back to a string + * hash_sha256 — SHA-256 hex digest of a string + */ + +/* list_len — return the number of elements in a list. */ +el_val_t list_len(el_val_t list) { + return el_list_len(list); +} + +/* list_get — return the element at index i in a list. */ +el_val_t list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\"" + * or "42") to the JSON array string arr. Returns a new JSON array string. + * Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" + * json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */ +el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) { + const char* arr = EL_CSTR(arr_v); + const char* elem = EL_CSTR(elem_v); + if (!arr || !*arr) arr = "[]"; + if (!elem || !*elem) elem = "null"; + + /* Trim whitespace, find the closing ']'. */ + const char* p = arr; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { + /* Not an array — return a single-element array. */ + size_t n = strlen(elem) + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + size_t arr_len = strlen(arr); + size_t elem_len = strlen(elem); + + /* Walk from the end to find the matching ']'. */ + const char* end = arr + arr_len - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; + if (*end != ']') { + /* Malformed — wrap elem in a new array. */ + size_t n = elem_len + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + + /* Content between '[' and ']'. */ + const char* inner_start = p + 1; + const char* inner_end = end; /* points AT ']' */ + /* Check if the array is empty (only whitespace between brackets). */ + const char* q = inner_start; + while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++; + int empty = (q == inner_end); + + /* Build: prefix + (comma if non-empty) + elem + "]" */ + size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */ + size_t sep_len = empty ? 0 : 1; /* "," if non-empty */ + size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */ + char* out = el_strbuf(out_len); + memcpy(out, arr, prefix_len); + if (!empty) out[prefix_len] = ','; + memcpy(out + prefix_len + sep_len, elem, elem_len); + out[prefix_len + sep_len + elem_len] = ']'; + out[prefix_len + sep_len + elem_len + 1] = '\0'; + return el_wrap_str(out); +} + +/* now_millis — milliseconds since Unix epoch. */ +el_val_t now_millis(void) { + return time_now(); +} + +/* unix_timestamp_ms — same as now_millis. */ +el_val_t unix_timestamp_ms(void) { + return time_now(); +} + +/* time_now_ms — same as now_millis. */ +el_val_t time_now_ms(void) { + return time_now(); +} + +/* log_info — write a structured [INFO] line to stderr. */ +void log_info(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[INFO] %s\n", msg ? msg : ""); +} + +/* log_warn — write a structured [WARN] line to stderr. */ +void log_warn(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[WARN] %s\n", msg ? msg : ""); +} + +/* config — read a configuration value from the environment. + * Returns "" if the variable is not set (same as __env_get). */ +el_val_t config(el_val_t key_v) { + const char* key = EL_CSTR(key_v); + if (!key || !*key) return EL_STR(""); + const char* val = getenv(key); + if (!val) return EL_STR(""); + return el_wrap_str(el_strdup(val)); +} + +#ifdef HAVE_CURL +/* http_patch — HTTP PATCH request with Content-Type: application/json. + * Returns the response body (same error convention as http_post_json). */ +el_val_t http_patch(el_val_t url_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_post_engram — HTTP POST with optional X-API-Key header. + * If key is "" no authentication header is sent. */ +el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_get_engram — HTTP GET with optional X-API-Key header. */ +el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + if (h) curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} +#endif /* HAVE_CURL */ + +/* str_to_bytes — encode a string as a JSON array of unsigned byte values. + * "hello" -> "[104,101,108,108,111]" + * Used by db.el to store binary content in Engram JSON nodes. */ +el_val_t str_to_bytes(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return el_wrap_str(el_strdup("[]")); + size_t n = strlen(s); + /* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */ + char* out = el_strbuf(n * 4 + 3); + size_t pos = 0; + out[pos++] = '['; + for (size_t i = 0; i < n; i++) { + unsigned char b = (unsigned char)s[i]; + if (i > 0) out[pos++] = ','; + /* Write decimal representation of b. */ + if (b >= 100) { + out[pos++] = (char)('0' + b / 100); + out[pos++] = (char)('0' + (b / 10) % 10); + out[pos++] = (char)('0' + b % 10); + } else if (b >= 10) { + out[pos++] = (char)('0' + b / 10); + out[pos++] = (char)('0' + b % 10); + } else { + out[pos++] = (char)('0' + b); + } + } + out[pos++] = ']'; + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* bytes_to_str — decode a JSON array of integer byte values back to a string. + * "[104,101,108,108,111]" -> "hello" + * Inverse of str_to_bytes. */ +el_val_t bytes_to_str(el_val_t arr_v) { + const char* s = EL_CSTR(arr_v); + if (!s) return el_wrap_str(el_strdup("")); + /* Skip whitespace, expect '['. */ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + + /* Count elements to size the output buffer. */ + int64_t n = (int64_t)json_array_len(arr_v); + if (n <= 0) return el_wrap_str(el_strdup("")); + + char* out = el_strbuf((size_t)n + 1); + size_t pos = 0; + + /* Walk the array, parse each integer, store as a byte. */ + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']' || *s == '\0') break; + /* Parse decimal integer. */ + char* end_ptr; + long v = strtol(s, &end_ptr, 10); + if (end_ptr == s) break; /* parse failure */ + s = end_ptr; + if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* hash_sha256 — return the SHA-256 hex digest of a string. + * Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */ +el_val_t hash_sha256(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)s, strlen(s), digest); + return el_hex_encode(digest, 32); +} + +/* ── __ prefixed aliases — public boundary for compiled El programs ────────── + * + * The El compiler's self-hosting back-end emits calls to __-prefixed function + * names (e.g. __println, __str_len). These wrappers forward to the existing + * el_runtime implementations so both naming conventions resolve at link time. + * + * Note: __thread_create and __thread_join are already defined above in the + * threading section; they are not repeated here. + * ──────────────────────────────────────────────────────────────────────────── */ + +/* I/O */ +el_val_t __println(el_val_t s) { return println(s); } +el_val_t __print(el_val_t s) { return print(s); } +el_val_t __readline(void) { return readline(); } + +/* String */ +el_val_t __int_to_str(el_val_t n) { return int_to_str(n); } +el_val_t __str_to_int(el_val_t s) { return str_to_int(s); } +el_val_t __float_to_str(el_val_t f) { return float_to_str(f); } +el_val_t __str_to_float(el_val_t s) { return str_to_float(s); } +el_val_t __str_len(el_val_t s) { return str_len(s); } +el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); } + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strcmp(ca, cb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strncmp(ca, cb, (size_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); } +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); } + +el_val_t __str_alloc(el_val_t n) { + if (n <= 0) n = 0; + char* buf = el_strbuf((size_t)n + 1); + memset(buf, 0, (size_t)n + 1); + return el_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* buf = (char*)(uintptr_t)s; + if (buf) buf[(size_t)i] = (char)c; + return s; +} + +/* URL encoding */ +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* Environment */ +el_val_t __env_get(el_val_t key) { return env(key); } + +/* Subprocess */ +el_val_t __exec(el_val_t cmd) { return exec(cmd); } +el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); } + +/* Process */ +el_val_t __exit_program(el_val_t code) { return exit_program(code); } + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path) { return fs_exists(path); } +el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); } +el_val_t __fs_read(el_val_t path) { return fs_read(path); } +el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); } +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); } +el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); } + +/* HTTP server (no curl dependency) */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); } +el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); } +el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); } + +/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here + * so el_runtime.c compiles standalone. When both translation units are linked + * the el_seed.c definitions win via their non-static linkage (strong symbols). + * These stubs are marked weak so they are silently overridden. */ +__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); } +__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; } +__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; } +__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; } + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); } +el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); } +el_val_t __json_array_len(el_val_t json) { return json_array_len(json); } +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); } +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); } + +/* State K/V */ +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_keys(void) { return state_keys(); } +el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); } + +/* UUID */ +el_val_t __uuid_v4(void) { return uuid_v4(); } + +/* Args */ +el_val_t __args_json(void) { return args(); } + +/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below + * with a matching stub in the #ifndef HAVE_CURL block. */ +#ifdef HAVE_CURL +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms) { + /* timeout_ms is accepted for API compatibility but ignored here; + * el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */ + (void)timeout_ms; + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map — same as __http_do but headers_map arg is a JSON-string + * rather than an ElMap. Parse it first, then delegate. */ +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + (void)timeout_ms; + /* Build a curl_slist from a JSON object {"Header":"value",...}. */ + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + /* Walk the JSON pairs with a simple parser reusing json_get_string logic. */ + /* For correctness we just call the existing json_get iteration path. + * We duplicate the key-extraction loop from headers_from_map but driven + * by JSON rather than ElMap. Use json_get_raw to iterate is not easy + * without knowing keys, so accept the JSON string and build a tmp map. */ + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map_to_file — same as __http_do_map but streams response body + * to a local file path rather than returning it as a string. */ +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path) { + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), + h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +#ifndef HAVE_CURL +/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── * + * These return a JSON error string so El programs get a clear message if they + * call HTTP/LLM functions in a curl-less build. */ +static el_val_t _no_curl_err(void) { + return el_wrap_str(el_strdup("{\"error\":\"not built with HAVE_CURL\"}")); +} +el_val_t http_get(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); } +el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t p) { (void)url; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t p) { (void)url; (void)b; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_engram(el_val_t url, el_val_t k, el_val_t b) { (void)url; (void)k; (void)b; return _no_curl_err(); } +el_val_t http_get_engram(el_val_t url, el_val_t k) { (void)url; (void)k; return _no_curl_err(); } +el_val_t llm_call(el_val_t m, el_val_t p) { (void)m; (void)p; return _no_curl_err(); } +el_val_t llm_call_system(el_val_t m, el_val_t s, el_val_t u) { (void)m; (void)s; (void)u; return _no_curl_err(); } +el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (void)m; (void)s; (void)u; (void)t; return _no_curl_err(); } +el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); } +el_val_t llm_models(void) { return el_list_empty(); } +void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; } +/* __ HTTP stubs (no-curl build) */ +el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); } +#endif /* !HAVE_CURL */ diff --git a/ui/examples/native-hello-ios/NativeHello/el_runtime.h b/ui/examples/native-hello-ios/NativeHello/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * EL_STR(s) cast string literal to el_val_t + * EL_CSTR(v) cast el_val_t back to const char* + * EL_INT(v) identity — el_val_t is already int64_t + * EL_NULL null / zero value + * EL_FALSE boolean false (0) + * EL_TRUE boolean true (1) + * + * Link requirements: + * -lcurl — required for the HTTP client (http_get, http_post, llm_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#define EL_INT(v) (v) +#define EL_NULL ((el_val_t)0) +#define EL_FALSE ((el_val_t)0) +#define EL_TRUE ((el_val_t)1) + +/* Float values share the el_val_t (int64) slot via a bit-cast. + * The codegen emits Float literals as `el_from_float()` so the + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +el_val_t str_len(el_val_t s); +el_val_t str_concat(el_val_t a, el_val_t b); +el_val_t int_to_str(el_val_t n); +el_val_t str_to_int(el_val_t s); +el_val_t native_str_to_int(el_val_t s); +el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end); +el_val_t str_contains(el_val_t s, el_val_t sub); +el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +void el_retain(el_val_t v); +void el_release(el_val_t v); + +/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */ +el_val_t el_arena_push(void); +el_val_t el_arena_pop(el_val_t mark); + +/* ── List ────────────────────────────────────────────────────────────────── */ + +el_val_t el_list_new(el_val_t count, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * auto-content-type contract for legacy handlers that return plain bodies. */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* SSE connection fd — set by http_worker_v2 before calling the El handler, + * cleared afterwards. Defined in el_seed.c; called from el_runtime.c. + * The getter is exposed as __http_conn_fd() to El programs. */ +void el_seed_set_http_conn_fd(int fd); + +/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default + * 60000ms). Read lazily on first use, so setting the env var any time before + * the first http_* call is sufficient. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"p":[],"a":["href","title"],"strong":[],...} + * where each value is the array of attribute names allowed for that tag. */ +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t html_raw(el_val_t s); +el_val_t html_escape(el_val_t s); + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t path); +el_val_t fs_write(el_val_t path, el_val_t content); +el_val_t fs_list(el_val_t path); +el_val_t fs_list_json(el_val_t path); +el_val_t fs_exists(el_val_t path); +el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_escape_string(el_val_t sv); +el_val_t json_build_object(el_val_t kvs); +el_val_t json_build_array(el_val_t items); + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +el_val_t time_to_parts(el_val_t ts); +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz); +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit); +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit); +el_val_t now_ns(void); + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * Both types share the el_val_t (int64) slot. Instants are nanoseconds + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_has(el_val_t key); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code); +el_val_t getpid_now(void); + +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + +/* ── CGI identity ───────────────────────────────────────────────────────────── + * Called at the start of main() in CGI programs (those with a `cgi {}` block). + * Records the program's DHARMA identity before any other code executes. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */ +el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */ +el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */ + +/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */ +el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */ +el_val_t stdout_restore(void); /* restore process stdout to terminal */ + +el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json); +el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json); +el_val_t trace_span_start(el_val_t name); +el_val_t trace_span_end(el_val_t span_handle); +el_val_t emit_event(el_val_t name, el_val_t duration_ms); + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v); +el_val_t __thread_join(el_val_t tid_v); + +/* ── __ prefixed aliases (self-hosting compiler ABI) ───────────────────────── + * The El self-hosting compiler emits calls to __-prefixed names. These are + * forwarding wrappers around the existing el_runtime functions above. */ + +/* I/O */ +el_val_t __println(el_val_t s); +el_val_t __print(el_val_t s); +el_val_t __readline(void); + +/* String */ +el_val_t __int_to_str(el_val_t n); +el_val_t __str_to_int(el_val_t s); +el_val_t __float_to_str(el_val_t f); +el_val_t __str_to_float(el_val_t s); +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); +el_val_t __str_cmp(el_val_t a, el_val_t b); +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); +el_val_t __str_concat_raw(el_val_t a, el_val_t b); +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); +el_val_t __str_alloc(el_val_t n); +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); + +/* URL encoding */ +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* Environment */ +el_val_t __env_get(el_val_t key); + +/* Subprocess */ +el_val_t __exec(el_val_t cmd); +el_val_t __exec_bg(el_val_t cmd); + +/* Process */ +el_val_t __exit_program(el_val_t code); + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path); +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_read(el_val_t path); +el_val_t __fs_write(el_val_t path, el_val_t content); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); +el_val_t __fs_list_raw(el_val_t path); + +/* HTTP server */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); +el_val_t __http_serve(el_val_t port, el_val_t handler); +el_val_t __http_serve_v2(el_val_t port, el_val_t handler); + +/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */ +el_val_t __http_conn_fd(void); +el_val_t __http_sse_open(el_val_t conn_id); +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); +el_val_t __http_sse_close(el_val_t conn_id); + +/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms); +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path); + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index); +el_val_t __json_array_get_string(el_val_t json, el_val_t index); +el_val_t __json_array_len(el_val_t json); +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json, el_val_t key); +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value); +el_val_t __json_parse_map(el_val_t json_str); +el_val_t __json_stringify_val(el_val_t val); + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s); + +/* State K/V */ +el_val_t __state_del(el_val_t key); +el_val_t __state_get(el_val_t key); +el_val_t __state_keys(void); +el_val_t __state_set(el_val_t key, el_val_t val); + +/* UUID */ +el_val_t __uuid_v4(void); + +/* Args */ +el_val_t __args_json(void); + +#ifdef __cplusplus +} +#endif diff --git a/ui/examples/native-hello-ios/NativeHello/el_seed.c b/ui/examples/native-hello-ios/NativeHello/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + return http_response(status, headers_json, body); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello-ios/NativeHello/el_uikit.m b/ui/examples/native-hello-ios/NativeHello/el_uikit.m new file mode 100644 index 0000000..01a97a1 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/el_uikit.m @@ -0,0 +1,1118 @@ +/* + * el_uikit.m — UIKit backend for the el native widget system. + * + * This file implements the iOS UIKit widget layer that el_seed.c calls + * through to when EL_TARGET_IOS is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_uikit_* C-callable functions declared here + * → UIView/UIWindow/UIStackView/UIButton/etc. via ObjC + * + * Widget handles: every widget (window, view, control) is assigned an int64_t + * slot index into _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved as null handle = -1 convention). + * + * Threading: All UIKit calls MUST run on the main thread. el_uikit_sync_main + * bounces work to the main thread synchronously if called from a background thread. + * + * Callback mechanism: when a widget fires an event (button tap, text change), + * the bridge looks up the registered callback function name, then calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This resolves the El function symbol at runtime, matching the __thread_create + * pattern already established in el_seed.c. + * + * iOS lifecycle: UIApplicationMain never returns. The el program's "main" body + * (window creation, layout, show) must execute inside applicationDidFinishLaunchingWithOptions. + * The caller sets el_main_entry_fn before calling __native_run_loop so the + * delegate can invoke it at the right time. + * + * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): + * clang -std=gnu11 -ObjC -fno-objc-arc -DEL_TARGET_IOS -framework UIKit \ + * -c el_uikit.m -o el_uikit.o + * Then link el_uikit.o alongside el_seed.c. + * + * Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC + * forbids explicit retain/release on struct-embedded object pointers and cannot + * insert automatic retain/release through C struct boundaries. MRC is the + * correct choice for this kind of C/ObjC bridge. + * + * Link flags required: + * -framework UIKit + */ + +#ifdef EL_TARGET_IOS + +#import +#import +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_UIKIT_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + id obj; /* UIWindow* or UIView* subclass, retained */ + char* cb_click; /* El function name for click/submit events */ + char* cb_change; /* El function name for value-change events */ +} ElWidget; + +static ElWidget _el_widgets[EL_UIKIT_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Side table for ObjC delegate objects keyed by widget slot (MRC retained). */ +static id _el_delegates[EL_UIKIT_MAX_WIDGETS]; + +/* Allocate a slot and return its index. Returns -1 if full. */ +static int64_t el_widget_alloc(ElWidgetKind kind, id obj) { + for (int i = 1; i < EL_UIKIT_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].obj = [obj retain]; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_UIKIT_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + [w->obj release]; + w->obj = nil; + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── Dispatch helpers ─────────────────────────────────────────────────────── */ + +/* Run a block on the main thread, waiting for completion. Safe to call from + * any thread including the main thread. Uses dispatch_sync so callers can + * rely on the block having run before the function returns. */ +static void el_uikit_sync_main(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. The El function must have the + * signature: fn handler(handle: Int, data: String) -> Void + * which compiles to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_uikit_invoke_cb(const char* fn_name, int64_t handle, const char* data) { + if (!fn_name || !*fn_name) return; + void* sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── ObjC helper: ElEventProxy — button tap and text event target ─────────── */ + +/* + * ElEventProxy is a lightweight ObjC action target / delegate aggregator. + * One instance is allocated per widget that has events. Stored in _el_delegates[]. + */ +@interface ElEventProxy : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +/* UIButton tap */ +- (void)elButtonTapped:(id)sender; +/* UITextField UIControlEventEditingChanged */ +- (void)elTextChanged:(id)sender; +@end + +@implementation ElEventProxy + +- (void)elButtonTapped:(id)sender { + (void)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, ""); + } +} + +- (void)elTextChanged:(id)sender { + UITextField* tf = (UITextField*)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + const char* val = [tf.text UTF8String]; + if (w->cb_change) { + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); + } +} + +/* UITextFieldDelegate — return key fires on_submit (stored in cb_click). */ +- (BOOL)textFieldShouldReturn:(UITextField*)textField { + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + const char* val = [textField.text UTF8String]; + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); + } + [textField resignFirstResponder]; + return YES; +} + +@end + +/* ── UITextView change delegate ──────────────────────────────────────────── */ + +@interface ElTextViewDelegate : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +@end + +@implementation ElTextViewDelegate + +- (void)textViewDidChange:(UITextView*)textView { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w || !w->cb_change) return; + const char* val = [textView.text UTF8String]; + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); +} + +@end + +/* ── iOS application lifecycle ───────────────────────────────────────────── */ + +/* + * el_main_entry_fn — set by el's main() (via __native_run_loop) before calling + * UIApplicationMain. The app delegate invokes it in didFinishLaunching so that + * the el program's window/widget setup runs at the correct lifecycle point. + */ +void (*el_main_entry_fn)(void) = NULL; + +/* argc/argv forwarded from el_runtime_init_args (or main). */ +static int _el_argc = 0; +static char** _el_argv = NULL; + +/* + * el_uikit_set_args — call from el_runtime_init_args / main() before + * __native_run_loop to forward argc/argv to UIApplicationMain. + */ +void el_uikit_set_args(int argc, char** argv) { + _el_argc = argc; + _el_argv = argv; +} + +@interface ElAppDelegate : UIResponder +@property (nonatomic, strong) UIWindow *keyWindow; +@end + +@implementation ElAppDelegate + +- (BOOL)application:(UIApplication*)app + didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + (void)app; (void)launchOptions; + /* + * The el program called __native_init and set up el_main_entry_fn before + * UIApplicationMain was entered. We call it here so window creation and + * widget layout happen on the main thread at the right lifecycle moment. + */ + if (el_main_entry_fn) { + el_main_entry_fn(); + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication*)app { (void)app; } +- (void)applicationDidEnterBackground:(UIApplication*)app { (void)app; } +- (void)applicationWillEnterForeground:(UIApplication*)app { (void)app; } +- (void)applicationDidBecomeActive:(UIApplication*)app { (void)app; } + +@end + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_uikit_init — one-time initialisation. Idempotent. Call before any other + * el_uikit_* function. On iOS there is no UIApplication equivalent of + * [NSApplication sharedApplication] to call here — the real initialisation + * happens inside UIApplicationMain which is entered from el_uikit_run_loop. + * This function records that init was requested. + */ +static int _el_uikit_init_done = 0; + +void el_uikit_init(void) { + _el_uikit_init_done = 1; +} + +/* + * el_uikit_run_loop — enters UIApplicationMain. Never returns. + * Must be called from the process main thread as the final step of main(). + * Sets el_main_entry_fn to the provided function pointer before entering + * UIApplicationMain so the app delegate can call back into el code. + */ +void el_uikit_run_loop(void (*entry_fn)(void)) { + el_main_entry_fn = entry_fn; + UIApplicationMain(_el_argc, _el_argv, nil, @"ElAppDelegate"); +} + +/* + * el_uikit_window_create — create a UIWindow and configure a root + * UINavigationController + UIViewController. Returns window handle. + * + * On iOS there is exactly one full-screen window. width/height from the + * manifest are stored but the window always fills the screen. The title + * is applied to the root view controller's navigationItem. + */ +int64_t el_uikit_window_create(const char* title, int width, int height, + int min_width, int min_height) { + (void)width; (void)height; (void)min_width; (void)min_height; + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScreen* screen = [UIScreen mainScreen]; + UIWindow* win = [[UIWindow alloc] initWithFrame:[screen bounds]]; + win.backgroundColor = [UIColor systemBackgroundColor]; + + /* Root view controller with a vertical UIStackView as content. */ + UIViewController* rootVC = [[UIViewController alloc] init]; + NSString* titleStr = [NSString stringWithUTF8String:title ? title : ""]; + rootVC.title = titleStr; + + UIStackView* rootStack = [[UIStackView alloc] initWithFrame:CGRectZero]; + rootStack.axis = UILayoutConstraintAxisVertical; + rootStack.alignment = UIStackViewAlignmentFill; + rootStack.distribution = UIStackViewDistributionFill; + rootStack.spacing = 0.0; + rootStack.translatesAutoresizingMaskIntoConstraints = NO; + [rootVC.view addSubview:rootStack]; + + /* Pin root stack to safe area. */ + UILayoutGuide* safe = rootVC.view.safeAreaLayoutGuide; + [NSLayoutConstraint activateConstraints:@[ + [rootStack.topAnchor constraintEqualToAnchor:safe.topAnchor], + [rootStack.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor], + [rootStack.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor], + [rootStack.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor], + ]]; + + UINavigationController* navCtrl = + [[UINavigationController alloc] initWithRootViewController:rootVC]; + win.rootViewController = navCtrl; + + /* Store the window. The root stack is accessible via + * ((UINavigationController*)win.rootViewController) + * .topViewController.view.subviews[0] + * but we reach it by casting obj to UIWindow* and digging at add_child time. */ + handle = el_widget_alloc(EL_WIDGET_WINDOW, win); + [win release]; + [rootVC release]; + [rootStack release]; + [navCtrl release]; + }); + return handle; +} + +/* + * el_uikit_window_show — make the window key and visible. + */ +void el_uikit_window_show(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + [win makeKeyAndVisible]; + }); +} + +/* + * el_uikit_window_set_title — update nav bar title at runtime. + */ +void el_uikit_window_set_title(int64_t handle, const char* title) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + UINavigationController* nav = (UINavigationController*)win.rootViewController; + if ([nav isKindOfClass:[UINavigationController class]]) { + nav.topViewController.title = + [NSString stringWithUTF8String:title ? title : ""]; + } + }); +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t make_uistack(UILayoutConstraintAxis axis, CGFloat spacing) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIStackView* sv = [[UIStackView alloc] initWithFrame:CGRectZero]; + sv.axis = axis; + sv.alignment = UIStackViewAlignmentFill; + sv.distribution = UIStackViewDistributionFill; + sv.spacing = spacing; + sv.translatesAutoresizingMaskIntoConstraints = NO; + ElWidgetKind kind = (axis == UILayoutConstraintAxisVertical) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + handle = el_widget_alloc(kind, sv); + [sv release]; + }); + return handle; +} + +int64_t el_uikit_vstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisVertical, (CGFloat)spacing); +} + +int64_t el_uikit_hstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisHorizontal, (CGFloat)spacing); +} + +int64_t el_uikit_zstack_create(void) { + /* ZStack = plain UIView; children are added with addSubview and use + * Auto Layout or absolute frames set by the caller. */ + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIView* v = [[UIView alloc] initWithFrame:CGRectZero]; + v.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); + [v release]; + }); + return handle; +} + +int64_t el_uikit_scroll_create(void) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScrollView* sv = [[UIScrollView alloc] initWithFrame:CGRectZero]; + sv.translatesAutoresizingMaskIntoConstraints = NO; + sv.alwaysBounceVertical = YES; + sv.showsHorizontalScrollIndicator = NO; + handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); + [sv release]; + }); + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_uikit_label_create(const char* text) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UILabel* lbl = [[UILabel alloc] initWithFrame:CGRectZero]; + lbl.text = [NSString stringWithUTF8String:text ? text : ""]; + lbl.numberOfLines = 0; + lbl.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); + [lbl release]; + }); + return handle; +} + +int64_t el_uikit_button_create(const char* label) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIButton* btn = [UIButton buttonWithType:UIButtonTypeSystem]; + [btn setTitle:[NSString stringWithUTF8String:label ? label : ""] + forState:UIControlStateNormal]; + btn.translatesAutoresizingMaskIntoConstraints = NO; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + [btn addTarget:proxy + action:@selector(elButtonTapped:) + forControlEvents:UIControlEventTouchUpInside]; + + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; /* retain in side table */ + handle = h; + /* btn is autoreleased from buttonWithType: — no release needed */ + }); + return handle; +} + +int64_t el_uikit_text_field_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextField* tf = [[UITextField alloc] initWithFrame:CGRectZero]; + tf.translatesAutoresizingMaskIntoConstraints = NO; + if (placeholder && *placeholder) { + tf.placeholder = [NSString stringWithUTF8String:placeholder]; + } + tf.borderStyle = UITextBorderStyleRoundedRect; + tf.autocorrectionType = UITextAutocorrectionTypeNo; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + tf.delegate = proxy; + [tf addTarget:proxy + action:@selector(elTextChanged:) + forControlEvents:UIControlEventEditingChanged]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; + handle = h; + [tf release]; + }); + return handle; +} + +int64_t el_uikit_text_area_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextView* tv = [[UITextView alloc] initWithFrame:CGRectZero]; + tv.translatesAutoresizingMaskIntoConstraints = NO; + tv.font = [UIFont systemFontOfSize:14.0]; + tv.layer.borderColor = [[UIColor systemGray4Color] CGColor]; + tv.layer.borderWidth = 1.0; + tv.layer.cornerRadius = 6.0; + tv.clipsToBounds = YES; + /* UITextView has no native placeholder; set accessibility hint. */ + if (placeholder && *placeholder) { + tv.accessibilityHint = [NSString stringWithUTF8String:placeholder]; + } + + ElTextViewDelegate* del = [[ElTextViewDelegate alloc] init]; + tv.delegate = del; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, tv); + del.widgetHandle = h; + _el_delegates[h] = del; + handle = h; + [tv release]; + }); + return handle; +} + +int64_t el_uikit_image_create(const char* path_or_name) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIImage* img = nil; + if (path_or_name && *path_or_name) { + /* Try filesystem path first, then bundle asset name. */ + img = [UIImage imageWithContentsOfFile: + [NSString stringWithUTF8String:path_or_name]]; + if (!img) { + img = [UIImage imageNamed: + [NSString stringWithUTF8String:path_or_name]]; + } + } + UIImageView* iv = [[UIImageView alloc] initWithFrame:CGRectZero]; + iv.translatesAutoresizingMaskIntoConstraints = NO; + iv.contentMode = UIViewContentModeScaleAspectFit; + if (img) { + iv.image = img; + } + handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); + [iv release]; + }); + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_uikit_widget_set_text(int64_t handle, const char* text) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = [NSString stringWithUTF8String:text ? text : ""]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).text = s; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).text = s; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitle:s forState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).text = s; + break; + case EL_WIDGET_WINDOW: + el_uikit_window_set_title(handle, text); + break; + default: break; + } + }); +} + +const char* el_uikit_widget_get_text(int64_t handle) { + __block const char* result = ""; + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = nil; + switch (w->kind) { + case EL_WIDGET_LABEL: + s = ((UILabel*)w->obj).text; + break; + case EL_WIDGET_TEXTFIELD: + s = ((UITextField*)w->obj).text; + break; + case EL_WIDGET_BUTTON: + s = [(UIButton*)w->obj titleForState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + s = ((UITextView*)w->obj).text; + break; + default: break; + } + if (s) result = strdup([s UTF8String]); + }); + return result; +} + +void el_uikit_widget_set_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIColor* c = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).textColor = c; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitleColor:c forState:UIControlStateNormal]; + ((UIButton*)w->obj).tintColor = c; + break; + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: + ((UIView*)w->obj).backgroundColor = c; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind == EL_WIDGET_WINDOW) { + UIWindow* win = (UIWindow*)w->obj; + win.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + return; + } + UIView* v = (UIView*)w->obj; + v.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + }); +} + +void el_uikit_widget_set_font(int64_t handle, const char* family, int size, int bold) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIFont* font; + if (bold) { + font = [UIFont boldSystemFontOfSize:(CGFloat)size]; + } else if (family && *family && strcmp(family, "system") != 0) { + font = [UIFont fontWithName:[NSString stringWithUTF8String:family] + size:(CGFloat)size]; + if (!font) font = [UIFont systemFontOfSize:(CGFloat)size]; + } else { + font = [UIFont systemFontOfSize:(CGFloat)size]; + } + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).font = font; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).font = font; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).font = font; + break; + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).titleLabel.font = font; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIEdgeInsets insets = UIEdgeInsetsMake( + (CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right); + if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { + UIStackView* sv = (UIStackView*)w->obj; + sv.layoutMargins = insets; + sv.layoutMarginsRelativeArrangement = YES; + } else if (w->kind == EL_WIDGET_TEXTAREA) { + UITextView* tv = (UITextView*)w->obj; + tv.textContainerInset = insets; + } else { + UIView* v = (UIView*)w->obj; + v.layoutMargins = insets; + } + }); +} + +void el_uikit_widget_set_width(int64_t handle, int width) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.widthAnchor + constraintEqualToConstant:(CGFloat)width]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_height(int64_t handle, int height) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.heightAnchor + constraintEqualToConstant:(CGFloat)height]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_flex(int64_t handle, int flex) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + /* flex > 0 → low hugging priority so the view expands to fill space. + * flex == 0 → high hugging priority so the view stays at intrinsic size. */ + UILayoutPriority p = (flex > 0) + ? UILayoutPriorityDefaultLow + : UILayoutPriorityDefaultHigh; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisHorizontal]; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisVertical]; + }); +} + +void el_uikit_widget_set_corner_radius(int64_t handle, int radius) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + v.layer.cornerRadius = (CGFloat)radius; + v.clipsToBounds = YES; + }); +} + +void el_uikit_widget_set_disabled(int64_t handle, int disabled) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + switch (w->kind) { + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).editable = !disabled; + break; + default: + ((UIView*)w->obj).userInteractionEnabled = !disabled; + break; + } + }); +} + +void el_uikit_widget_set_hidden(int64_t handle, int hidden) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + ((UIView*)w->obj).hidden = (BOOL)hidden; + }); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_uikit_widget_add_child — attach child widget to parent. + * + * - Window: child is added to the root UIStackView inside the root VC. + * - VStack/HStack: child is added as an arranged subview. + * - ZStack: child is added as a plain subview (caller manages layout). + * - ScrollView: child is added as a subview and pinned to content layout guide. + * - Generic view: child is added as a plain subview. + */ +void el_uikit_widget_add_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* can't add window as subview */ + + UIView* childView = (UIView*)cw->obj; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Dig to the root stack view: window → navCtrl → topVC → view → subviews[0] */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + UIView* rootVCView = nav.topViewController.view; + UIStackView* stack = nil; + for (UIView* sub in rootVCView.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + stack = (UIStackView*)sub; + break; + } + } + if (stack) { + [stack addArrangedSubview:childView]; + } else { + [rootVCView addSubview:childView]; + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: { + UIStackView* sv = (UIStackView*)pw->obj; + [sv addArrangedSubview:childView]; + break; + } + case EL_WIDGET_SCROLL: { + UIScrollView* sv = (UIScrollView*)pw->obj; + [sv addSubview:childView]; + /* Pin child to content layout guide so scroll view knows content size. */ + UILayoutGuide* cg = sv.contentLayoutGuide; + childView.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [childView.topAnchor constraintEqualToAnchor:cg.topAnchor], + [childView.leadingAnchor constraintEqualToAnchor:cg.leadingAnchor], + [childView.trailingAnchor constraintEqualToAnchor:cg.trailingAnchor], + [childView.bottomAnchor constraintEqualToAnchor:cg.bottomAnchor], + /* Width matches scroll view's frame (vertical scroll only). */ + [childView.widthAnchor constraintEqualToAnchor:sv.frameLayoutGuide.widthAnchor], + ]]; + break; + } + case EL_WIDGET_ZSTACK: + default: { + UIView* pv = (UIView*)pw->obj; + [pv addSubview:childView]; + break; + } + } + }); +} + +/* + * el_uikit_widget_remove_child — detach a child widget from its parent. + */ +void el_uikit_widget_remove_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!cw || cw->kind == EL_WIDGET_WINDOW) return; + UIView* childView = (UIView*)cw->obj; + /* If parent is a stack view, remove from arranged subviews too. */ + if (pw && (pw->kind == EL_WIDGET_VSTACK || pw->kind == EL_WIDGET_HSTACK)) { + UIStackView* sv = (UIStackView*)pw->obj; + [sv removeArrangedSubview:childView]; + } else if (pw && pw->kind == EL_WIDGET_WINDOW) { + /* Find and remove from root stack. */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + for (UIView* sub in nav.topViewController.view.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + [(UIStackView*)sub removeArrangedSubview:childView]; + break; + } + } + } + [childView removeFromSuperview]; + }); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_uikit_widget_on_click(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_uikit_widget_on_change(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_uikit_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit = return key → stored in cb_click slot. */ + el_uikit_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_uikit_widget_destroy(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind != EL_WIDGET_WINDOW) { + UIView* v = (UIView*)w->obj; + /* If it's in a stack view, also remove from arranged subviews. */ + if ([v.superview isKindOfClass:[UIStackView class]]) { + [(UIStackView*)v.superview removeArrangedSubview:v]; + } + [v removeFromSuperview]; + } + [_el_delegates[handle] release]; + _el_delegates[handle] = nil; + el_widget_free(handle); + }); +} + +/* ── Manifest reader ──────────────────────────────────────────────────────── */ + +/* + * el_uikit_manifest_read — read an app manifest file. Tries the given path + * first; if not found, looks in the main bundle. Returns a C string with the + * file contents (heap-allocated; caller must not free — matches AppKit bridge + * convention of returning static/permanent lifetime strings from manifest). + * Returns NULL on failure. + */ +const char* el_uikit_manifest_read(const char* path) { + if (!path || !*path) return NULL; + + NSString* pathStr = [NSString stringWithUTF8String:path]; + NSString* content = [NSString stringWithContentsOfFile:pathStr + encoding:NSUTF8StringEncoding + error:nil]; + if (!content) { + /* Try bundle resource lookup — strip to last component without extension. */ + NSString* baseName = [[pathStr lastPathComponent] + stringByDeletingPathExtension]; + NSString* ext = [pathStr pathExtension]; + NSString* bundlePath = [[NSBundle mainBundle] + pathForResource:baseName ofType:(ext.length ? ext : nil)]; + if (bundlePath) { + content = [NSString stringWithContentsOfFile:bundlePath + encoding:NSUTF8StringEncoding + error:nil]; + } + } + if (!content) return NULL; + return strdup([content UTF8String]); +} + +/* ── __widget_* C API (called from el_seed.c) ─────────────────────────────── */ +/* + * These are the symbols that el compiled programs actually call. They forward + * directly to the el_uikit_* implementations above. el_val_t is int64_t (the + * same type used by the AppKit bridge). + */ + +void __native_init(void) { + el_uikit_init(); +} + +void __native_run_loop(void) { + /* el_main_entry_fn must be set by the caller before this. On iOS the + * entry function pointer pattern is inverted vs macOS: the el program + * packages up its "build UI" work into a lambda / function and registers + * it via el_main_entry_fn, then calls __native_run_loop. + * el_uikit_run_loop takes that fn pointer and passes it to UIApplicationMain. */ + el_uikit_run_loop(el_main_entry_fn); +} + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_uikit_window_create( + (const char*)(uintptr_t)title, + (int)width, (int)height, + (int)min_width, (int)min_height); +} + +void __window_show(el_val_t handle) { + el_uikit_window_show((int64_t)handle); +} + +void __window_set_title(el_val_t handle, el_val_t title) { + el_uikit_window_set_title((int64_t)handle, + (const char*)(uintptr_t)title); +} + +el_val_t __vstack_create(el_val_t spacing) { + return (el_val_t)el_uikit_vstack_create((int)spacing); +} + +el_val_t __hstack_create(el_val_t spacing) { + return (el_val_t)el_uikit_hstack_create((int)spacing); +} + +el_val_t __zstack_create(void) { + return (el_val_t)el_uikit_zstack_create(); +} + +el_val_t __scroll_create(void) { + return (el_val_t)el_uikit_scroll_create(); +} + +el_val_t __label_create(el_val_t text) { + return (el_val_t)el_uikit_label_create((const char*)(uintptr_t)text); +} + +el_val_t __button_create(el_val_t label) { + return (el_val_t)el_uikit_button_create((const char*)(uintptr_t)label); +} + +el_val_t __text_field_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_field_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __text_area_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_area_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __image_create(el_val_t path_or_name) { + return (el_val_t)el_uikit_image_create( + (const char*)(uintptr_t)path_or_name); +} + +void __widget_set_text(el_val_t handle, el_val_t text) { + el_uikit_widget_set_text((int64_t)handle, (const char*)(uintptr_t)text); +} + +el_val_t __widget_get_text(el_val_t handle) { + return (el_val_t)(uintptr_t)el_uikit_widget_get_text((int64_t)handle); +} + +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_uikit_widget_set_color((int64_t)handle, + (float)r, (float)g, (float)b, (float)a); +} + +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a) { + el_uikit_widget_set_bg_color((int64_t)handle, + (float)r, (float)g, (float)b, (float)a); +} + +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold) { + el_uikit_widget_set_font((int64_t)handle, + (const char*)(uintptr_t)family, (int)size, (int)bold); +} + +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_uikit_widget_set_padding((int64_t)handle, + (int)top, (int)right, (int)bottom, (int)left); +} + +void __widget_set_width(el_val_t handle, el_val_t width) { + el_uikit_widget_set_width((int64_t)handle, (int)width); +} + +void __widget_set_height(el_val_t handle, el_val_t height) { + el_uikit_widget_set_height((int64_t)handle, (int)height); +} + +void __widget_set_flex(el_val_t handle, el_val_t flex) { + el_uikit_widget_set_flex((int64_t)handle, (int)flex); +} + +void __widget_set_corner_radius(el_val_t handle, el_val_t radius) { + el_uikit_widget_set_corner_radius((int64_t)handle, (int)radius); +} + +void __widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_uikit_widget_set_disabled((int64_t)handle, (int)disabled); +} + +void __widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_uikit_widget_set_hidden((int64_t)handle, (int)hidden); +} + +void __widget_add_child(el_val_t parent, el_val_t child) { + el_uikit_widget_add_child((int64_t)parent, (int64_t)child); +} + +void __widget_remove_child(el_val_t parent, el_val_t child) { + el_uikit_widget_remove_child((int64_t)parent, (int64_t)child); +} + +void __widget_destroy(el_val_t handle) { + el_uikit_widget_destroy((int64_t)handle); +} + +void __widget_on_click(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_click((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +void __widget_on_change(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_change((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +void __widget_on_submit(el_val_t handle, el_val_t fn_name) { + el_uikit_widget_on_submit((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +el_val_t __manifest_read(el_val_t path) { + return (el_val_t)(uintptr_t)el_uikit_manifest_read( + (const char*)(uintptr_t)path); +} + +#endif /* EL_TARGET_IOS */ diff --git a/ui/examples/native-hello-ios/NativeHello/main.m b/ui/examples/native-hello-ios/NativeHello/main.m new file mode 100644 index 0000000..4ca6b86 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/main.m @@ -0,0 +1,48 @@ +/* + * main.m — entry point for native-hello-ios. + * + * iOS UIKit lifecycle: + * UIApplicationMain never returns. The compiled el program's boot body + * (native_init → window_from_manifest → app_build → window_show) must + * run inside applicationDidFinishLaunchingWithOptions on the main thread. + * + * How it works: + * 1. el_uikit_set_args() forwards argc/argv to UIApplicationMain. + * 2. el_main_entry_fn is set to el_app_boot (defined below) before + * UIApplicationMain is entered. + * 3. ElAppDelegate.didFinishLaunchingWithOptions calls el_main_entry_fn(), + * which runs the el program's widget setup at the correct lifecycle point. + * 4. __native_run_loop() inside the compiled el main is a no-op on iOS — + * UIApplicationMain already owns the run loop. + * + * The generated native_hello.c exports el_app_main() (renamed from main by + * the gen step in build.sh). We call it from el_app_boot so it runs after + * UIApplicationMain sets up the application object. + * + * Compile with -fno-objc-arc (required by el_uikit.m). + */ + +#import + +/* From el_uikit.m */ +void el_uikit_set_args(int argc, char **argv); +extern void (*el_main_entry_fn)(void); + +/* Forward declaration of the renamed compiled el main. */ +int el_app_main(int argc, char **argv); + +/* Boot function registered as el_main_entry_fn. Called by ElAppDelegate + * inside didFinishLaunchingWithOptions on the main thread. */ +static void el_app_boot(void) { + char *argv[] = {"el-app", NULL}; + el_app_main(1, argv); +} + +int main(int argc, char *argv[]) { + el_uikit_set_args(argc, argv); + /* Register the boot function so ElAppDelegate can call it. */ + el_main_entry_fn = el_app_boot; + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, @"ElAppDelegate"); + } +} diff --git a/ui/examples/native-hello-ios/NativeHello/native_hello.c b/ui/examples/native-hello-ios/NativeHello/native_hello.c new file mode 100644 index 0000000..e18f274 --- /dev/null +++ b/ui/examples/native-hello-ios/NativeHello/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int el_app_main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello-ios/build.sh b/ui/examples/native-hello-ios/build.sh new file mode 100755 index 0000000..3c1d92d --- /dev/null +++ b/ui/examples/native-hello-ios/build.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# build.sh — Build and install native-hello-ios to the iOS Simulator. +# +# Usage: +# ./build.sh # gen C sources + xcodebuild for simulator +# ./build.sh gen # regenerate C sources only +# ./build.sh build # xcodebuild only (skip elc step) +# ./build.sh install # gen + build + boot simulator + install + launch +# ./build.sh clean # remove build/ directory +# +# Requirements: +# - Xcode with iOS Simulator SDK installed +# - elc at ../../../lang/dist/platform/elc (or in PATH) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el" +EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el" +SOURCES_DIR="${SCRIPT_DIR}/NativeHello" +BUILD_DIR="${SCRIPT_DIR}/build" + +# Locate elc. +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +gen() { + echo "==> Generating native_hello.c..." + EL_MANIFEST="${EL_MANIFEST}" \ + "${ELC}" "${EL_APP_ENTRY}" \ + > "${SOURCES_DIR}/native_hello.c" + # Rename the compiled el main() to el_app_main so it doesn't conflict with + # main.m's main(). main.m's main() is the real entry point for UIApplicationMain. + # Use a simple sed substitution on the exact signature elc emits. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_app_main(int _argc, char** _argv) {/' \ + "${SOURCES_DIR}/native_hello.c" + + echo "==> Generating el_native_vessel.c..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${SOURCES_DIR}/el_native_vessel.c" + # Rename vessel main() similarly. + sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \ + "${SOURCES_DIR}/el_native_vessel.c" + + echo "==> Copying runtime files..." + cp "${EL_RUNTIME}/el_uikit.m" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_seed.c" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_runtime.c" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_runtime.h" "${SOURCES_DIR}/" + cp "${EL_RUNTIME}/el_native_target.h" "${SOURCES_DIR}/" + + echo "==> Source generation complete." +} + +xcode_build() { + echo "==> Building with xcodebuild (iphonesimulator)..." + xcodebuild \ + -project "${SCRIPT_DIR}/NativeHello.xcodeproj" \ + -scheme NativeHello \ + -sdk iphonesimulator \ + -configuration Debug \ + -derivedDataPath "${BUILD_DIR}" \ + CLANG_ENABLE_OBJC_ARC=NO \ + OTHER_CFLAGS="-DEL_TARGET_IOS" \ + OTHER_LDFLAGS="-ldl" \ + build + + APP_PATH=$(find "${BUILD_DIR}" -name "NativeHello.app" -maxdepth 6 | head -1) + if [ -n "${APP_PATH}" ]; then + echo "==> Build complete." + echo " App bundle: ${APP_PATH}" + else + echo "==> Build complete (app bundle in ${BUILD_DIR})." + fi +} + +install_and_launch() { + gen + xcode_build + + APP_PATH=$(find "${BUILD_DIR}" -name "NativeHello.app" -maxdepth 6 | head -1) + if [ -z "${APP_PATH}" ]; then + echo "Error: could not find NativeHello.app in ${BUILD_DIR}" + exit 1 + fi + + # Boot the default simulator if not already booted. + SIM_UDID=$(xcrun simctl list devices available --json \ + | python3 -c " +import json,sys +d=json.load(sys.stdin)['devices'] +for runtime,devs in d.items(): + for dev in devs: + if 'iPhone' in dev['name'] and dev['isAvailable']: + print(dev['udid']) + exit() +") + if [ -z "${SIM_UDID}" ]; then + echo "Error: no available iPhone simulator found." + exit 1 + fi + echo "==> Using simulator: ${SIM_UDID}" + xcrun simctl boot "${SIM_UDID}" 2>/dev/null || true + open -a Simulator + + echo "==> Installing app..." + xcrun simctl install "${SIM_UDID}" "${APP_PATH}" + + echo "==> Launching app..." + xcrun simctl launch "${SIM_UDID}" "ai.neurontechnologies.el.nativehello" +} + +clean() { + echo "==> Cleaning..." + rm -rf "${BUILD_DIR}" + echo "==> Clean complete." +} + +case "${1:-all}" in + gen) gen ;; + build) xcode_build ;; + install) install_and_launch ;; + clean) clean ;; + all|*) gen && xcode_build ;; +esac diff --git a/ui/examples/native-hello/-o b/ui/examples/native-hello/-o new file mode 100644 index 0000000..e69de29 diff --git a/ui/examples/native-hello/Dockerfile.linux-gtk4 b/ui/examples/native-hello/Dockerfile.linux-gtk4 new file mode 100644 index 0000000..2079162 --- /dev/null +++ b/ui/examples/native-hello/Dockerfile.linux-gtk4 @@ -0,0 +1,88 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y \ + build-essential \ + libgtk-4-dev \ + pkg-config \ + libcurl4-openssl-dev \ + binutils \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# All sources arrive in /build — runtime/ subdirectory contains the el runtime, +# top-level contains elc-generated app/vessel C files. +COPY runtime/ runtime/ +COPY el_native_vessel.c el_native_vessel.h native_hello.c ./ + +# Compile el_gtk4.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_gtk4.c -o /build/el_gtk4.o && \ + echo "el_gtk4.c compiled OK" + +# Compile el_seed.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_seed.c -o /build/el_seed.o && \ + echo "el_seed.c compiled OK" + +# Compile el_runtime.c +RUN gcc -std=c11 -I/build/runtime \ + -c /build/runtime/el_runtime.c -o /build/el_runtime.o && \ + echo "el_runtime.c compiled OK" + +# Patch el_runtime.o: hide __-prefixed symbols that el_seed.o also defines. +# This is the Linux objcopy equivalent of macOS nmedit -s. +# el_seed.o is canonical for all __ symbols; el_runtime.o provides high-level +# builtins (str_len, json_get_string, etc.) that el_seed.o does NOT export. +RUN nm /build/el_seed.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.seed_T.txt && \ + nm /build/el_runtime.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.rt_T.txt && \ + comm -12 /build/.seed_T.txt /build/.rt_T.txt > /build/.dups.txt && \ + if [ -s /build/.dups.txt ]; then \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.dups.txt); \ + eval "objcopy $ARGS /build/el_runtime.o"; \ + echo "Patched $(wc -l < /build/.dups.txt) duplicate symbols in el_runtime.o"; \ + else \ + echo "No duplicate symbols found"; \ + fi + +# Compile vessel +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -c /build/el_native_vessel.c -o /build/el_native_vessel.o && \ + echo "vessel compiled OK" + +# Compile app (before patching vessel, so we know which symbols to localise) +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -include /build/el_native_vessel.h \ + -c /build/native_hello.c -o /build/native_hello.o && \ + echo "app compiled OK" + +# Patch el_native_vessel.o: +# 1. Localise main() so it doesn't conflict with the app's main. +# 2. Localise any T/B symbols the app also defines (elc emits shared globals +# like TOKEN_PRIMARY in every translation unit; GNU ld rejects duplicates +# unlike macOS ld which picks the first one silently). +RUN nm /build/native_hello.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.app_syms.txt && \ + nm /build/el_native_vessel.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.vessel_syms.txt && \ + comm -12 /build/.app_syms.txt /build/.vessel_syms.txt > /build/.vessel_dups.txt && \ + echo "main" >> /build/.vessel_dups.txt && \ + sort -u /build/.vessel_dups.txt -o /build/.vessel_dups.txt && \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.vessel_dups.txt) && \ + eval "objcopy $ARGS /build/el_native_vessel.o" && \ + echo "Patched $(wc -l < /build/.vessel_dups.txt) symbols in el_native_vessel.o" + +# Link — add -lm for math functions (exp, log, etc.) used by el_runtime.c +RUN gcc \ + /build/native_hello.o \ + /build/el_native_vessel.o \ + /build/el_gtk4.o \ + /build/el_runtime.o \ + /build/el_seed.o \ + $(pkg-config --libs gtk4) -ldl -lpthread -lcurl -lm \ + -o /build/native-hello-linux && \ + echo "==> Linked: native-hello-linux" && \ + file /build/native-hello-linux diff --git a/ui/examples/native-hello/Dockerfile.pi b/ui/examples/native-hello/Dockerfile.pi new file mode 100644 index 0000000..a4a30a2 --- /dev/null +++ b/ui/examples/native-hello/Dockerfile.pi @@ -0,0 +1,99 @@ +FROM --platform=linux/arm64 ubuntu:22.04 + +# GTK4 + SDL2 for Raspberry Pi (ARM64 / aarch64) +# Targets Pi 3 / 4 / 5 running 64-bit Raspberry Pi OS or Ubuntu 22.04. +# +# GTK4 requires a desktop environment / Wayland compositor at runtime. +# SDL2 works on bare framebuffer or KMS/DRM without a full desktop. +# +# Usage: +# docker build --platform linux/arm64 \ +# -f Dockerfile.pi --tag el-native-pi-arm64 ./build-docker/ +# docker create --name pi-extract el-native-pi-arm64 +# docker cp pi-extract:/build/native-hello-linux-pi ./build/native-hello-pi-arm64 +# docker rm pi-extract +# +# The GTK4 build is the default. SDL2 is available as an alternative +# for headless / bare-metal Pi deployments. + +RUN apt-get update && apt-get install -y \ + build-essential \ + libgtk-4-dev \ + libsdl2-dev \ + libsdl2-ttf-dev \ + libsdl2-image-dev \ + pkg-config \ + libcurl4-openssl-dev \ + binutils \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# All sources arrive in /build +COPY runtime/ runtime/ +COPY el_native_vessel.c el_native_vessel.h native_hello.c ./ + +# ── GTK4 build ──────────────────────────────────────────────────────────────── + +# Compile el_gtk4.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_gtk4.c -o /build/el_gtk4.o && \ + echo "el_gtk4.c compiled OK" + +# Compile el_seed.c +RUN gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + -std=c11 -I/build/runtime \ + -c /build/runtime/el_seed.c -o /build/el_seed.o && \ + echo "el_seed.c compiled OK" + +# Compile el_runtime.c +RUN gcc -std=c11 -I/build/runtime \ + -c /build/runtime/el_runtime.c -o /build/el_runtime.o && \ + echo "el_runtime.c compiled OK" + +# Patch el_runtime.o: localise __-prefixed duplicate symbols (el_seed.o is canonical) +RUN nm /build/el_seed.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.seed_T.txt && \ + nm /build/el_runtime.o | awk '/[0-9a-f]+ T /{print $3}' | sort > /build/.rt_T.txt && \ + comm -12 /build/.seed_T.txt /build/.rt_T.txt > /build/.dups.txt && \ + if [ -s /build/.dups.txt ]; then \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.dups.txt); \ + eval "objcopy $ARGS /build/el_runtime.o"; \ + echo "Patched $(wc -l < /build/.dups.txt) duplicate symbols in el_runtime.o"; \ + fi + +# Compile vessel +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -c /build/el_native_vessel.c -o /build/el_native_vessel.o && \ + echo "vessel compiled OK" + +# Compile app +RUN gcc -DEL_TARGET_LINUX -std=c11 -I/build/runtime \ + -include /build/runtime/el_native_target.h \ + -include /build/el_native_vessel.h \ + -c /build/native_hello.c -o /build/native_hello.o && \ + echo "app compiled OK" + +# Patch el_native_vessel.o: localise shared globals (main + TOKEN_* etc.) +RUN nm /build/native_hello.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.app_syms.txt && \ + nm /build/el_native_vessel.o | awk '/[0-9a-f]+ [TBb] /{print $3}' | sort > /build/.vessel_syms.txt && \ + comm -12 /build/.app_syms.txt /build/.vessel_syms.txt > /build/.vessel_dups.txt && \ + echo "main" >> /build/.vessel_dups.txt && \ + sort -u /build/.vessel_dups.txt -o /build/.vessel_dups.txt && \ + ARGS=$(awk '{printf "--localize-symbol=%s ", $1}' /build/.vessel_dups.txt) && \ + eval "objcopy $ARGS /build/el_native_vessel.o" && \ + echo "Patched $(wc -l < /build/.vessel_dups.txt) symbols in el_native_vessel.o" + +# Link (GTK4) +RUN gcc \ + /build/native_hello.o \ + /build/el_native_vessel.o \ + /build/el_gtk4.o \ + /build/el_runtime.o \ + /build/el_seed.o \ + $(pkg-config --libs gtk4) -ldl -lpthread -lcurl -lm \ + -o /build/native-hello-linux-pi && \ + echo "==> Linked: native-hello-linux-pi (GTK4)" && \ + file /build/native-hello-linux-pi diff --git a/ui/examples/native-hello/build-docker/el_native_vessel.c b/ui/examples/native-hello/build-docker/el_native_vessel.c new file mode 100644 index 0000000..28bf38c --- /dev/null +++ b/ui/examples/native-hello/build-docker/el_native_vessel.c @@ -0,0 +1,459 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + +el_val_t native_init(void) { + __native_init(); + return 0; +} + +el_val_t native_run_loop(void) { + __native_run_loop(); + return 0; +} + +el_val_t manifest_read(el_val_t path) { + return __manifest_read(path); + return 0; +} + +el_val_t manifest_title(el_val_t m) { + return json_get_string(m, EL_STR("title")); + return 0; +} + +el_val_t manifest_width(el_val_t m) { + return json_get_int(m, EL_STR("width")); + return 0; +} + +el_val_t manifest_height(el_val_t m) { + return json_get_int(m, EL_STR("height")); + return 0; +} + +el_val_t manifest_min_width(el_val_t m) { + return json_get_int(m, EL_STR("min_width")); + return 0; +} + +el_val_t manifest_min_height(el_val_t m) { + return json_get_int(m, EL_STR("min_height")); + return 0; +} + +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height) { + return __window_create(title, width, height, min_width, min_height); + return 0; +} + +el_val_t window_from_manifest(el_val_t manifest_path) { + el_val_t m = manifest_read(manifest_path); + el_val_t title = manifest_title(m); + el_val_t w = manifest_width(m); + el_val_t h = manifest_height(m); + el_val_t mw = manifest_min_width(m); + el_val_t mh = manifest_min_height(m); + el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; }); + el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; }); + el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; }); + el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; }); + el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; }); + return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh); + return 0; +} + +el_val_t window_show(el_val_t handle) { + __window_show(handle); + return 0; +} + +el_val_t window_set_title(el_val_t handle, el_val_t title) { + __window_set_title(handle, title); + return 0; +} + +el_val_t vstack(el_val_t spacing) { + return __vstack_create(spacing); + return 0; +} + +el_val_t vstack_tight(void) { + return __vstack_create(0); + return 0; +} + +el_val_t hstack(el_val_t spacing) { + return __hstack_create(spacing); + return 0; +} + +el_val_t zstack(void) { + return __zstack_create(); + return 0; +} + +el_val_t scroll(void) { + return __scroll_create(); + return 0; +} + +el_val_t label(el_val_t text) { + return __label_create(text); + return 0; +} + +el_val_t button(el_val_t label) { + return __button_create(label); + return 0; +} + +el_val_t text_field(el_val_t placeholder) { + return __text_field_create(placeholder); + return 0; +} + +el_val_t text_area(el_val_t placeholder) { + return __text_area_create(placeholder); + return 0; +} + +el_val_t image(el_val_t path_or_name) { + return __image_create(path_or_name); + return 0; +} + +el_val_t widget_set_text(el_val_t handle, el_val_t text) { + __widget_set_text(handle, text); + return 0; +} + +el_val_t widget_get_text(el_val_t handle) { + return __widget_get_text(handle); + return 0; +} + +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + __widget_set_bg_color(handle, r, g, b, a); + return 0; +} + +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) { + el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; }); + __widget_set_font(handle, family, size, bold_int); + return 0; +} + +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) { + __widget_set_padding(handle, top, right, bottom, left); + return 0; +} + +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) { + __widget_set_padding(handle, p, p, p, p); + return 0; +} + +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) { + __widget_set_padding(handle, py, px, py, px); + return 0; +} + +el_val_t widget_set_width(el_val_t handle, el_val_t width) { + __widget_set_width(handle, width); + return 0; +} + +el_val_t widget_set_height(el_val_t handle, el_val_t height) { + __widget_set_height(handle, height); + return 0; +} + +el_val_t widget_set_flex(el_val_t handle, el_val_t flex) { + __widget_set_flex(handle, flex); + return 0; +} + +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) { + __widget_set_corner_radius(handle, radius); + return 0; +} + +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; }); + __widget_set_disabled(handle, d); + return 0; +} + +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; }); + __widget_set_hidden(handle, h); + return 0; +} + +el_val_t widget_add_child(el_val_t parent, el_val_t child) { + __widget_add_child(parent, child); + return 0; +} + +el_val_t widget_remove_child(el_val_t parent, el_val_t child) { + __widget_remove_child(parent, child); + return 0; +} + +el_val_t widget_destroy(el_val_t handle) { + __widget_destroy(handle); + return 0; +} + +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) { + __widget_on_click(handle, fn_name); + return 0; +} + +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) { + __widget_on_change(handle, fn_name); + return 0; +} + +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) { + __widget_on_submit(handle, fn_name); + return 0; +} + +el_val_t hex_channel(el_val_t s, el_val_t offset) { + el_val_t hi = str_slice(s, offset, (offset + 1)); + el_val_t lo = str_slice(s, (offset + 1), (offset + 2)); + el_val_t h = hex_nibble(hi); + el_val_t l = hex_nibble(lo); + return ((h * 16) + l); + return 0; +} + +el_val_t hex_nibble(el_val_t c) { + if (str_eq(c, EL_STR("0"))) { + return 0; + } + if (str_eq(c, EL_STR("1"))) { + return 1; + } + if (str_eq(c, EL_STR("2"))) { + return 2; + } + if (str_eq(c, EL_STR("3"))) { + return 3; + } + if (str_eq(c, EL_STR("4"))) { + return 4; + } + if (str_eq(c, EL_STR("5"))) { + return 5; + } + if (str_eq(c, EL_STR("6"))) { + return 6; + } + if (str_eq(c, EL_STR("7"))) { + return 7; + } + if (str_eq(c, EL_STR("8"))) { + return 8; + } + if (str_eq(c, EL_STR("9"))) { + return 9; + } + if (str_eq(c, EL_STR("a"))) { + return 10; + } + if (str_eq(c, EL_STR("b"))) { + return 11; + } + if (str_eq(c, EL_STR("c"))) { + return 12; + } + if (str_eq(c, EL_STR("d"))) { + return 13; + } + if (str_eq(c, EL_STR("e"))) { + return 14; + } + if (str_eq(c, EL_STR("f"))) { + return 15; + } + if (str_eq(c, EL_STR("A"))) { + return 10; + } + if (str_eq(c, EL_STR("B"))) { + return 11; + } + if (str_eq(c, EL_STR("C"))) { + return 12; + } + if (str_eq(c, EL_STR("D"))) { + return 13; + } + if (str_eq(c, EL_STR("E"))) { + return 14; + } + if (str_eq(c, EL_STR("F"))) { + return 15; + } + return 0; + return 0; +} + +el_val_t color_hex_r(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; }); + el_val_t v = hex_channel(s, 0); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_g(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; }); + el_val_t v = hex_channel(s, 2); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_b(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; }); + el_val_t v = hex_channel(s, 4); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t color_hex_a(el_val_t hex) { + el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; }); + if (str_len(s) < 8) { + return el_from_float(1.0); + } + el_val_t v = hex_channel(s, 6); + return float_div(int_to_float(v), el_from_float(255.0)); + return 0; +} + +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) { + widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) { + widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex)); + return 0; +} + +el_val_t style_surface(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_SURFACE); + widget_set_corner_radius(handle, 8); + return 0; +} + +el_val_t style_button_primary(el_val_t handle) { + widget_set_bg_color_hex(handle, TOKEN_PRIMARY); + widget_set_color_hex(handle, TOKEN_ON_PRIMARY); + widget_set_corner_radius(handle, 8); + widget_set_padding_xy(handle, 16, 8); + return 0; +} + +el_val_t style_label_body(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 14, 0); + return 0; +} + +el_val_t style_label_heading(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_ON_BG); + widget_set_font(handle, EL_STR("system"), 20, 1); + return 0; +} + +el_val_t style_label_muted(el_val_t handle) { + widget_set_color_hex(handle, TOKEN_OUTLINE); + widget_set_font(handle, EL_STR("system"), 12, 0); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + TOKEN_PRIMARY = EL_STR("#60a5fa"); + TOKEN_ON_PRIMARY = EL_STR("#0f172a"); + TOKEN_BACKGROUND = EL_STR("#0f172a"); + TOKEN_ON_BG = EL_STR("#f8fafc"); + TOKEN_SURFACE = EL_STR("#1e293b"); + TOKEN_ON_SURFACE = EL_STR("#f8fafc"); + TOKEN_OUTLINE = EL_STR("#475569"); + TOKEN_ERROR = EL_STR("#f87171"); + return 0; +} + diff --git a/ui/examples/native-hello/build-docker/el_native_vessel.h b/ui/examples/native-hello/build-docker/el_native_vessel.h new file mode 100644 index 0000000..b62c288 --- /dev/null +++ b/ui/examples/native-hello/build-docker/el_native_vessel.h @@ -0,0 +1,69 @@ +#include +#include +#include "el_runtime.h" + +el_val_t native_init(void); +el_val_t native_run_loop(void); +el_val_t manifest_read(el_val_t path); +el_val_t manifest_title(el_val_t m); +el_val_t manifest_width(el_val_t m); +el_val_t manifest_height(el_val_t m); +el_val_t manifest_min_width(el_val_t m); +el_val_t manifest_min_height(el_val_t m); +el_val_t window_create(el_val_t title, el_val_t width, el_val_t height, el_val_t min_width, el_val_t min_height); +el_val_t window_from_manifest(el_val_t manifest_path); +el_val_t window_show(el_val_t handle); +el_val_t window_set_title(el_val_t handle, el_val_t title); +el_val_t vstack(el_val_t spacing); +el_val_t vstack_tight(void); +el_val_t hstack(el_val_t spacing); +el_val_t zstack(void); +el_val_t scroll(void); +el_val_t label(el_val_t text); +el_val_t button(el_val_t label); +el_val_t text_field(el_val_t placeholder); +el_val_t text_area(el_val_t placeholder); +el_val_t image(el_val_t path_or_name); +el_val_t widget_set_text(el_val_t handle, el_val_t text); +el_val_t widget_get_text(el_val_t handle); +el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left); +el_val_t widget_set_padding_all(el_val_t handle, el_val_t p); +el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py); +el_val_t widget_set_width(el_val_t handle, el_val_t width); +el_val_t widget_set_height(el_val_t handle, el_val_t height); +el_val_t widget_set_flex(el_val_t handle, el_val_t flex); +el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius); +el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled); +el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden); +el_val_t widget_add_child(el_val_t parent, el_val_t child); +el_val_t widget_remove_child(el_val_t parent, el_val_t child); +el_val_t widget_destroy(el_val_t handle); +el_val_t widget_on_click(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_change(el_val_t handle, el_val_t fn_name); +el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name); +el_val_t hex_channel(el_val_t s, el_val_t offset); +el_val_t hex_nibble(el_val_t c); +el_val_t color_hex_r(el_val_t hex); +el_val_t color_hex_g(el_val_t hex); +el_val_t color_hex_b(el_val_t hex); +el_val_t color_hex_a(el_val_t hex); +el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex); +el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex); +el_val_t style_surface(el_val_t handle); +el_val_t style_button_primary(el_val_t handle); +el_val_t style_label_body(el_val_t handle); +el_val_t style_label_heading(el_val_t handle); +el_val_t style_label_muted(el_val_t handle); + +el_val_t TOKEN_PRIMARY; +el_val_t TOKEN_ON_PRIMARY; +el_val_t TOKEN_BACKGROUND; +el_val_t TOKEN_ON_BG; +el_val_t TOKEN_SURFACE; +el_val_t TOKEN_ON_SURFACE; +el_val_t TOKEN_OUTLINE; +el_val_t TOKEN_ERROR; + diff --git a/ui/examples/native-hello/build-docker/native_hello.c b/ui/examples/native-hello/build-docker/native_hello.c new file mode 100644 index 0000000..050d56a --- /dev/null +++ b/ui/examples/native-hello/build-docker/native_hello.c @@ -0,0 +1,123 @@ +#include +#include +#include "el_runtime.h" + +el_val_t on_greet_click(el_val_t widget, el_val_t data); +el_val_t on_counter_click(el_val_t widget, el_val_t data); +el_val_t on_name_change(el_val_t widget, el_val_t data); +el_val_t app_build(el_val_t window); + +el_val_t g_window; +el_val_t g_label; +el_val_t g_input; +el_val_t g_button; +el_val_t g_counter; +el_val_t g_counter_lbl; +el_val_t manifest_env; +el_val_t manifest_path; +el_val_t win; +el_val_t g_window; + +el_val_t on_greet_click(el_val_t widget, el_val_t data) { + el_val_t name = widget_get_text(g_input); + el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; }); + widget_set_text(g_label, greeting); + return 0; +} + +el_val_t on_counter_click(el_val_t widget, el_val_t data) { + el_val_t g_counter = (g_counter + 1); + widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter))); + return 0; +} + +el_val_t on_name_change(el_val_t widget, el_val_t data) { + el_val_t has_text = (str_len(data) > 0); + widget_set_disabled(g_button, !has_text); + return 0; +} + +el_val_t app_build(el_val_t window) { + el_val_t root = vstack(0); + widget_set_padding_all(root, 24); + widget_set_bg_color_hex(root, EL_STR("#0f172a")); + widget_set_flex(root, 1); + el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo")); + style_label_heading(title); + widget_add_child(root, title); + el_val_t gap1 = label(EL_STR("")); + widget_set_height(gap1, 16); + widget_add_child(root, gap1); + el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer.")); + style_label_muted(subtitle); + widget_add_child(root, subtitle); + el_val_t gap2 = label(EL_STR("")); + widget_set_height(gap2, 24); + widget_add_child(root, gap2); + el_val_t input_row = hstack(8); + el_val_t name_label = label(EL_STR("Name:")); + style_label_body(name_label); + widget_set_width(name_label, 60); + el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6")); + style_label_body(input); + widget_set_flex(input, 1); + widget_on_change(input, EL_STR("on_name_change")); + el_val_t g_input = input; + el_val_t greet_btn = button(EL_STR("Greet")); + style_button_primary(greet_btn); + widget_set_disabled(greet_btn, 1); + widget_on_click(greet_btn, EL_STR("on_greet_click")); + el_val_t g_button = greet_btn; + widget_add_child(input_row, name_label); + widget_add_child(input_row, input); + widget_add_child(input_row, greet_btn); + widget_add_child(root, input_row); + el_val_t gap3 = label(EL_STR("")); + widget_set_height(gap3, 12); + widget_add_child(root, gap3); + el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6")); + style_label_body(greeting); + widget_set_color_hex(greeting, EL_STR("#60a5fa")); + widget_set_font(greeting, EL_STR("system"), 16, 1); + el_val_t g_label = greeting; + widget_add_child(root, greeting); + el_val_t gap4 = label(EL_STR("")); + widget_set_height(gap4, 24); + widget_add_child(root, gap4); + el_val_t counter_row = hstack(12); + el_val_t counter_lbl = label(EL_STR("Clicks: 0")); + style_label_body(counter_lbl); + el_val_t g_counter_lbl = counter_lbl; + el_val_t counter_btn = button(EL_STR("Click me")); + style_button_primary(counter_btn); + widget_on_click(counter_btn, EL_STR("on_counter_click")); + widget_add_child(counter_row, counter_lbl); + widget_add_child(counter_row, counter_btn); + widget_add_child(root, counter_row); + widget_add_child(window, root); + return 0; +} + +int main(int _argc, char** _argv) { + el_runtime_init_args(_argc, _argv); + g_window = (-1); + g_label = (-1); + g_input = (-1); + g_button = (-1); + g_counter = 0; + g_counter_lbl = (-1); + native_init(); + manifest_env = env(EL_STR("EL_MANIFEST")); + manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; }); + win = window_from_manifest(manifest_path); + g_window = win; + if (win < 0) { + println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?")); + exit_program(1); + } + app_build(win); + window_show(win); + native_run_loop(); + return 0; +} + diff --git a/ui/examples/native-hello/build-docker/runtime/el_gtk4.c b/ui/examples/native-hello/build-docker/runtime/el_gtk4.c new file mode 100644 index 0000000..1ae01d0 --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_gtk4.c @@ -0,0 +1,1027 @@ +/* + * el_gtk4.c — GTK4 backend for the el native widget system (Linux). + * + * This file implements the Linux GTK4 widget layer that el_seed.c calls + * through to when EL_TARGET_LINUX is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_gtk4_* C-callable functions declared here + * → GtkWidget* / GtkWindow* via GTK4 C API + * + * Widget handles: every widget is assigned an int64_t slot index into + * _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved; invalid handle = -1 convention). + * + * Threading: GTK4 must run on the main thread. el_gtk4_sync_main() uses + * g_main_context_invoke_full() to dispatch from any thread, waiting for + * completion via a GMutex/GCond pair — the same role as dispatch_sync() in + * the AppKit bridge. + * + * Callback mechanism: on signal, the bridge calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * matching the el_appkit.m / __thread_create pattern in el_seed.c. + * + * Compile: + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + * el_gtk4.c -c -o el_gtk4.o + * Then link el_gtk4.o alongside el_seed.c with: + * $(pkg-config --libs gtk4) -ldl + * + * GTK4 version notes: + * • gtk_widget_get_style_context() is deprecated in GTK 4.10+. + * A #if GTK_MINOR_VERSION guard selects the correct CSS-provider path. + * • gtk_editable_get_text() / gtk_editable_set_text() are the unified + * GTK4 text API for GtkEntry and any GtkEditable. + */ + +#ifdef EL_TARGET_LINUX + +/* _GNU_SOURCE: expose RTLD_DEFAULT (dlfcn.h) and strdup (string.h) on Linux. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include + +/* G_APPLICATION_DEFAULT_FLAGS was added in GLib 2.74. + * Ubuntu 22.04 ships GLib 2.72 — use the older alias when running on it. */ +#ifndef G_APPLICATION_DEFAULT_FLAGS +#define G_APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE +#endif + +/* el_val_t must already be defined by el_runtime.h / el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +#ifndef EL_CSTR +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#endif +#ifndef EL_STR +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#endif + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_GTK4_MAX_WIDGETS 4096 + +typedef enum { + EL_WIDGET_FREE = 0, + EL_WIDGET_WINDOW = 1, + EL_WIDGET_VSTACK = 2, + EL_WIDGET_HSTACK = 3, + EL_WIDGET_ZSTACK = 4, + EL_WIDGET_SCROLL = 5, + EL_WIDGET_LABEL = 6, + EL_WIDGET_BUTTON = 7, + EL_WIDGET_TEXTFIELD = 8, + EL_WIDGET_TEXTAREA = 9, + EL_WIDGET_IMAGE = 10, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + GtkWidget* widget; /* Strong reference (g_object_ref'd on alloc). */ + char* cb_click; /* El function name for click/submit events. */ + char* cb_change;/* El function name for value-change events. */ +} ElWidget; + +static ElWidget _el_widgets[EL_GTK4_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Allocate a slot; returns slot index or -1 if full. Takes a strong ref. */ +static int64_t el_widget_alloc(ElWidgetKind kind, GtkWidget* w) { + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].widget = (GtkWidget*)g_object_ref(w); + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +/* Look up a slot. Returns NULL for invalid/free handle. */ +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_GTK4_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +/* Release a slot. Unrefs the widget; callers must remove from parent first. */ +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->widget) { g_object_unref(w->widget); w->widget = NULL; } + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* Forward declaration — defined below in the GtkApplication globals section. */ +static int _el_app_running; + +/* ── CSS / styling helpers ────────────────────────────────────────────────── */ + +/* + * apply_css — attach a CSS snippet to a single widget with APPLICATION priority. + * GTK 4.10 deprecated gtk_widget_get_style_context(); use gtk_widget_add_css_class() + * + a display-level provider for new GTK, or the style-context path for older GTK. + * + * We use the style-context path behind a version guard because it reliably + * scopes the CSS to just this widget, which is what the el property setters + * need (each call replaces/adds to the widget's own stylesheet). + */ +static void apply_css(GtkWidget* widget, const char* css_str) { + if (!widget || !css_str) return; + GtkCssProvider* p = gtk_css_provider_new(); + +#if GTK_CHECK_VERSION(4, 12, 0) + /* GTK 4.12+: load_from_string takes a plain string (no length). */ + gtk_css_provider_load_from_string(p, css_str); +#else + gtk_css_provider_load_from_data(p, css_str, -1); +#endif + +#if GTK_MINOR_VERSION >= 10 + /* 4.10+: style context is deprecated; use display-scoped provider + css class. + * We add a unique widget name so the selector targets only this widget. */ + static guint64 _css_id_counter = 0; + char name_buf[32]; + snprintf(name_buf, sizeof(name_buf), "el-w-%" G_GUINT64_FORMAT, ++_css_id_counter); + gtk_widget_set_name(widget, name_buf); + + /* Wrap the CSS to scope to this widget's name. */ + char* scoped = NULL; + int n = asprintf(&scoped, "#%s { %s }", name_buf, css_str); + (void)n; + if (scoped) { + GtkCssProvider* sp = gtk_css_provider_new(); +#if GTK_CHECK_VERSION(4, 12, 0) + gtk_css_provider_load_from_string(sp, scoped); +#else + gtk_css_provider_load_from_data(sp, scoped, -1); +#endif + gtk_style_context_add_provider_for_display( + gtk_widget_get_display(widget), + GTK_STYLE_PROVIDER(sp), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(sp); + free(scoped); + } + g_object_unref(p); +#else + /* GTK < 4.10: style context is available and widget-scoped. */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_style_context_add_provider( + gtk_widget_get_style_context(widget), + GTK_STYLE_PROVIDER(p), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + G_GNUC_END_IGNORE_DEPRECATIONS + g_object_unref(p); +#endif +} + +/* ── Main-thread dispatch ────────────────────────────────────────────────── */ + +/* + * Dispatch a function on the GTK main thread, blocking until it completes. + * If already on the main thread, calls the function directly. + * + * Use a GMainContext + GMutex/GCond pair so we can synchronously wait for + * the idle callback to execute, mirroring dispatch_sync(main_queue, ...). + */ + +typedef struct { + void (*fn)(void*); + void* data; + GMutex mutex; + GCond cond; + int done; +} ElSyncCall; + +static gboolean _el_sync_dispatch(gpointer user_data) { + ElSyncCall* call = (ElSyncCall*)user_data; + call->fn(call->data); + g_mutex_lock(&call->mutex); + call->done = 1; + g_cond_signal(&call->cond); + g_mutex_unlock(&call->mutex); + return G_SOURCE_REMOVE; +} + +static void el_gtk4_sync_main(void (*fn)(void*), void* data) { + /* + * Pre-run phase (el boot sequence, before native_run_loop): + * The GMainContext is not yet iterating. We are on the main thread. + * Call directly — no dispatch needed. + * + * Post-run phase (callbacks from GTK signal handlers or other threads): + * Dispatch to the main context and wait for completion. + */ + if (!_el_app_running) { + fn(data); + return; + } + + /* Check if we're already on the thread running the default main context. */ + GMainContext* main_ctx = g_main_context_default(); + if (g_main_context_is_owner(main_ctx)) { + fn(data); + return; + } + ElSyncCall call; + call.fn = fn; + call.data = data; + call.done = 0; + g_mutex_init(&call.mutex); + g_cond_init(&call.cond); + + g_main_context_invoke(main_ctx, _el_sync_dispatch, &call); + + g_mutex_lock(&call.mutex); + while (!call.done) { + g_cond_wait(&call.cond, &call.mutex); + } + g_mutex_unlock(&call.mutex); + g_mutex_clear(&call.mutex); + g_cond_clear(&call.cond); +} + +/* Convenience macro: declare a local struct, fill it, dispatch. */ +#define EL_SYNC(type, var, ...) \ + type var = { __VA_ARGS__ }; \ + el_gtk4_sync_main(_el_sync_##var, &var) + +/* ── El callback invocation ──────────────────────────────────────────────── */ + +/* + * El callback functions have signature (per el_seed.c conventions): + * void fn_name(int64_t handle, int64_t data) + * where data is a (const char*) cast to int64_t — same as AppKit bridge. + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_gtk4_invoke_cb(const char* fn_name, int64_t handle, + const char* data) { + if (!fn_name || !*fn_name) return; + void* sym = dlsym(RTLD_DEFAULT, fn_name); + if (!sym) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── GtkApplication (global) ─────────────────────────────────────────────── */ + +static GtkApplication* _el_app = NULL; +static int _el_app_running = 0; /* set to 1 after activate fires */ + +/* ── Signal callbacks ────────────────────────────────────────────────────── */ + +/* Slot index stored as widget data so signal handlers can look it up. */ +#define EL_SLOT_KEY "el-slot" + +static void _el_on_button_clicked(GtkButton* btn, gpointer user_data) { + (void)btn; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + el_gtk4_invoke_cb(w->cb_click, slot, ""); + } +} + +static void _el_on_entry_changed(GtkEditable* editable, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_change) { + const char* text = gtk_editable_get_text(editable); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + } +} + +static void _el_on_entry_activate(GtkEntry* entry, gpointer user_data) { + (void)entry; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + const char* text = gtk_editable_get_text(GTK_EDITABLE(entry)); + el_gtk4_invoke_cb(w->cb_click, slot, text ? text : ""); + } +} + +/* GtkTextBuffer "changed" signal for textarea. */ +static void _el_on_textbuffer_changed(GtkTextBuffer* buf, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (!w || !w->cb_change) return; + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + char* text = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + g_free(text); +} + +/* ── GtkApplication activate callback ───────────────────────────────────── */ + +static void _el_app_activate(GtkApplication* app, gpointer user_data) { + /* + * Register all pre-created windows with the application, and mark the + * app as running so subsequent calls use el_gtk4_sync_main dispatching. + * + * On macOS the GTK4 Quartz backend requires g_application_run() on the + * main thread. Windows created before run use gtk_window_new(); we + * register them here via gtk_application_add_window() so they participate + * in the application lifecycle (app quits when last window closes, etc.). + */ + (void)user_data; + _el_app_running = 1; + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_WINDOW && _el_widgets[i].widget) { + gtk_application_add_window(app, GTK_WINDOW(_el_widgets[i].widget)); + } + } +} + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_gtk4_init — initialize GTK4 + GtkApplication. Idempotent. + * Must be called once before any other el_gtk4_* function. + */ +void el_gtk4_init(void) { + static int done = 0; + if (done) return; + done = 1; + + _el_app = gtk_application_new("ai.neuralplatform.el", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(_el_app, "activate", G_CALLBACK(_el_app_activate), NULL); +} + +/* + * el_gtk4_run_loop — start the GTK main loop. Never returns. + * el code calls __native_run_loop() which dispatches here. + * + * On macOS the GTK4 Quartz backend requires g_application_run() to be called + * from the main thread. Since the el boot sequence (native_init → window_create + * → widget_build → native_run_loop) runs on the main thread, g_application_run + * is always called from the main thread here. + * + * Windows created before this call use gtk_window_new() (no GApplication + * needed); the activate handler registers them with the GtkApplication. + */ +void el_gtk4_run_loop(void) { + if (!_el_app) el_gtk4_init(); + int status = g_application_run(G_APPLICATION(_el_app), 0, NULL); + (void)status; +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +typedef struct { + const char* title; + int width, height, min_width, min_height; + int64_t result; +} _ElWindowCreateArgs; + +static void _el_window_create_main(void* vp) { + _ElWindowCreateArgs* a = (_ElWindowCreateArgs*)vp; + /* + * Use gtk_window_new() rather than gtk_application_window_new(). + * gtk_application_window_new() requires the GApplication to be running + * (i.e., after g_application_run / activate), which is not yet the case + * when the el boot sequence creates windows before native_run_loop(). + * The activate handler in el_gtk4_run_loop registers these windows with + * the GtkApplication via gtk_application_add_window(). + */ + GtkWidget* win = gtk_window_new(); + gtk_window_set_title(GTK_WINDOW(win), a->title ? a->title : ""); + gtk_window_set_default_size(GTK_WINDOW(win), a->width, a->height); + + /* Set minimum size via content-area size hint. */ + if (a->min_width > 0 || a->min_height > 0) { + gtk_widget_set_size_request(win, a->min_width, a->min_height); + } + + /* Use a vertical GtkBox as the root content widget so children stack + * vertically by default — mirrors the NSStackView root in AppKit. */ + GtkWidget* root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_window_set_child(GTK_WINDOW(win), root); + + a->result = el_widget_alloc(EL_WIDGET_WINDOW, win); +} + +int64_t el_gtk4_window_create(const char* title, int width, int height, + int min_width, int min_height) { + _ElWindowCreateArgs a = { title, width, height, min_width, min_height, -1 }; + el_gtk4_sync_main(_el_window_create_main, &a); + return a.result; +} + +typedef struct { int64_t handle; } _ElHandleArgs; + +static void _el_window_show_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + gtk_window_present(GTK_WINDOW(w->widget)); +} + +void el_gtk4_window_show(int64_t handle) { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_window_show_main, &a); +} + +typedef struct { int64_t handle; const char* title; } _ElSetTitleArgs; + +static void _el_window_set_title_main(void* vp) { + _ElSetTitleArgs* a = (_ElSetTitleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + gtk_window_set_title(GTK_WINDOW(w->widget), a->title ? a->title : ""); +} + +void el_gtk4_window_set_title(int64_t handle, const char* title) { + _ElSetTitleArgs a = { handle, title }; + el_gtk4_sync_main(_el_window_set_title_main, &a); +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +typedef struct { + GtkOrientation orient; + int spacing; + int64_t result; +} _ElStackCreateArgs; + +static void _el_stack_create_main(void* vp) { + _ElStackCreateArgs* a = (_ElStackCreateArgs*)vp; + GtkWidget* box = gtk_box_new(a->orient, a->spacing); + ElWidgetKind kind = (a->orient == GTK_ORIENTATION_VERTICAL) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + a->result = el_widget_alloc(kind, box); +} + +int64_t el_gtk4_vstack_create(int spacing) { + _ElStackCreateArgs a = { GTK_ORIENTATION_VERTICAL, spacing, -1 }; + el_gtk4_sync_main(_el_stack_create_main, &a); + return a.result; +} + +int64_t el_gtk4_hstack_create(int spacing) { + _ElStackCreateArgs a = { GTK_ORIENTATION_HORIZONTAL, spacing, -1 }; + el_gtk4_sync_main(_el_stack_create_main, &a); + return a.result; +} + +typedef struct { int64_t result; } _ElResultArgs; + +static void _el_zstack_create_main(void* vp) { + _ElResultArgs* a = (_ElResultArgs*)vp; + GtkWidget* overlay = gtk_overlay_new(); + a->result = el_widget_alloc(EL_WIDGET_ZSTACK, overlay); +} + +int64_t el_gtk4_zstack_create(void) { + _ElResultArgs a = { -1 }; + el_gtk4_sync_main(_el_zstack_create_main, &a); + return a.result; +} + +static void _el_scroll_create_main(void* vp) { + _ElResultArgs* a = (_ElResultArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + a->result = el_widget_alloc(EL_WIDGET_SCROLL, sw); +} + +int64_t el_gtk4_scroll_create(void) { + _ElResultArgs a = { -1 }; + el_gtk4_sync_main(_el_scroll_create_main, &a); + return a.result; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +typedef struct { const char* text; int64_t result; } _ElTextArgs; + +static void _el_label_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* lbl = gtk_label_new(a->text ? a->text : ""); + gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f); + gtk_label_set_wrap(GTK_LABEL(lbl), FALSE); + a->result = el_widget_alloc(EL_WIDGET_LABEL, lbl); +} + +int64_t el_gtk4_label_create(const char* text) { + _ElTextArgs a = { text, -1 }; + el_gtk4_sync_main(_el_label_create_main, &a); + return a.result; +} + +static void _el_button_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* btn = gtk_button_new_with_label(a->text ? a->text : ""); + int64_t slot = el_widget_alloc(EL_WIDGET_BUTTON, btn); + if (slot >= 0) { + g_signal_connect(btn, "clicked", + G_CALLBACK(_el_on_button_clicked), + (gpointer)(intptr_t)slot); + } + a->result = slot; +} + +int64_t el_gtk4_button_create(const char* label) { + _ElTextArgs a = { label, -1 }; + el_gtk4_sync_main(_el_button_create_main, &a); + return a.result; +} + +static void _el_text_field_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* entry = gtk_entry_new(); + if (a->text && *a->text) { + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), a->text); + } + int64_t slot = el_widget_alloc(EL_WIDGET_TEXTFIELD, entry); + if (slot >= 0) { + gpointer slot_ptr = (gpointer)(intptr_t)slot; + g_signal_connect(entry, "changed", + G_CALLBACK(_el_on_entry_changed), slot_ptr); + g_signal_connect(entry, "activate", + G_CALLBACK(_el_on_entry_activate), slot_ptr); + } + a->result = slot; +} + +int64_t el_gtk4_text_field_create(const char* placeholder) { + _ElTextArgs a = { placeholder, -1 }; + el_gtk4_sync_main(_el_text_field_create_main, &a); + return a.result; +} + +static void _el_text_area_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(sw, TRUE); + + GtkWidget* tv = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(tv), GTK_WRAP_WORD_CHAR); + gtk_widget_set_hexpand(tv, TRUE); + gtk_widget_set_vexpand(tv, TRUE); + + /* GTK4 GtkTextView has no native placeholder; set accessible description. */ + if (a->text && *a->text) { + gtk_accessible_update_property(GTK_ACCESSIBLE(tv), + GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, a->text, -1); + } + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), tv); + + /* Store the scroll window as the widget; text view accessible via child. */ + int64_t slot = el_widget_alloc(EL_WIDGET_TEXTAREA, sw); + if (slot >= 0) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + g_signal_connect(buf, "changed", + G_CALLBACK(_el_on_textbuffer_changed), + (gpointer)(intptr_t)slot); + } + a->result = slot; +} + +int64_t el_gtk4_text_area_create(const char* placeholder) { + _ElTextArgs a = { placeholder, -1 }; + el_gtk4_sync_main(_el_text_area_create_main, &a); + return a.result; +} + +static void _el_image_create_main(void* vp) { + _ElTextArgs* a = (_ElTextArgs*)vp; + GtkWidget* img = NULL; + if (a->text && *a->text) { + /* Try as a filesystem path first. */ + if (g_file_test(a->text, G_FILE_TEST_EXISTS)) { + img = gtk_picture_new_for_filename(a->text); + } + if (!img) { + /* Fall back to icon name. */ + img = gtk_image_new_from_icon_name(a->text); + } + } + if (!img) { + img = gtk_picture_new(); + } + a->result = el_widget_alloc(EL_WIDGET_IMAGE, img); +} + +int64_t el_gtk4_image_create(const char* path_or_name) { + _ElTextArgs a = { path_or_name, -1 }; + el_gtk4_sync_main(_el_image_create_main, &a); + return a.result; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +typedef struct { int64_t handle; const char* text; } _ElWidgetTextArgs; + +static void _el_widget_set_text_main(void* vp) { + _ElWidgetTextArgs* a = (_ElWidgetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + const char* s = a->text ? a->text : ""; + switch (w->kind) { + case EL_WIDGET_LABEL: + gtk_label_set_text(GTK_LABEL(w->widget), s); + break; + case EL_WIDGET_TEXTFIELD: + gtk_editable_set_text(GTK_EDITABLE(w->widget), s); + break; + case EL_WIDGET_BUTTON: + gtk_button_set_label(GTK_BUTTON(w->widget), s); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + gtk_text_buffer_set_text(buf, s, -1); + } + break; + } + case EL_WIDGET_WINDOW: + gtk_window_set_title(GTK_WINDOW(w->widget), s); + break; + default: break; + } +} + +void el_gtk4_widget_set_text(int64_t handle, const char* text) { + _ElWidgetTextArgs a = { handle, text }; + el_gtk4_sync_main(_el_widget_set_text_main, &a); +} + +typedef struct { int64_t handle; const char* result; } _ElWidgetGetTextArgs; + +static void _el_widget_get_text_main(void* vp) { + _ElWidgetGetTextArgs* a = (_ElWidgetGetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) { a->result = ""; return; } + switch (w->kind) { + case EL_WIDGET_LABEL: + a->result = gtk_label_get_text(GTK_LABEL(w->widget)); + break; + case EL_WIDGET_TEXTFIELD: + a->result = gtk_editable_get_text(GTK_EDITABLE(w->widget)); + break; + case EL_WIDGET_BUTTON: + a->result = gtk_button_get_label(GTK_BUTTON(w->widget)); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + /* Returns g_malloc'd string — caller treats it as short-lived. */ + a->result = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + } else { + a->result = ""; + } + break; + } + default: + a->result = ""; + break; + } +} + +const char* el_gtk4_widget_get_text(int64_t handle) { + _ElWidgetGetTextArgs a = { handle, "" }; + el_gtk4_sync_main(_el_widget_get_text_main, &a); + return a.result ? strdup(a.result) : strdup(""); +} + +typedef struct { + int64_t handle; + float r, g, b, a; +} _ElColorArgs; + +static void _el_widget_set_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[128]; + snprintf(css, sizeof(css), "color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_color(int64_t handle, float r, float g, float b, float a) { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_color_main, &args); +} + +static void _el_widget_set_bg_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + char css[128]; + snprintf(css, sizeof(css), "background-color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_bg_color_main, &args); +} + +typedef struct { + int64_t handle; + const char* family; + int size; + int bold; +} _ElFontArgs; + +static void _el_widget_set_font_main(void* vp) { + _ElFontArgs* a = (_ElFontArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[256]; + const char* fam = (a->family && *a->family && strcmp(a->family, "system") != 0) + ? a->family : "sans-serif"; + snprintf(css, sizeof(css), + "font-family: %s; font-size: %dpx; font-weight: %s;", + fam, a->size, a->bold ? "bold" : "normal"); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_font(int64_t handle, const char* family, int size, int bold) { + _ElFontArgs a = { handle, family, size, bold }; + el_gtk4_sync_main(_el_widget_set_font_main, &a); +} + +typedef struct { int64_t handle; int top, right, bottom, left; } _ElPaddingArgs; + +static void _el_widget_set_padding_main(void* vp) { + _ElPaddingArgs* a = (_ElPaddingArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + char css[128]; + snprintf(css, sizeof(css), "padding: %dpx %dpx %dpx %dpx;", + a->top, a->right, a->bottom, a->left); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + _ElPaddingArgs a = { handle, top, right, bottom, left }; + el_gtk4_sync_main(_el_widget_set_padding_main, &a); +} + +typedef struct { int64_t handle; int value; } _ElIntPropArgs; + +static void _el_widget_set_width_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gint cur_h = -1; + gtk_widget_get_size_request(w->widget, NULL, &cur_h); + gtk_widget_set_size_request(w->widget, a->value, cur_h); +} + +void el_gtk4_widget_set_width(int64_t handle, int width) { + _ElIntPropArgs a = { handle, width }; + el_gtk4_sync_main(_el_widget_set_width_main, &a); +} + +static void _el_widget_set_height_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gint cur_w = -1; + gtk_widget_get_size_request(w->widget, &cur_w, NULL); + gtk_widget_set_size_request(w->widget, cur_w, a->value); +} + +void el_gtk4_widget_set_height(int64_t handle, int height) { + _ElIntPropArgs a = { handle, height }; + el_gtk4_sync_main(_el_widget_set_height_main, &a); +} + +static void _el_widget_set_flex_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gboolean expand = (a->value > 0) ? TRUE : FALSE; + gtk_widget_set_hexpand(w->widget, expand); + gtk_widget_set_vexpand(w->widget, expand); +} + +void el_gtk4_widget_set_flex(int64_t handle, int flex) { + _ElIntPropArgs a = { handle, flex }; + el_gtk4_sync_main(_el_widget_set_flex_main, &a); +} + +static void _el_widget_set_corner_radius_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + char css[64]; + snprintf(css, sizeof(css), "border-radius: %dpx;", a->value); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_corner_radius(int64_t handle, int radius) { + _ElIntPropArgs a = { handle, radius }; + el_gtk4_sync_main(_el_widget_set_corner_radius_main, &a); +} + +static void _el_widget_set_disabled_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + gtk_widget_set_sensitive(w->widget, !a->value); +} + +void el_gtk4_widget_set_disabled(int64_t handle, int disabled) { + _ElIntPropArgs a = { handle, disabled }; + el_gtk4_sync_main(_el_widget_set_disabled_main, &a); +} + +static void _el_widget_set_hidden_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + gtk_widget_set_visible(w->widget, !a->value); +} + +void el_gtk4_widget_set_hidden(int64_t handle, int hidden) { + _ElIntPropArgs a = { handle, hidden }; + el_gtk4_sync_main(_el_widget_set_hidden_main, &a); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +typedef struct { int64_t parent; int64_t child; } _ElParentChildArgs; + +static void _el_widget_add_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add a window as child */ + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Append to the root VBox that window_create installs as child. */ + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_append(GTK_BOX(root), child_widget); + } else { + /* Fallback: replace root child. */ + gtk_window_set_child(GTK_WINDOW(pw->widget), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_append(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + /* First child becomes the base; subsequent children are overlays. */ + if (!gtk_overlay_get_child(GTK_OVERLAY(pw->widget))) { + gtk_overlay_set_child(GTK_OVERLAY(pw->widget), child_widget); + } else { + gtk_overlay_add_overlay(GTK_OVERLAY(pw->widget), child_widget); + } + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), + child_widget); + break; + default: + /* Generic fallback — shouldn't be reached for standard widget kinds. */ + break; + } +} + +void el_gtk4_widget_add_child(int64_t parent, int64_t child) { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_add_child_main, &a); +} + +static void _el_widget_remove_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_remove(GTK_BOX(root), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_remove(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + gtk_overlay_remove_overlay(GTK_OVERLAY(pw->widget), child_widget); + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), NULL); + break; + default: + break; + } +} + +void el_gtk4_widget_remove_child(int64_t parent, int64_t child) { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_remove_child_main, &a); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +/* + * on_click / on_change / on_submit just store the function name string. + * The actual g_signal_connect calls happen at widget creation time. + * Here we update the stored callback so the already-connected signal handler + * will call the new function name. + */ + +void el_gtk4_widget_on_click(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_gtk4_widget_on_change(int64_t handle, const char* fn_name) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; +} + +void el_gtk4_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, "submit" = Enter key activate → stored in cb_click. */ + el_gtk4_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +static void _el_widget_destroy_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + if (w->kind == EL_WIDGET_WINDOW) { + gtk_window_destroy(GTK_WINDOW(w->widget)); + } else { + /* Unparent the widget before freeing the slot. */ + GtkWidget* parent = gtk_widget_get_parent(w->widget); + if (parent) { + if (GTK_IS_BOX(parent)) { + gtk_box_remove(GTK_BOX(parent), w->widget); + } else if (GTK_IS_OVERLAY(parent)) { + gtk_overlay_remove_overlay(GTK_OVERLAY(parent), w->widget); + } else if (GTK_IS_SCROLLED_WINDOW(parent)) { + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(parent), NULL); + } + /* gtk_window_get_child case: the parent is a GtkBox inside a window */ + } + } + el_widget_free(a->handle); +} + +void el_gtk4_widget_destroy(int64_t handle) { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_widget_destroy_main, &a); +} + +#endif /* EL_TARGET_LINUX */ diff --git a/ui/examples/native-hello/build-docker/runtime/el_native_target.h b/ui/examples/native-hello/build-docker/runtime/el_native_target.h new file mode 100644 index 0000000..dc1edcb --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_native_target.h @@ -0,0 +1,573 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, + el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/ui/examples/native-hello/build-docker/runtime/el_runtime.c b/ui/examples/native-hello/build-docker/runtime/el_runtime.c new file mode 100644 index 0000000..a735c9f --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_runtime.c @@ -0,0 +1,11733 @@ +/* + * el_runtime.c — El language C runtime implementation + * + * All functions use el_val_t (= int64_t) as the universal value type. + * Strings are transported as their pointer address cast to int64_t. + * On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe. + * + * Compile with: + * cc -std=c11 -I -lcurl -lpthread -o .c el_runtime.c + * + * Link requirements: -lcurl (HTTP client + LLM), -lpthread (HTTP server). + */ + +/* Feature-test macros must be set before any standard headers. _GNU_SOURCE + * exposes clock_gettime/CLOCK_REALTIME, strcasecmp, and the dlfcn extensions + * (RTLD_DEFAULT) — all of which macOS hands us without asking but glibc on + * Debian gates behind an explicit opt-in. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_runtime.h" + +#include +#include /* strcasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* dlsym for http_set_handler fallback */ +#include +#include +#include +#include +#include +#include /* getrusage — memory guard */ +#ifdef HAVE_CURL +#include +#endif + +/* ── Internal allocators ─────────────────────────────────────────────────── */ + +/* + * Per-request string arena + * + * Every El string allocated via el_strbuf / el_strdup during an HTTP request + * is registered in a thread-local arena. When el_request_end() is called at + * the end of the worker thread, every arena entry is freed — recovering all + * the intermediate strings from el_str_concat chains (build_system_prompt, + * engram_compile, etc.) that are otherwise leaked forever. + * + * Long-lived allocations (state_set values, engram internal storage) call + * el_strdup_persist() / el_strbuf_persist() which bypass the arena entirely. + */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static _Thread_local ElArena _tl_arena = {NULL, 0, 0}; +static _Thread_local int _tl_arena_active = 0; + +/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response. + * Allows serving PNGs and other binary files without strlen truncation. */ +static _Thread_local size_t _tl_fs_read_len = 0; + +static void el_arena_track(char* p) { + if (!_tl_arena_active || !p) return; + if (_tl_arena.count >= _tl_arena.cap) { + size_t nc = _tl_arena.cap == 0 ? EL_ARENA_INITIAL : _tl_arena.cap * 2; + char** grown = realloc(_tl_arena.ptrs, nc * sizeof(char*)); + if (!grown) return; /* can't track — will leak this one ptr, but don't crash */ + _tl_arena.ptrs = grown; + _tl_arena.cap = nc; + } + _tl_arena.ptrs[_tl_arena.count++] = p; +} + +/* Called by http_worker before dispatching the El handler. */ +void el_request_start(void) { + _tl_arena.count = 0; + _tl_arena_active = 1; +} + +/* Called by http_worker after the El handler returns and the response is sent. + * Frees every intermediate string allocated during the request. */ +void el_request_end(void) { + _tl_arena_active = 0; + for (size_t i = 0; i < _tl_arena.count; i++) { + free(_tl_arena.ptrs[i]); + } + _tl_arena.count = 0; +} + +/* ── Scoped arena for CLI use ─────────────────────────────────────────────── * + * CLI programs never call el_request_start/end, so all strdup allocations are + * permanent. el_arena_push/pop let the compiler free intermediate strings + * after each compilation unit. + * + * el_arena_push() — activates the arena if not already active, saves the + * current arena count as a mark, and returns it as an el_val_t Int. + * el_arena_pop(mark) — frees all strings allocated since the push mark and + * resets the count. If count reaches 0, deactivates the arena. + */ +#define EL_ARENA_SCOPE_DEPTH 32 +static _Thread_local size_t _tl_arena_scope[EL_ARENA_SCOPE_DEPTH]; +static _Thread_local int _tl_arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_tl_arena_active) { + _tl_arena_active = 1; + } + if (_tl_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) { + _tl_arena_scope[_tl_arena_scope_depth++] = _tl_arena.count; + } + return (el_val_t)(int64_t)_tl_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _tl_arena.count) save = 0; + for (size_t i = save; i < _tl_arena.count; i++) { + if (_tl_arena.ptrs[i]) { + free(_tl_arena.ptrs[i]); + _tl_arena.ptrs[i] = NULL; + } + } + _tl_arena.count = save; + if (_tl_arena_scope_depth > 0) _tl_arena_scope_depth--; + if (save == 0) _tl_arena_active = 0; + return 0; +} + +/* Persistent allocation — bypasses the arena (state_set, engram internals). */ +static char* el_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} +static char* el_strbuf_persist(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +/* Wrap an allocated C string as el_val_t */ +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) puts(str); + else puts(""); + return 0; +} + +el_val_t print(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) fputs(str, stdout); + return 0; +} + +el_val_t readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +/* __read_n — read exactly n bytes from stdin. + * Allocates a buffer of size n+1, calls fread(buf, 1, n, stdin) to read + * exactly n raw bytes (including \r, \n, NUL, etc.), null-terminates, and + * returns the buffer as an El String. Returns "" on EOF or I/O error. + * + * Used by the El LSP server to read JSON-RPC message bodies after parsing + * the Content-Length header. readline() cannot be used for the body because + * it stops at the first \n and LSP JSON bodies are not newline-terminated. */ +el_val_t __read_n(el_val_t nv) { + int64_t n = EL_INT(nv); + if (n <= 0) return el_wrap_str(el_strdup("")); + char* buf = malloc((size_t)n + 1); + if (!buf) { fputs("el_runtime: __read_n: out of memory\n", stderr); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)n, stdin); + buf[got] = '\0'; + if (got == 0) { free(buf); return el_wrap_str(el_strdup("")); } + /* Track in arena so the allocation is freed when the request ends. */ + el_arena_track(buf); + return el_wrap_str(buf); +} + +/* __print_raw — write a string to stdout without any modification. + * Unlike println/print (which call puts/fputs and may add newlines or flush + * in platform-specific ways), this uses fwrite with the exact byte count so + * that embedded \r\n pairs in LSP Content-Length headers survive intact. */ +void __print_raw(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return; + size_t len = strlen(s); + fwrite(s, 1, len, stdout); + fflush(stdout); +} + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* prefix = EL_CSTR(prefv); + if (!s || !prefix) return 0; + size_t lp = strlen(prefix); + return (el_val_t)(strncmp(s, prefix, lp) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suffix = EL_CSTR(sufv); + if (!s || !suffix) return 0; + size_t ls = strlen(s); + size_t lsuf = strlen(suffix); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)strlen(s); +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)atoll(s); +} + +/* native_str_to_int — El compiler-generated alias for str_to_int. + * Converts a string el_val_t to its integer representation. */ +el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); } + +el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s); + size_t lf = strlen(from); + size_t lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t out_sz = ls + count * lt + 1; + char* out = el_strbuf(out_sz); + char* dst = out; + p = s; + const char* found; + while ((found = strstr(p, from)) != NULL) { + size_t chunk = (size_t)(found - p); + memcpy(dst, p, chunk); dst += chunk; + memcpy(dst, to, lt); dst += lt; + p = found + lf; + } + strcpy(dst, p); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; + va_start(ap, count); + for (int64_t i = 0; i < count; i++) { + lst->elems[i] = va_arg(ap, el_val_t); + } + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length; + fresh->capacity = cap; + fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; + va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; } + +el_val_t el_map_get(el_val_t mapv, el_val_t keyv) { + ElMap* m = as_map(mapv); + const char* key = EL_CSTR(keyv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { + return el_map_get(mapv, keyv); +} + +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { + const char* key = EL_CSTR(keyv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; + } + m->keys[m->count] = keyv; + m->values[m->count] = value; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } +} + +/* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ + +typedef struct JsonBuf JsonBuf; +typedef struct JsonParser JsonParser; +static void jb_init(JsonBuf* b); +static void jb_putc(JsonBuf* b, char c); +static void jb_puts(JsonBuf* b, const char* s); +static void jb_emit_escaped(JsonBuf* b, const char* s); +static int looks_like_string(el_val_t v); +static const char* json_find_key(const char* s, const char* key); +static const char* json_skip_value(const char* p); +static char* jp_parse_string_raw(JsonParser* jp); + +/* Struct definitions are visible here because batch 2/3 helpers above use + * them by value; the bodies (jb_init, etc.) appear in the JSON section. */ +struct JsonBuf { + char* buf; + size_t len; + size_t cap; +}; + +struct JsonParser { + const char* p; + const char* end; + int err; +}; + +/* ── Batch 2: Real HTTP (libcurl client + POSIX-socket server) ───────────── */ +/* + * Client: blocking libcurl easy-handle calls. Errors are returned as a JSON + * fragment {"error":"..."} so callers can detect via str_starts_with("{") / + * json_get_string("error", ...). + * + * Server: bind/listen/accept loop on a TCP socket. Each accepted connection + * is handled in its own pthread (detached). A semaphore-style counter caps + * concurrent in-flight connections at HTTP_MAX_CONNS (64). When the cap is + * reached, accept() blocks until a worker exits. This prevents runaway + * thread creation under high load. + * + * Handler dispatch: El does not expose first-class function references at + * the runtime layer, so the second argument to http_serve(port, handler) is + * treated as a string name (or any el_val_t — the runtime ignores its + * value and uses the registry). Callers register a C-level handler via + * + * extern void el_runtime_register_handler(const char* name, + * el_val_t (*fn)(el_val_t, + * el_val_t, + * el_val_t)); + * + * and select the active handler by calling http_set_handler("name") from + * El, or by setting it directly through the C registry. If no handler is + * registered, the server replies with a 200 carrying a default message so + * the loop is observable. + */ + +/* ── JSON error helper (used by HTTP, PQ, crypto stubs) ─────────────────── */ + +/* JSON-escape an arbitrary C string into an allocated buffer. */ +static char* json_escape_alloc(const char* s) { + if (!s) return el_strdup(""); + JsonBuf b; jb_init(&b); + for (const char* p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': jb_puts(&b, "\\\""); break; + case '\\': jb_puts(&b, "\\\\"); break; + case '\n': jb_puts(&b, "\\n"); break; + case '\r': jb_puts(&b, "\\r"); break; + case '\t': jb_puts(&b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(&b, tmp); + } else jb_putc(&b, (char)c); + } + } + return b.buf; +} + +static el_val_t http_error_json(const char* msg) { + char* esc = json_escape_alloc(msg ? msg : "unknown error"); + char* buf = el_strbuf(strlen(esc) + 16); + sprintf(buf, "{\"error\":\"%s\"}", esc); + free(esc); + return el_wrap_str(buf); +} + +#ifdef HAVE_CURL +/* ── HTTP client write-callback buffer ───────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} HttpBuf; + +static void httpbuf_init(HttpBuf* b) { + b->cap = 1024; + b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void httpbuf_append(HttpBuf* b, const void* src, size_t n) { + if (b->len + n + 1 > b->cap) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + httpbuf_append((HttpBuf*)ud, ptr, n); + return n; +} + +/* HTTP timeout (ms) — read once from EL_HTTP_TIMEOUT_MS, default 60000. + * Applied via CURLOPT_TIMEOUT_MS on every libcurl request. */ +static long _el_http_timeout_ms = -1; +static long el_http_timeout_ms(void) { + long v = __atomic_load_n(&_el_http_timeout_ms, __ATOMIC_ACQUIRE); + if (v >= 0) return v; + const char* s = getenv("EL_HTTP_TIMEOUT_MS"); + long parsed = 60000L; + if (s && *s) { + char* end = NULL; + long n = strtol(s, &end, 10); + if (end != s && n > 0) parsed = n; + } + __atomic_store_n(&_el_http_timeout_ms, parsed, __ATOMIC_RELEASE); + return parsed; +} + +/* Internal: do a libcurl request; takes optional body/headers, optional method override. */ +static el_val_t http_do(const char* method, const char* url, const char* body, + struct curl_slist* extra_headers) { + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } else if (method && strcmp(method, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +el_val_t http_get(el_val_t url) { + return http_do("GET", EL_CSTR(url), NULL, NULL); +} + +el_val_t http_post(el_val_t url, el_val_t body) { + return http_do("POST", EL_CSTR(url), EL_CSTR(body), NULL); +} + +el_val_t http_post_json(el_val_t url, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +/* Build a curl_slist from an ElMap of name -> value strings. */ +static struct curl_slist* headers_from_map(el_val_t headers_map) { + struct curl_slist* h = NULL; + ElMap* m = as_map(headers_map); + if (!m) return NULL; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + return h; +} + +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("GET", EL_CSTR(url), NULL, h); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* http_post_json_with_headers — POST with Content-Type: application/json plus + * any additional headers supplied as an El map. Combines http_post_json and + * http_post_with_headers: the Content-Type header is always prepended so + * callers do not have to include it in their map. */ +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + /* Append caller-supplied headers from the map */ + ElMap* m = as_map(headers_map); + if (m) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + size_t n = strlen(k) + strlen(v) + 4; + char* line = malloc(n); + if (!line) continue; + snprintf(line, n, "%s: %s", k, v); + h = curl_slist_append(h, line); + free(line); + } + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(json_body), h); + curl_slist_free_all(h); + return r; +} + +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header) { + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/x-www-form-urlencoded"); + const char* a = EL_CSTR(auth_header); + if (a && *a) { + size_t n = strlen(a) + 32; + char* line = malloc(n); + snprintf(line, n, "Authorization: %s", a); + h = curl_slist_append(h, line); + free(line); + } + el_val_t r = http_do("POST", EL_CSTR(url), EL_CSTR(form_body), h); + curl_slist_free_all(h); + return r; +} + +/* HTTP DELETE — mirrors http_post but with CURLOPT_CUSTOMREQUEST=DELETE. + * Returns response body on success; on transport failure returns an error + * JSON fragment (same convention as http_get/http_post). Callers that + * expect "" on failure should check for a leading '{' and an "error" key. */ +el_val_t http_delete(el_val_t url) { + return http_do("DELETE", EL_CSTR(url), NULL, NULL); +} + +/* ── HTTP → file streaming ──────────────────────────────────────────────── + * + * Why this exists: el_val_t strings are NUL-terminated by convention, so + * accumulating an HTTP response into an httpbuf and then wrapping its + * `.data` pointer with el_wrap_str() loses the byte length. Any consumer + * that does strlen() on the wrapped pointer truncates the body at the + * first embedded NUL. Audio (MP3, WAV, OGG), images (PNG, JPEG), and any + * other binary payload hits this. The vessels that download such bodies + * (e.g. ElevenLabs TTS → MP3) get silently corrupted files. + * + * The fix: wire libcurl's CURLOPT_WRITEFUNCTION directly to fwrite() + * against a fopen()-ed FILE*. The bytes never pass through an el_val_t + * string, so embedded NULs are preserved verbatim. Caller's contract is + * just "a file at this path with the response body in it". */ + +static size_t http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + FILE* f = (FILE*)ud; + return fwrite(ptr, size, nmemb, f); +} + +/* Internal: stream body to file. method is "GET" or "POST". body may be NULL + * (GET) or NUL-terminated (POST). headers may be NULL. Returns 1/0. */ +static el_val_t http_do_to_file(const char* method, const char* url, + const char* body, struct curl_slist* extra_headers, + const char* output_path) { + if (!url || !*url) return 0; + if (!output_path || !*output_path) return 0; + FILE* f = fopen(output_path, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(output_path); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + curl_easy_setopt(c, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx → CURLE_HTTP_RETURNED_ERROR */ + if (extra_headers) curl_easy_setopt(c, CURLOPT_HTTPHEADER, extra_headers); + + if (method && strcmp(method, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + /* For the request body we still rely on strlen — POST bodies are + * caller-controlled and JSON/text in every known El use case. + * If a future caller needs a binary POST body, add a *_bytes + * variant that takes an explicit length, mirroring fs_write_bytes. */ + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + curl_easy_cleanup(c); + + /* Flush + close before signalling success, so the file is fully on disk + * by the time the caller reads back. */ + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + + if (rc != CURLE_OK || !flush_ok || !close_ok) { + remove(output_path); + return 0; + } + return 1; +} + +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("GET", EL_CSTR(url), NULL, h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} + +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path) { + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do_to_file("POST", EL_CSTR(url), EL_CSTR(body), h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +/* ── HTTP server (POSIX sockets + pthreads) ──────────────────────────────── */ + +#define HTTP_MAX_CONNS 64 + +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); + +typedef struct { + char* name; + http_handler_fn fn; +} HttpHandlerEntry; + +static HttpHandlerEntry _http_handlers[32]; +static size_t _http_handler_count = 0; +static char* _http_active_handler = NULL; +static pthread_mutex_t _http_handler_mu = PTHREAD_MUTEX_INITIALIZER; + +static pthread_mutex_t _http_conn_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _http_conn_cv = PTHREAD_COND_INITIALIZER; +static int _http_conn_active = 0; + +/* Public C-level API: register a handler by name. Programs that want El + * `http_serve` to dispatch into their handler call this from main() before + * http_serve. Not declared in the header to keep the public API minimal — + * extern lookup works since C symbols are global. */ +void el_runtime_register_handler(const char* name, http_handler_fn fn); +void el_runtime_register_handler(const char* name, http_handler_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, name) == 0) { + _http_handlers[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(name); + _http_handlers[_http_handler_count].fn = fn; + _http_handler_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler); + _http_active_handler = el_strdup(n ? n : ""); + /* If the name is not yet in the registry, try dlsym lookup against + * the running binary's symbol table. Every El `fn name(...)` compiles + * to a global C symbol with that exact name, so El programs can self- + * register their own handlers just by calling http_set_handler("name"). */ + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler_count < sizeof(_http_handlers) / sizeof(_http_handlers[0])) { + _http_handlers[_http_handler_count].name = el_strdup(n); + _http_handlers[_http_handler_count].fn = (http_handler_fn)sym; + _http_handler_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler_fn http_lookup_active(void) { + http_handler_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler) { + for (size_t i = 0; i < _http_handler_count; i++) { + if (strcmp(_http_handlers[i].name, _http_active_handler) == 0) { + out = _http_handlers[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Auto-detect Content-Type from response body. */ +static const char* http_detect_content_type(const char* body) { + if (!body) return "text/plain; charset=utf-8"; + const char* p = body; + /* Binary magic bytes — check before stripping whitespace */ + if ((unsigned char)p[0] == 0x89 && p[1]=='P' && p[2]=='N' && p[3]=='G') + return "image/png"; + if ((unsigned char)p[0] == 0xFF && (unsigned char)p[1] == 0xD8) + return "image/jpeg"; + if (strncmp(p, "GIF8", 4) == 0) return "image/gif"; + if (strncmp(p, "RIFF", 4) == 0) return "image/webp"; + if (strncmp(p, "wOFF", 4) == 0) return "font/woff"; + if (strncmp(p, "wOF2", 4) == 0) return "font/woff2"; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (strncasecmp(p, "= cap) { + if (cap >= 1024 * 1024) { free(buf); return -1; } + cap *= 2; + buf = realloc(buf, cap); + if (!buf) return -1; + } + ssize_t n = recv(fd, buf + len, cap - len - 1, 0); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + /* Parse request line */ + char* sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + *sp1 = '\0'; + *out_method = el_strdup(buf); + char* path_start = sp1 + 1; + char* sp2 = strchr(path_start, ' '); + if (!sp2) { free(*out_method); *out_method = NULL; free(buf); return -1; } + *sp2 = '\0'; + *out_path = el_strdup(path_start); + char* hdr_end = strstr(sp2 + 1, "\r\n\r\n"); + /* Capture the raw header block (after the request line's CRLF, up to + * but not including the terminating \r\n\r\n) for callers that asked + * for it. The legacy 3-arg path passes NULL and skips this. */ + if (out_headers_block) { + char* hdr_start = strstr(sp2 + 1, "\r\n"); + if (hdr_start && hdr_start < hdr_end) { + hdr_start += 2; + size_t hb_len = (size_t)(hdr_end - hdr_start); + char* hb = malloc(hb_len + 1); + if (hb) { + memcpy(hb, hdr_start, hb_len); + hb[hb_len] = '\0'; + *out_headers_block = hb; + } + } else { + *out_headers_block = el_strdup(""); + } + } + /* Find Content-Length */ + long content_length = 0; + char* hp = sp2 + 1; + while (hp < hdr_end) { + char* line_end = strstr(hp, "\r\n"); + /* line_end == hdr_end means we're on the LAST header line — its + * trailing \r\n is the same \r\n that begins the \r\n\r\n header + * terminator. Process this line; only stop when line_end is past + * hdr_end (which means the parser walked off the end of the + * header block). The previous condition (line_end >= hdr_end) + * silently dropped any Content-Length that appeared as the last + * header — exactly what real curl/clients tend to emit. */ + if (!line_end || line_end > hdr_end) break; + if (strncasecmp(hp, "Content-Length:", 15) == 0) { + content_length = strtol(hp + 15, NULL, 10); + if (content_length < 0) content_length = 0; + if (content_length > 64 * 1024 * 1024) content_length = 64 * 1024 * 1024; + } + hp = line_end + 2; + } + /* Body: any bytes already read past hdr_end, plus more recv */ + char* body_start = hdr_end + 4; + size_t body_have = (buf + len) - body_start; + char* body = malloc((size_t)content_length + 1); + if (!body) { free(*out_method); free(*out_path); *out_method=NULL; *out_path=NULL; free(buf); return -1; } + if ((long)body_have > content_length) body_have = (size_t)content_length; + if (body_have > 0) memcpy(body, body_start, body_have); + while ((long)body_have < content_length) { + ssize_t n = recv(fd, body + body_have, (size_t)content_length - body_have, 0); + if (n <= 0) break; + body_have += (size_t)n; + } + body[body_have] = '\0'; + *out_body = body; + free(buf); + return 0; +} + +/* Reason phrase for common HTTP statuses. Falls back to "Status" for the + * long tail — clients only care about the numeric code. */ +static const char* http_reason_phrase(int status) { + switch (status) { + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + default: return "Status"; + } +} + +/* Best-effort send with retry on partial writes. */ +static int http_send_all(int fd, const char* p, size_t left) { + while (left > 0) { + ssize_t w = send(fd, p, left, 0); + if (w <= 0) return -1; + p += w; left -= (size_t)w; + } + return 0; +} + +/* Discriminator that http_response() embeds at the start of its envelope. + * A handler returning a string starting with this exact prefix is treated + * as a structured response; anything else is treated as a raw body. */ +#define EL_HTTP_RESPONSE_TAG "{\"el_http_response\":1" + +/* Keys that conflict with runtime-managed headers are silently dropped to + * avoid double-emission — the runtime always emits its own Content-Length + * and Connection: close. Content-Type from the envelope IS allowed and + * overrides auto-detection. */ +static int http_header_is_managed(const char* k) { + return strcasecmp(k, "Content-Length") == 0 + || strcasecmp(k, "Connection") == 0; +} + +/* Walk an ElMap of header pairs and emit each as `K: V\r\n` into JsonBuf b. + * Sets *out_saw_content_type to 1 if the map contained an explicit + * Content-Type so the caller can skip auto-detection. */ +static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map, + int* out_saw_content_type) { + *out_saw_content_type = 0; + if (headers_map == 0) return; + ElMap* m = (ElMap*)(uintptr_t)headers_map; + if (!m || m->hdr.magic != EL_MAGIC_MAP) return; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + const char* v = EL_CSTR(m->values[i]); + if (!k || !v) continue; + if (http_header_is_managed(k)) continue; + if (strcasecmp(k, "Content-Type") == 0) *out_saw_content_type = 1; + jb_puts(b, k); + jb_puts(b, ": "); + jb_puts(b, v); + jb_puts(b, "\r\n"); + } +} + +/* Parse the envelope produced by http_response(). On success returns 1 and + * populates *out_status, *out_headers_map (an ElMap el_val_t — caller must + * el_release), and *out_body (allocated). On failure returns 0. + * + * Implementation: feeds the entire envelope through the recursive-descent + * JSON parser (which builds proper ElMap/ElList values), then pulls the + * three top-level fields by name. Avoids re-stringifying the headers map + * since json_stringify() does not support nested objects. */ +static int http_parse_envelope(const char* s, int* out_status, + el_val_t* out_headers_map, char** out_body, + el_val_t* out_parsed_root) { + if (!s) return 0; + if (strncmp(s, EL_HTTP_RESPONSE_TAG, + sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; + + el_val_t parsed = json_parse(EL_STR(s)); + if (parsed == EL_NULL) return 0; + + int status = 200; + el_val_t hmap = 0; + char* body = NULL; + + el_val_t sv = el_map_get(parsed, EL_STR("status")); + if (sv != 0) { + /* status comes back as an integer — el_val_t holds it directly. */ + long sc = (long)sv; + if (sc >= 100 && sc <= 599) status = (int)sc; + } + + el_val_t hv = el_map_get(parsed, EL_STR("headers")); + if (hv != 0) { + ElMap* hm = (ElMap*)(uintptr_t)hv; + if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; + } + + el_val_t bv = el_map_get(parsed, EL_STR("body")); + if (bv != 0) { + const char* bs = EL_CSTR(bv); + if (bs) body = el_strdup(bs); + } + if (!body) body = el_strdup(""); + + *out_status = status; + *out_headers_map = hmap; + *out_body = body; + *out_parsed_root = parsed; /* caller releases to free hmap + entries */ + return 1; +} + +/* Lightweight `__status__` envelope: if the body's first key is `__status__` + * and its value is a numeric literal, lift the status to the HTTP layer and + * strip the marker from the body before sending. This is the common case for + * El handlers that want to return 4xx/5xx without going through + * http_response() — they just prepend `{"__status__":,...}` to the JSON + * they were already returning. + * + * We deliberately recognise ONLY the first-key form so the contract is cheap + * to detect and unambiguous: `{"__status__":401,"error":"unauthorized"}` is + * an envelope, but `{"error":"...","__status__":401}` is not. Product code + * controls placement. + * + * On success returns 1 with *out_status set and *out_body_alloc populated + * with a freshly malloc'd body (caller frees). On failure returns 0 and + * leaves outputs untouched. */ +static int http_parse_status_envelope(const char* s, int* out_status, + char** out_body_alloc) { + if (!s) return 0; + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + static const char marker[] = "\"__status__\""; + size_t mlen = sizeof(marker) - 1; + if (strncmp(p, marker, mlen) != 0) return 0; + p += mlen; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p < '0' || *p > '9') return 0; /* non-numeric -> not an envelope */ + int status = 0; + while (*p >= '0' && *p <= '9') { + status = status * 10 + (*p - '0'); + p++; + } + if (status < 100 || status > 599) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Two trailing shapes accepted: + * ,"k":v,...} -> body becomes {"k":v,...} + * } -> body becomes {} + * Anything else (e.g. `:` re-appearing, garbage) drops the envelope so + * we don't strip what we shouldn't. */ + if (*p == '}') { + *out_status = status; + *out_body_alloc = el_strdup("{}"); + return 1; + } + if (*p != ',') return 0; + p++; /* skip the comma; the rest of the object follows */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + /* Build the trimmed body: '{' + remainder. */ + size_t rest_len = strlen(p); + char* out = (char*)malloc(rest_len + 2); + if (!out) return 0; + out[0] = '{'; + memcpy(out + 1, p, rest_len); + out[rest_len + 1] = '\0'; + *out_status = status; + *out_body_alloc = out; + return 1; +} + +/* Send a fully-built HTTP response. If `body` starts with the envelope tag, + * unpack status/headers/body. Otherwise emit the historical 200-OK with + * auto-detected Content-Type. */ +/* Thread-local flag: if 1, http_send_response writes status + headers but + * NO body (HEAD method behaviour). Set by http_worker before calling + * http_send_response, cleared after. */ +static __thread int _tl_http_head_only = 0; + +static void http_send_response(int fd, const char* body) { + if (!body) body = ""; + + int status = 200; + el_val_t env_headers_map = 0; + char* env_body = NULL; + el_val_t env_parsed_root = 0; + int is_envelope = http_parse_envelope(body, &status, + &env_headers_map, &env_body, + &env_parsed_root); + + /* If the rich http_response() envelope didn't claim this body, try the + * lightweight `__status__` form. This second envelope is malloc-backed so + * we route it through env_body and let the existing cleanup path free it + * — same lifetime contract, no special case at the bottom of the + * function. */ + if (!is_envelope) { + char* trimmed = NULL; + if (http_parse_status_envelope(body, &status, &trimmed)) { + env_body = trimmed; + is_envelope = 1; + } + } + + const char* eff_body = is_envelope ? env_body : body; + /* Use the real byte count from fs_read if available (handles binary files + * with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for + * normal text/JSON responses where _tl_fs_read_len is 0. */ + size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body); + _tl_fs_read_len = 0; /* consume — one-shot per response */ + int head_only = _tl_http_head_only; + + JsonBuf hdrs; jb_init(&hdrs); + int saw_content_type = 0; + if (is_envelope) { + http_emit_headers_from_map(&hdrs, env_headers_map, + &saw_content_type); + } + if (!saw_content_type) { + jb_puts(&hdrs, "Content-Type: "); + jb_puts(&hdrs, http_detect_content_type(eff_body)); + jb_puts(&hdrs, "\r\n"); + } + + char status_line[64]; + int sl = snprintf(status_line, sizeof(status_line), + "HTTP/1.1 %d %s\r\n", + status, http_reason_phrase(status)); + if (sl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + char tail[128]; + int tl = snprintf(tail, sizeof(tail), + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", blen); + if (tl < 0) { + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); free(hdrs.buf); return; + } + + if (http_send_all(fd, status_line, (size_t)sl) == 0 + && http_send_all(fd, hdrs.buf, hdrs.len) == 0 + && http_send_all(fd, tail, (size_t)tl) == 0 + && (head_only + /* HEAD requests echo headers + Content-Length but no body. */ + ? 1 + : http_send_all(fd, eff_body, blen) == 0)) { + /* sent successfully */ + } + + if (env_parsed_root) el_release(env_parsed_root); + free(env_body); + free(hdrs.buf); +} + +typedef struct { + int fd; +} HttpWorkerArg; + +static void* http_worker(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL; + if (http_read_request(fd, &method, &path, &body, NULL) == 0) { + http_handler_fn h = http_lookup_active(); + char* response = NULL; + /* HEAD: dispatch as GET so existing handlers respond with the same + * body, but flag the response writer to emit headers only. RFC 9110 + * requires HEAD to mirror GET headers + Content-Length without body. */ + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), EL_STR(body)); + const char* rs = EL_CSTR(r); + /* Copy response out BEFORE arena teardown. + * For binary files, _tl_fs_read_len holds the real byte count — + * use memcpy instead of strdup so null bytes are preserved. */ + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } else { + response = el_strdup_persist("el-runtime: no http handler registered"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); + close(fd); + /* release a slot */ + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve(el_val_t port, el_val_t handler) { + /* If `handler` looks like a string name, register it as the active handler. */ + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return 0; } + /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6. + * This makes `localhost` work in browsers that resolve it to ::1 first. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* ── HTTP server v2 — request headers + structured response ──────────────── */ +/* + * v2 widens the handler signature from + * (method, path, body) -> body_string + * to + * (method, path, headers_map, body) -> body_string_or_envelope + * + * The response envelope is detected uniformly inside http_send_response — so + * 4-arg handlers can return either a plain body or http_response(...). The + * 3-arg path stays untouched in spirit (its handlers still build plain + * bodies; the envelope tag, being `{"el_http_response":1`, will never + * collide with normal JSON the legacy server.el routes return). + * + * Registry is parallel to the 3-arg handler registry: separate name table, + * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 + * handlers in the same process is fine — they don't share the active slot. */ + +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, + el_val_t headers_map, el_val_t body); + +typedef struct { + char* name; + http_handler4_fn fn; +} HttpHandler4Entry; + +static HttpHandler4Entry _http_handlers4[32]; +static size_t _http_handler4_count = 0; +static char* _http_active_handler4 = NULL; + +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn); +void el_runtime_register_handler_v2(const char* name, http_handler4_fn fn) { + if (!name || !fn) return; + pthread_mutex_lock(&_http_handler_mu); + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, name) == 0) { + _http_handlers4[i].fn = fn; + pthread_mutex_unlock(&_http_handler_mu); + return; + } + } + if (_http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(name); + _http_handlers4[_http_handler4_count].fn = fn; + _http_handler4_count++; + } + pthread_mutex_unlock(&_http_handler_mu); +} + +el_val_t http_set_handler_v2(el_val_t name) { + const char* n = EL_CSTR(name); + pthread_mutex_lock(&_http_handler_mu); + free(_http_active_handler4); + _http_active_handler4 = el_strdup(n ? n : ""); + if (n && *n) { + int found = 0; + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, n) == 0) { found = 1; break; } + } + if (!found) { + void* sym = dlsym(RTLD_DEFAULT, n); + if (sym && _http_handler4_count < + sizeof(_http_handlers4) / sizeof(_http_handlers4[0])) { + _http_handlers4[_http_handler4_count].name = el_strdup(n); + _http_handlers4[_http_handler4_count].fn = + (http_handler4_fn)sym; + _http_handler4_count++; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return 0; +} + +static http_handler4_fn http_lookup_active_v2(void) { + http_handler4_fn out = NULL; + pthread_mutex_lock(&_http_handler_mu); + if (_http_active_handler4) { + for (size_t i = 0; i < _http_handler4_count; i++) { + if (strcmp(_http_handlers4[i].name, + _http_active_handler4) == 0) { + out = _http_handlers4[i].fn; break; + } + } + } + pthread_mutex_unlock(&_http_handler_mu); + return out; +} + +/* Build an ElMap from the raw header block produced by http_read_request. + * Keys are lowercased (RFC 7230 — case-insensitive); values have leading + * whitespace trimmed. Repeated headers with the same name are joined with + * ", " in arrival order, matching standard library behaviour elsewhere. */ +static el_val_t http_build_headers_map(const char* hdr_block) { + el_val_t m = el_map_new(0); + if (!hdr_block || !*hdr_block) return m; + const char* p = hdr_block; + while (*p) { + const char* line_end = strstr(p, "\r\n"); + const char* end = line_end ? line_end : p + strlen(p); + const char* colon = NULL; + for (const char* c = p; c < end; c++) { + if (*c == ':') { colon = c; break; } + } + if (colon && colon > p) { + size_t klen = (size_t)(colon - p); + char* key = malloc(klen + 1); + if (key) { + for (size_t i = 0; i < klen; i++) { + unsigned char ch = (unsigned char)p[i]; + key[i] = (char)tolower(ch); + } + key[klen] = '\0'; + const char* vstart = colon + 1; + while (vstart < end && (*vstart == ' ' || *vstart == '\t')) vstart++; + size_t vlen = (size_t)(end - vstart); + /* Strip trailing OWS just in case. */ + while (vlen > 0 + && (vstart[vlen - 1] == ' ' + || vstart[vlen - 1] == '\t')) vlen--; + /* Coalesce repeats: if key already present, append ", value". */ + el_val_t existing = el_map_get(m, EL_STR(key)); + if (existing != 0 && looks_like_string(existing)) { + const char* old = EL_CSTR(existing); + size_t olen = strlen(old); + char* combined = malloc(olen + 2 + vlen + 1); + if (combined) { + memcpy(combined, old, olen); + memcpy(combined + olen, ", ", 2); + memcpy(combined + olen + 2, vstart, vlen); + combined[olen + 2 + vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(combined)); + } + free(key); + } else { + char* val = malloc(vlen + 1); + if (val) { + memcpy(val, vstart, vlen); + val[vlen] = '\0'; + m = el_map_set(m, EL_STR(key), EL_STR(val)); + } else { + free(key); + } + } + } + } + if (!line_end) break; + p = line_end + 2; + } + return m; +} + +static void* http_worker_v2(void* arg) { + HttpWorkerArg* a = (HttpWorkerArg*)arg; + int fd = a->fd; + free(a); + char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; + if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { + http_handler4_fn h = http_lookup_active_v2(); + char* response = NULL; + int head_only = (method && strcmp(method, "HEAD") == 0); + const char* dispatch_method = head_only ? "GET" : method; + el_request_start(); /* begin per-request arena */ + if (h) { + el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); + el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); + const char* rs = EL_CSTR(r); + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + el_release(hmap); + } else { + response = el_strdup_persist( + "el-runtime: no v2 http handler registered " + "(call http_set_handler_v2)"); + } + el_request_end(); /* free all intermediate strings */ + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } + free(method); free(path); free(body); free(hdr_block); + close(fd); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + return NULL; +} + +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { + const char* hname = EL_CSTR(handler); + if (hname && looks_like_string(handler)) { + http_set_handler_v2(handler); + } + int p = (int)port; + if (p <= 0 || p > 65535) { + fprintf(stderr, "http_serve_v2: invalid port %d\n", p); + return 0; + } + /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); + if (sock < 0) { perror("socket"); return 0; } + int yes = 1; int no = 0; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(sock); return 0; + } + if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); + while (1) { + struct sockaddr_in6 cli; + socklen_t clen = sizeof(cli); + int cfd = accept(sock, (struct sockaddr*)&cli, &clen); + if (cfd < 0) { + if (errno == EINTR) continue; + perror("accept"); break; + } + pthread_mutex_lock(&_http_conn_mu); + while (_http_conn_active >= HTTP_MAX_CONNS) { + pthread_cond_wait(&_http_conn_cv, &_http_conn_mu); + } + _http_conn_active++; + pthread_mutex_unlock(&_http_conn_mu); + HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); + if (!arg) { close(cfd); continue; } + arg->fd = cfd; + pthread_t tid; + if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { + close(cfd); free(arg); + pthread_mutex_lock(&_http_conn_mu); + _http_conn_active--; + pthread_cond_signal(&_http_conn_cv); + pthread_mutex_unlock(&_http_conn_mu); + continue; + } + pthread_detach(tid); + } + close(sock); + return 0; +} + +/* Build the response envelope a 4-arg handler can return. We hand-write + * the JSON so the discriminator key always lands first — the runtime's + * http_parse_envelope() detects it via prefix match. headers_json must be + * either "" (empty), "{}" (empty object), or a well-formed JSON object + * literal; anything else will produce a malformed envelope and the runtime + * will treat the whole string as a plain body (no envelope detected). */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + long sc = (long)status; + if (sc < 100 || sc > 599) sc = 200; + const char* hj = EL_CSTR(headers_json); + if (!hj || !*hj) hj = "{}"; + /* Light validation: must start with '{' and end with '}'. */ + size_t hlen = strlen(hj); + int hj_ok = (hlen >= 2 && hj[0] == '{' && hj[hlen - 1] == '}'); + if (!hj_ok) hj = "{}"; + const char* b = EL_CSTR(body); + if (!b) b = ""; + + JsonBuf out; jb_init(&out); + jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ + jb_puts(&out, ",\"status\":"); + char num[32]; + snprintf(num, sizeof(num), "%ld", sc); + jb_puts(&out, num); + jb_puts(&out, ",\"headers\":"); + jb_puts(&out, hj); + jb_puts(&out, ",\"body\":"); + jb_emit_escaped(&out, b); + jb_putc(&out, '}'); + return el_wrap_str(out.buf); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + _tl_fs_read_len = 0; + if (!path) return el_wrap_str(el_strdup("")); + FILE* f = fopen(path, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return el_wrap_str(el_strdup("")); } /* pipe/special file */ + char* buf = el_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + _tl_fs_read_len = got; /* store real byte count for binary-safe send */ + fclose(f); + return el_wrap_str(buf); +} + +el_val_t fs_write(el_val_t pathv, el_val_t contentv) { + const char* path = EL_CSTR(pathv); + const char* content = EL_CSTR(contentv); + if (!path || !content) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t n = strlen(content); + size_t written = fwrite(content, 1, n, f); + fclose(f); + return written == n ? 1 : 0; +} + +/* fs_write_bytes — explicit-length binary write. Bypasses strlen so embedded + * NULs survive. Caller must know the byte count (e.g. from base64_decode, + * or the fixed 32-byte sha256_bytes/hmac_sha256_bytes outputs). + * + * If `length` is negative, treats as failure. If `length` is 0, creates an + * empty file (still useful as a "touch with content" primitive). */ +el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { + const char* path = EL_CSTR(pathv); + const char* bytes = EL_CSTR(bytesv); + int64_t n = (int64_t)lengthv; + if (!path || !bytes) return 0; + if (n < 0) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + size_t written = (n > 0) ? fwrite(bytes, 1, (size_t)n, f) : 0; + int flush_ok = (fflush(f) == 0); + int close_ok = (fclose(f) == 0); + if (!flush_ok || !close_ok || written != (size_t)n) { + remove(path); + return 0; + } + return 1; +} + +// stdout_to_file / stdout_restore — redirect process stdout to a file and +// restore it. Used by the compiler's JS post-processing pipeline to capture +// codegen output before piping through terser / obfuscator. +#include +static int _el_saved_stdout_fd = -1; + +el_val_t stdout_to_file(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return (el_val_t)(int64_t)-1; + fflush(stdout); + _el_saved_stdout_fd = dup(STDOUT_FILENO); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return (el_val_t)(int64_t)-1; + dup2(fd, STDOUT_FILENO); + close(fd); + return (el_val_t)(int64_t)0; +} + +el_val_t stdout_restore(void) { + if (_el_saved_stdout_fd >= 0) { + fflush(stdout); + dup2(_el_saved_stdout_fd, STDOUT_FILENO); + close(_el_saved_stdout_fd); + _el_saved_stdout_fd = -1; + } + return (el_val_t)(int64_t)0; +} + +// exec_command — run a shell command, return exit code (0 = success). +// Used by elb and other El tooling to invoke subprocesses. +el_val_t exec_command(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return (el_val_t)(int64_t)-1; + int ret = system(cmd); + return (el_val_t)(int64_t)ret; +} + +// exec_capture — run a shell command, capture stdout, return as String. +// Returns "" on failure. +el_val_t exec_capture(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd) return el_wrap_str(el_strdup("")); + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); + pclose(f); + return el_wrap_str(b.buf); +} + +// exec — run a shell command via /bin/sh, capture stdout, return as String. +// Times out after 30 seconds. Returns "" on any error. +// El name: exec(cmd) -> String +el_val_t exec(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + /* Build a time-limited command: wrap with timeout(1) if available, + * otherwise rely on the 30s read loop guard below. We use the simple + * popen approach with a deadline measured by wall clock so the caller + * is never blocked indefinitely. */ + FILE* f = popen(cmd, "r"); + if (!f) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + char buf[4096]; + /* 30-second wall-clock deadline */ + time_t deadline = time(NULL) + 30; + while (time(NULL) < deadline) { + if (fgets(buf, sizeof(buf), f) == NULL) break; + jb_puts(&b, buf); + } + pclose(f); + return el_wrap_str(b.buf); +} + +// exec_bg — run a shell command in background, return PID as String. +// The child process runs independently; the caller is not blocked. +// Returns "" on fork failure. +// El name: exec_bg(cmd) -> String +el_val_t exec_bg(el_val_t cmdv) { + const char* cmd = EL_CSTR(cmdv); + if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); + pid_t pid = fork(); + if (pid < 0) { + /* fork failed */ + return el_wrap_str(el_strdup("")); + } + if (pid == 0) { + /* child: detach from parent's stdio, exec via shell */ + setsid(); + int devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + execl("/bin/sh", "sh", "-c", cmd, (char*)NULL); + _exit(127); + } + /* parent: convert pid to string and return immediately */ + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); + return el_wrap_str(el_strdup(pidbuf)); +} + +el_val_t fs_list(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + el_val_t lst = el_list_empty(); + if (!path) return lst; + DIR* d = opendir(path); + if (!d) return lst; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + lst = el_list_append(lst, el_wrap_str(el_strdup(e->d_name))); + } + closedir(d); + return lst; +} + +/* fs_list_json — return directory entries as a JSON array of strings. + * Returns "[]" for missing or non-directory paths. Excludes "." and "..". */ +el_val_t fs_list_json(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return EL_STR("[]"); + DIR* d = opendir(path); + if (!d) return EL_STR("[]"); + /* Collect entries first so we can build the JSON in one pass. */ + char** names = NULL; + size_t count = 0, cap = 0; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + if (count >= cap) { + cap = cap ? cap * 2 : 16; + names = realloc(names, cap * sizeof(char*)); + if (!names) { closedir(d); return EL_STR("[]"); } + } + names[count++] = strdup(e->d_name); + } + closedir(d); + /* Build JSON array. */ + size_t sz = 3; /* "[]" + NUL */ + for (size_t i = 0; i < count; i++) sz += strlen(names[i]) * 2 + 6; /* conservative */ + char* buf = malloc(sz); + if (!buf) { for (size_t i = 0; i < count; i++) free(names[i]); free(names); return EL_STR("[]"); } + size_t pos = 0; + buf[pos++] = '['; + for (size_t i = 0; i < count; i++) { + if (i > 0) buf[pos++] = ','; + buf[pos++] = '"'; + for (const char* p = names[i]; *p; p++) { + if (*p == '"' || *p == '\\') buf[pos++] = '\\'; + else if (*p == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; continue; } + else if (*p == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; continue; } + buf[pos++] = *p; + } + buf[pos++] = '"'; + free(names[i]); + } + free(names); + buf[pos++] = ']'; + buf[pos] = '\0'; + return el_wrap_str(buf); +} + +/* fs_exists — true iff stat(path) succeeds. Symlinks are followed. */ +el_val_t fs_exists(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + struct stat st; + return (el_val_t)(stat(path, &st) == 0 ? 1 : 0); +} + +/* fs_mkdir — create directory at path with mode 0755, mkdir -p semantics. + * Returns 1 if path exists or was created (incl. all parents); 0 on failure. + * Walks the path component-by-component so missing intermediate dirs are + * also created. An existing leaf is not an error. */ +el_val_t fs_mkdir(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path || !*path) return 0; + size_t n = strlen(path); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, path, n + 1); + /* Walk components; create each prefix in turn. */ + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + /* Tolerate the case where this prefix exists as a non-dir + * only when stat says it's a directory. */ + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); + return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +/* ── URL encoding ─────────────────────────────────────────────────────────── */ + +/* RFC 3986 percent-encoding for URL components (form bodies, query strings). + * Unreserved set: A-Z a-z 0-9 - _ . ~ — passed through verbatim. + * Everything else (including space) becomes %XX hex. */ +el_val_t url_encode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + static const char hex[] = "0123456789ABCDEF"; + size_t n = strlen(s); + char* out = el_strbuf(n * 3); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } else { + out[o++] = '%'; + out[o++] = hex[(c >> 4) & 0xF]; + out[o++] = hex[c & 0xF]; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* Decode percent-encoded URL component. '+' becomes space (form-encoded); + * malformed %-escapes are emitted verbatim. */ +el_val_t url_decode(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + size_t o = 0; + for (size_t i = 0; i < n; i++) { + char c = s[i]; + if (c == '+') { + out[o++] = ' '; + } else if (c == '%' && i + 2 < n) { + char h1 = s[i + 1], h2 = s[i + 2]; + int v1 = (h1 >= '0' && h1 <= '9') ? h1 - '0' + : (h1 >= 'a' && h1 <= 'f') ? h1 - 'a' + 10 + : (h1 >= 'A' && h1 <= 'F') ? h1 - 'A' + 10 : -1; + int v2 = (h2 >= '0' && h2 <= '9') ? h2 - '0' + : (h2 >= 'a' && h2 <= 'f') ? h2 - 'a' + 10 + : (h2 >= 'A' && h2 <= 'F') ? h2 - 'A' + 10 : -1; + if (v1 >= 0 && v2 >= 0) { + out[o++] = (char)((v1 << 4) | v2); + i += 2; + } else { + out[o++] = c; + } + } else { + out[o++] = c; + } + } + out[o] = '\0'; + return el_wrap_str(out); +} + +/* ── html_raw ──────────────────────────────────────────────────────────────── + * Identity passthrough for raw HTML template interpolation. + * El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is + * without any escaping. The caller is responsible for safety. + */ +el_val_t html_raw(el_val_t s) { + return s; +} + +/* ── html_escape ───────────────────────────────────────────────────────────── + * Escape < > " ' & for safe HTML text interpolation. + * El's {expr} in HTML templates compiles to html_escape(expr). + */ +el_val_t html_escape(el_val_t sv) { + const char* src = EL_CSTR(sv); + if (!src) return EL_STR(""); + size_t len = strlen(src); + /* Worst case: every byte → 6 chars (") */ + char* out = (char*)malloc(len * 6 + 1); + if (!out) return sv; + el_arena_track(out); + char* p = out; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + switch (c) { + case '&': memcpy(p, "&", 5); p += 5; break; + case '<': memcpy(p, "<", 4); p += 4; break; + case '>': memcpy(p, ">", 4); p += 4; break; + case '"': memcpy(p, """, 6); p += 6; break; + case '\'': memcpy(p, "'", 5); p += 5; break; + default: *p++ = (char)c; break; + } + } + *p = '\0'; + return el_wrap_str(out); +} + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input, allowlist_json) + * + * Strict allowlist HTML cleaner. Replaces the older denylist patterns + * (str_replace cascades that wrapped dangerous tags in HTML comments and + * renamed `on*` attributes). The denylist approach is fragile: comment- + * wrapping can be re-broken by a literal `-->` inside an attacker-supplied + * attribute value, and every new attack vector requires a code change. + * + * Design: + * - Single-pass byte-level state machine. + * - Tag and attribute names are matched case-insensitively against the + * allowlist. Unknown tags are dropped entirely (the open and close + * markers are stripped; their inner text content survives, escaped). + * - A small set of "dangerous container" tags (script, style, iframe, + * object, embed, form, plus a few rarer ones) drop themselves AND + * their full subtree — text between `` is + * CDATA-like and must not be re-emitted as escaped text either. + * - Comments (), doctype (), CDATA (), + * and processing instructions () are dropped entirely. + * - Text content outside dropped subtrees is HTML-escaped (&, <, >, ", '). + * - Attribute values are unquoted/dequoted, then re-emitted with double + * quotes around the cleanly-escaped value. + * - For `` and any `src` attribute, the URL scheme is validated: + * only http:, https:, mailto:, fragment-only `#anchor`, or relative + * paths are allowed. Anything else (javascript:, data:, vbscript:, + * about:, file:, etc.) drops the attribute. + * - Self-closing void tags (br, hr, img, etc.) emit without a close tag. + * - Malformed input (unclosed tag at EOF, bad attribute syntax) drops + * the pending tag and continues. Pre-encoded entities (<, &, + * etc.) are passed through verbatim — the browser will decode them + * safely on render. + * + * Allowlist format (JSON string): + * {"p":[],"a":["href","title"],"strong":[],...} + * - Key = lowercase tag name. + * - Value = JSON array of allowed attribute names (lowercase). + * - Empty array means tag allowed but no attributes survive. + * + * Output is a freshly-allocated arena-tracked el_val_t string. */ + +/* Internal byte buffer with realloc-doubling. Used during sanitization; + * the final result is copied into an arena-tracked el_strbuf so the caller + * sees standard runtime memory semantics. */ +typedef struct { + char* data; + size_t len; + size_t cap; +} html_buf_t; + +static void html_buf_init(html_buf_t* b) { + b->cap = 256; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->len = 0; +} + +static void html_buf_grow(html_buf_t* b, size_t need) { + if (b->len + need + 1 <= b->cap) return; + size_t nc = b->cap; + while (b->len + need + 1 > nc) nc *= 2; + char* nd = realloc(b->data, nc); + if (!nd) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->data = nd; + b->cap = nc; +} + +static void html_buf_putc(html_buf_t* b, char c) { + html_buf_grow(b, 1); + b->data[b->len++] = c; +} + +static void html_buf_puts(html_buf_t* b, const char* s) { + if (!s) return; + size_t n = strlen(s); + html_buf_grow(b, n); + memcpy(b->data + b->len, s, n); + b->len += n; +} + +static void html_buf_free(html_buf_t* b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + +/* ASCII tolower, locale-independent. */ +static int html_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +/* Case-insensitive ASCII compare of [a, a+n) against c-string `s`. + * Returns 1 iff lengths match and bytes are equal under tolower. */ +static int html_ieq_n(const char* a, size_t n, const char* s) { + if (!a || !s) return 0; + if (strlen(s) != n) return 0; + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)s[i])) return 0; + } + return 1; +} + +/* Case-insensitive ASCII compare of two byte slices. */ +static int html_iemem(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (html_tolower((unsigned char)a[i]) != html_tolower((unsigned char)b[i])) return 0; + } + return 1; +} + +/* Walk a JSON allowlist object and find the value (an array) for a given + * tag key, comparing case-insensitively. On hit returns a pointer to the + * opening `[` of the array and writes the byte length of the array span + * (including the brackets) to *out_len. On miss returns NULL. + * + * The parser is intentionally tiny: it does not handle escapes inside + * keys (allowlist authors do not need them), and it relies on balanced + * brackets/quotes within the value array. */ +static const char* html_allowlist_find(const char* allow, const char* tag, + size_t tag_len, size_t* out_len) { + if (!allow) return NULL; + const char* p = allow; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return NULL; + p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == 0) return NULL; + if (*p != '"') return NULL; + p++; + const char* k = p; + while (*p && *p != '"') p++; + if (*p != '"') return NULL; + size_t klen = (size_t)(p - k); + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return NULL; + const char* arr_start = p; + int depth = 0; + int in_str = 0; + while (*p) { + char c = *p; + if (in_str) { + if (c == '\\' && p[1]) { p += 2; continue; } + if (c == '"') in_str = 0; + } else { + if (c == '"') in_str = 1; + else if (c == '[') depth++; + else if (c == ']') { depth--; if (depth == 0) { p++; break; } } + } + p++; + } + size_t alen = (size_t)(p - arr_start); + int match = (klen == tag_len) && html_iemem(k, tag, klen); + if (match) { + if (out_len) *out_len = alen; + return arr_start; + } + } + return NULL; +} + +/* Returns 1 iff `attr` (length attr_len) appears as a string element + * in the JSON array slice [arr, arr+arr_len). Comparison is case- + * insensitive. */ +static int html_attr_in_array(const char* arr, size_t arr_len, + const char* attr, size_t attr_len) { + if (!arr || arr_len < 2) return 0; + const char* p = arr + 1; + const char* end = arr + arr_len - 1; + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) return 0; + if (*p != '"') return 0; + p++; + const char* s = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) return 0; + size_t slen = (size_t)(p - s); + p++; + if (slen == attr_len && html_iemem(s, attr, slen)) return 1; + } + return 0; +} + +/* Hard-coded set of tags whose content is ALSO dropped (entire subtree). */ +static int html_is_dangerous_container(const char* tag, size_t tag_len) { + static const char* names[] = { + "script", "style", "iframe", "object", "embed", "form", + "noscript", "noembed", "template", "svg", "math", "frame", + "frameset", "applet", "audio", "video", "source", "track", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* HTML void elements — emit without a close tag. */ +static int html_is_void(const char* tag, size_t tag_len) { + static const char* names[] = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + NULL + }; + for (int i = 0; names[i]; i++) { + if (html_ieq_n(tag, tag_len, names[i])) return 1; + } + return 0; +} + +/* Append a single byte HTML-escaped into the output buffer. */ +static void html_escape_byte(html_buf_t* out, unsigned char c) { + switch (c) { + case '<': html_buf_puts(out, "<"); break; + case '>': html_buf_puts(out, ">"); break; + case '"': html_buf_puts(out, """); break; + case '\'': html_buf_puts(out, "'"); break; + default: html_buf_putc(out, (char)c); break; + } +} + +/* Validate a URL value against the allowlist of safe schemes for hrefs. + * Returns 1 iff the URL is safe to emit. Acceptable forms: + * - http:// or https:// (case-insensitive) + * - mailto: + * - fragment-only `#anchor` + * - relative path that does not contain a colon before the first + * slash/?/# (so `foo/bar`, `/foo`, `?x=1` are OK; `javascript:x` is + * not — its colon precedes any path/hash/query separator). + * + * URL leading whitespace and embedded ASCII control bytes (TAB, LF, CR) + * are stripped before the scheme test, mirroring how browsers normalise + * URLs (these bytes are otherwise a known XSS bypass: `java\tscript:`). */ +static int html_url_is_safe(const char* url, size_t len) { + if (!url || len == 0) return 1; /* empty href is harmless */ + size_t i = 0; + while (i < len) { + unsigned char c = (unsigned char)url[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) { + i++; continue; + } + break; + } + if (i >= len) return 1; /* whitespace only */ + if (url[i] == '#') return 1; /* fragment only */ + if (url[i] == '/' || url[i] == '?') return 1; /* relative */ + /* Find the first scheme-terminating character. */ + size_t scheme_end = (size_t)-1; + for (size_t j = i; j < len; j++) { + char c = url[j]; + if (c == ':') { scheme_end = j; break; } + if (c == '/' || c == '?' || c == '#') break; + } + if (scheme_end == (size_t)-1) return 1; /* no colon → relative path */ + /* Lowercase the scheme, stripping embedded control bytes. */ + char scheme[32]; + size_t sl = 0; + for (size_t j = i; j < scheme_end && sl < sizeof(scheme) - 1; j++) { + unsigned char c = (unsigned char)url[j]; + if (c == '\t' || c == '\n' || c == '\r' || c == 0x0B || c == 0x0C) continue; + scheme[sl++] = (char)html_tolower(c); + } + scheme[sl] = '\0'; + if (strcmp(scheme, "http") == 0) return 1; + if (strcmp(scheme, "https") == 0) return 1; + if (strcmp(scheme, "mailto") == 0) return 1; + return 0; +} + +el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) { + const char* input = EL_CSTR(input_v); + const char* allow = EL_CSTR(allowlist_v); + if (!input) return el_wrap_str(el_strdup("")); + if (!allow) allow = "{}"; + size_t in_len = strlen(input); + + html_buf_t out; + html_buf_init(&out); + + size_t i = 0; + while (i < in_len) { + unsigned char c = (unsigned char)input[i]; + if (c != '<') { + /* Plain text — escape and emit. We pass `&` through verbatim + * to preserve pre-encoded entities (`<`, `&`, `&#x...;`) + * which the browser will decode safely. */ + if (c == '&') html_buf_putc(&out, '&'); + else html_escape_byte(&out, c); + i++; + continue; + } + /* `<` — try to parse a tag. */ + if (i + 1 >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + /* Comments, doctype, CDATA, processing instructions — drop entirely. */ + if (input[i + 1] == '!') { + if (i + 3 < in_len && input[i + 2] == '-' && input[i + 3] == '-') { + size_t j = i + 4; + while (j + 2 < in_len && !(input[j] == '-' && input[j + 1] == '-' && input[j + 2] == '>')) j++; + if (j + 2 < in_len) i = j + 3; + else i = in_len; + continue; + } + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + if (input[i + 1] == '?') { + size_t j = i + 2; + while (j < in_len && input[j] != '>') j++; + i = (j < in_len) ? j + 1 : in_len; + continue; + } + int is_close = 0; + size_t name_start = i + 1; + if (input[i + 1] == '/') { + is_close = 1; + name_start = i + 2; + } + if (name_start >= in_len) { + html_buf_puts(&out, "<"); + i++; + continue; + } + unsigned char nc = (unsigned char)input[name_start]; + if (!((nc >= 'a' && nc <= 'z') || (nc >= 'A' && nc <= 'Z'))) { + /* `<` followed by non-letter — emit as escaped text. */ + html_buf_puts(&out, "<"); + i++; + continue; + } + size_t name_end = name_start; + while (name_end < in_len) { + unsigned char x = (unsigned char)input[name_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + name_end++; + } else { + break; + } + } + const char* tag = input + name_start; + size_t tag_len = name_end - name_start; + /* Find the `>` that closes this tag, respecting quoted attrs. */ + size_t cur = name_end; + int self_close = 0; + while (cur < in_len) { + unsigned char x = (unsigned char)input[cur]; + if (x == '"' || x == '\'') { + unsigned char q = x; + cur++; + while (cur < in_len && (unsigned char)input[cur] != q) cur++; + if (cur < in_len) cur++; /* skip closing quote */ + continue; + } + if (x == '/' && cur + 1 < in_len && input[cur + 1] == '>') { + self_close = 1; + break; + } + if (x == '>') break; + cur++; + } + if (cur >= in_len) { + /* Malformed: unclosed tag at EOF. Drop the rest of the input. */ + i = in_len; + continue; + } + size_t tag_end = self_close ? cur + 2 : cur + 1; /* one past `>` */ + /* Dangerous container — drop the whole subtree. */ + if (!is_close && html_is_dangerous_container(tag, tag_len)) { + if (self_close || html_is_void(tag, tag_len)) { + i = tag_end; + continue; + } + size_t scan = tag_end; + int found_close = 0; + while (scan < in_len) { + if (input[scan] != '<') { scan++; continue; } + if (scan + 1 < in_len && input[scan + 1] == '/') { + size_t cn_start = scan + 2; + size_t cn_end = cn_start; + while (cn_end < in_len) { + unsigned char x = (unsigned char)input[cn_end]; + if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || + (x >= '0' && x <= '9') || x == '-' || x == '_' || x == ':') { + cn_end++; + } else break; + } + if (cn_end - cn_start == tag_len && + html_iemem(input + cn_start, tag, tag_len)) { + size_t end_close = cn_end; + while (end_close < in_len && input[end_close] != '>') end_close++; + i = (end_close < in_len) ? end_close + 1 : in_len; + found_close = 1; + break; + } + } + scan++; + } + if (!found_close) { + /* No matching close — drop everything from here on. */ + i = in_len; + } + continue; + } + /* Look up the tag in the allowlist. */ + size_t arr_len = 0; + const char* arr = html_allowlist_find(allow, tag, tag_len, &arr_len); + if (!arr) { + /* Tag not allowed. Drop the open/close marker; inner text is + * processed by the outer loop and re-emitted as escaped text. */ + i = tag_end; + continue; + } + if (is_close) { + if (!html_is_void(tag, tag_len)) { + html_buf_putc(&out, '<'); + html_buf_putc(&out, '/'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + html_buf_putc(&out, '>'); + } + i = tag_end; + continue; + } + /* Allowed open tag. Emit ``. */ + html_buf_putc(&out, '<'); + for (size_t k = 0; k < tag_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)tag[k])); + } + size_t a = name_end; + while (a < cur) { + unsigned char x = (unsigned char)input[a]; + if (x == ' ' || x == '\t' || x == '\n' || x == '\r' || x == '/') { a++; continue; } + size_t an_start = a; + while (a < cur) { + unsigned char y = (unsigned char)input[a]; + if (y == '=' || y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '/' || y == '>') break; + a++; + } + size_t an_len = a - an_start; + if (an_len == 0) { a++; continue; } + size_t av_start = 0; + size_t av_len = 0; + int has_value = 0; + size_t b = a; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && input[b] == '=') { + has_value = 1; + b++; + while (b < cur && (input[b] == ' ' || input[b] == '\t' || input[b] == '\n' || input[b] == '\r')) b++; + if (b < cur && (input[b] == '"' || input[b] == '\'')) { + unsigned char q = (unsigned char)input[b]; + b++; + av_start = b; + while (b < cur && (unsigned char)input[b] != q) b++; + av_len = b - av_start; + if (b < cur) b++; + } else { + av_start = b; + while (b < cur) { + unsigned char y = (unsigned char)input[b]; + if (y == ' ' || y == '\t' || y == '\n' || y == '\r' || y == '>') break; + b++; + } + av_len = b - av_start; + } + a = b; + } + if (!html_attr_in_array(arr, arr_len, input + an_start, an_len)) continue; + int is_href = (an_len == 4 && html_iemem(input + an_start, "href", 4)); + int is_src = (an_len == 3 && html_iemem(input + an_start, "src", 3)); + if ((is_href || is_src) && has_value) { + if (!html_url_is_safe(input + av_start, av_len)) continue; + } + html_buf_putc(&out, ' '); + for (size_t k = 0; k < an_len; k++) { + html_buf_putc(&out, (char)html_tolower((unsigned char)input[an_start + k])); + } + if (has_value) { + html_buf_puts(&out, "=\""); + for (size_t k = 0; k < av_len; k++) { + unsigned char y = (unsigned char)input[av_start + k]; + /* Re-escape so the emitted attribute is well-formed + * double-quoted HTML. `&` passes through to preserve + * pre-encoded entities. */ + if (y == '"') html_buf_puts(&out, """); + else if (y == '<') html_buf_puts(&out, "<"); + else if (y == '>') html_buf_puts(&out, ">"); + else html_buf_putc(&out, (char)y); + } + html_buf_putc(&out, '"'); + } + } + html_buf_putc(&out, '>'); + i = tag_end; + } + /* Copy into arena-tracked buffer so the standard runtime memory model + * applies to the returned string. */ + char* result = el_strbuf(out.len); + memcpy(result, out.data, out.len); + result[out.len] = '\0'; + html_buf_free(&out); + return el_wrap_str(result); +} + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +/* True iff the segment is non-empty and every byte is an ASCII digit. We treat + * such segments as numeric array indices when walking a dot-path; mixed names + * like "0a" remain object-key lookups, so a key named "0" still wins over an + * index when the surrounding container is an object. */ +static int json_path_seg_is_index(const char* seg, size_t n) { + if (n == 0) return 0; + for (size_t i = 0; i < n; i++) { + if (seg[i] < '0' || seg[i] > '9') return 0; + } + return 1; +} + +/* Skip JSON whitespace. */ +static const char* json_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +/* Descend one segment into the JSON cursor `p`. + * - If `p` points at an array `[...]` and the segment is all digits, + * advance to that element (zero-based). + * - Otherwise treat the segment as an object key and use json_find_key + * scoped to a one-level slice of the current container. + * Returns NULL if the descent fails (segment not found, container mismatch). + * + * `seg` is a pointer into the original path string and `seg_len` is its + * byte length — this avoids an extra alloc per segment. */ +static const char* json_path_descend(const char* p, const char* seg, size_t seg_len) { + if (!p || !seg) return NULL; + p = json_skip_ws(p); + if (*p == '[' && json_path_seg_is_index(seg, seg_len)) { + long idx = 0; + for (size_t i = 0; i < seg_len; i++) idx = idx * 10 + (seg[i] - '0'); + p++; /* step past '[' */ + p = json_skip_ws(p); + long cur = 0; + while (*p && *p != ']') { + if (cur == idx) return p; + const char* end = json_skip_value(p); + if (!end || end == p) return NULL; + p = json_skip_ws(end); + if (*p == ',') { p++; p = json_skip_ws(p); cur++; continue; } + /* No comma after this element — only acceptable at the closing ']', + * which means we ran out of elements. */ + break; + } + return NULL; + } + /* Object lookup. json_find_key walks at depth 1 of whatever container it + * receives, so we slice from `p` onwards. Caller already positioned us at + * the opening '{' (or at whitespace before it). */ + if (*p != '{') return NULL; + /* Build a NUL-terminated copy of the key segment for the lookup. We only + * pay this cost when the segment isn't a numeric index. */ + char stack_key[256]; + char* k = stack_key; + if (seg_len + 1 > sizeof(stack_key)) { + k = malloc(seg_len + 1); + if (!k) return NULL; + } + memcpy(k, seg, seg_len); + k[seg_len] = '\0'; + const char* found = json_find_key(p, k); + if (k != stack_key) free(k); + return found; +} + +/* Read the JSON value at `p` into a freshly-allocated, arena-owned el_val_t. + * - String -> unescaped, wrapped el_val_t string + * - Anything else -> raw JSON slice as a string (matches the historical + * json_get behaviour: numbers/bools/null come back stringified). */ +static el_val_t json_read_value(const char* p) { + p = json_skip_ws(p); + if (*p == '"') { + p++; + size_t cap = strlen(p) + 1; + char* out = el_strbuf(cap); + char* w = out; + while (*p && *p != '"') { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 'r': *w++ = '\r'; break; + case 't': *w++ = '\t'; break; + default: *w++ = *p; break; + } + } else { + *w++ = *p; + } + p++; + } + *w = '\0'; + return el_wrap_str(out); + } + /* Object/array/number/bool/null — return the raw slice up to the value's + * end. json_skip_value tracks brace/bracket/string state so nested objects + * round-trip cleanly. */ + const char* end = json_skip_value(p); + if (!end) end = p; + size_t n = (size_t)(end - p); + /* Strip trailing whitespace from scalar values so callers don't see + * `123 ` when they parsed a pretty-printed number. */ + while (n > 0 && (p[n-1] == ' ' || p[n-1] == '\t' || p[n-1] == '\n' || p[n-1] == '\r')) { + n--; + } + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t jsonv, el_val_t keyv) { + const char* json = EL_CSTR(jsonv); + const char* key = EL_CSTR(keyv); + if (!json || !key) return el_wrap_str(el_strdup("")); + + /* Fast path: key contains no '.' — keep the historical single-segment + * substring search so existing callers retain their O(strlen) cost + * profile. The dot-path walker is only paid for when needed. */ + if (!strchr(key, '.')) { + size_t klen = strlen(key); + char stack_pat[512]; + char* pattern; + if (klen + 5 <= sizeof(stack_pat)) { + pattern = stack_pat; + } else { + pattern = malloc(klen + 5); + if (!pattern) return el_wrap_str(el_strdup("")); + } + snprintf(pattern, klen + 5, "\"%s\":", key); + const char* p = strstr(json, pattern); + if (pattern != stack_pat) free(pattern); + if (!p) return el_wrap_str(el_strdup("")); + p += strlen(key) + 3; /* skip "key": */ + return json_read_value(p); + } + + /* Dot-path traversal. Walk segments left to right; at each step, descend + * into the current container by either array index (all-digit segment on + * an array cursor) or object key. */ + const char* cursor = json_skip_ws(json); + const char* seg_start = key; + const char* k = key; + while (1) { + if (*k == '.' || *k == '\0') { + size_t seg_len = (size_t)(k - seg_start); + cursor = json_path_descend(cursor, seg_start, seg_len); + if (!cursor) return el_wrap_str(el_strdup("")); + if (*k == '\0') break; + k++; + seg_start = k; + continue; + } + k++; + } + return json_read_value(cursor); +} + +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ +/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static + * inlines so generated programs (which #include the header) can call them + * for Float literals. No definitions are needed here. */ + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +/* JsonParser struct is forward-declared near the HTTP/Engram section. */ + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +/* JsonBuf struct is forward-declared near the HTTP section so HTTP helpers + * can use it. Its definition appears there. */ + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); break; + case '\n': jb_puts(b, "\\n"); break; + case '\r': jb_puts(b, "\\r"); break; + case '\t': jb_puts(b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; + snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(b, tmp); + } else { + jb_putc(b, (char)c); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates. + * Threshold: 4 GiB (0x100000000). On 64-bit systems heap addresses from + * malloc/mmap start well above 4 GiB (ASLR pushes them to ~0x7f...). + * El integer values (counters, unix timestamps up to ~2106) all fit below + * 0x100000000 (4294967296). The old threshold of 1,000,000 caused unix + * timestamps (~1.7e9) to be misidentified as string pointers — a segfault + * risk in json_stringify and jb_emit_value. */ + uintptr_t p = (uintptr_t)v; + if (p < 0x100000000ULL) return 0; /* integers, timestamps, counters */ + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + /* Clear fs_read binary-length hint — result is a fresh null-terminated + * string, not the raw file bytes, so Content-Length must use strlen. */ + _tl_fs_read_len = 0; + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + /* raw_val is the JSON value as-is (already encoded by the caller). + * If it looks like a plain (non-JSON) string, wrap it as a JSON string. + * Convention: callers pass pre-encoded values like "\"bob\"" for strings, + * "42" for numbers, "true"/"false" for booleans. */ + const char* raw_val = EL_CSTR(value); + if (!k) k = ""; + if (!raw_val) raw_val = "null"; + if (!json || !*json) { + /* Build a fresh object */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_puts(&b, raw_val); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_puts(&b, raw_val); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* json_array_get — return the i-th element of a JSON array as a JSON + * fragment string. Nested objects and arrays are returned verbatim + * (json_skip_value tracks brace/bracket depth so nested structures are + * preserved intact). Out-of-range index → "". */ +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + int64_t idx = (int64_t)index; + if (!s || idx < 0) return el_wrap_str(el_strdup("")); + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return el_wrap_str(el_strdup("")); + int64_t i = 0; + while (*s) { + const char* start = s; + const char* end = json_skip_value(s); + if (end == s) break; + if (i == idx) { + size_t n = (size_t)(end - start); + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); + } + i++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return el_wrap_str(el_strdup("")); +} + +/* json_array_get_string — same as json_array_get, but assume the element + * is a JSON string and return the unquoted/unescaped value. Non-string + * elements yield "". */ +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { + .p = s, + .end = s + strlen(s), + .err = 0, + }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { + free(parsed); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(parsed); +} + +/* json_escape_string — escape a string value for embedding in JSON. + * Returns the escaped content WITHOUT surrounding quotes. + * "say \"hello\"" -> "say \\\"hello\\\"" */ +el_val_t json_escape_string(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + /* Worst case: every char needs a 2-char escape. */ + char* out = malloc(n * 2 + 1); + if (!out) return el_wrap_str(el_strdup("")); + size_t j = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '"') { out[j++] = '\\'; out[j++] = '"'; } + else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; } + else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; } + else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; } + else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; } + else { out[j++] = (char)c; } + } + out[j] = '\0'; + el_val_t result = el_wrap_str(el_strdup(out)); + free(out); + return result; +} + +/* json_build_object — build a JSON object from a flat key-value list. + * kvs is [key0, val0, key1, val1, ...]. Values are raw JSON (pass + * strings as "\"value\"" or use json_escape_string). */ +el_val_t json_build_object(el_val_t kvs) { + el_val_t list = kvs; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + int first = 1; + for (int64_t i = 0; i + 1 < n; i += 2) { + el_val_t k = el_list_get(list, (el_val_t)i); + el_val_t v = el_list_get(list, (el_val_t)(i + 1)); + const char* ks = EL_CSTR(k); + const char* vs = EL_CSTR(v); + if (!ks || !vs) continue; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '"'); + jb_puts(&b, ks); + jb_puts(&b, "\":\""); + /* escape the value string */ + size_t vn = strlen(vs); + for (size_t j = 0; j < vn; j++) { + unsigned char c = (unsigned char)vs[j]; + if (c == '"') { jb_putc(&b, '\\'); jb_putc(&b, '"'); } + else if (c == '\\') { jb_putc(&b, '\\'); jb_putc(&b, '\\'); } + else if (c == '\n') { jb_putc(&b, '\\'); jb_putc(&b, 'n'); } + else if (c == '\r') { jb_putc(&b, '\\'); jb_putc(&b, 'r'); } + else if (c == '\t') { jb_putc(&b, '\\'); jb_putc(&b, 't'); } + else { jb_putc(&b, (char)c); } + } + jb_putc(&b, '"'); + } + jb_putc(&b, '}'); + return el_wrap_str(b.buf); +} + +/* json_build_array — build a JSON array from a list of raw JSON values. + * items is ["\"alpha\"", "\"beta\"", "42", "true", ...]. */ +el_val_t json_build_array(el_val_t items) { + el_val_t list = items; + int64_t n = el_list_len(list); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < n; i++) { + el_val_t v = el_list_get(list, (el_val_t)i); + const char* vs = EL_CSTR(v); + if (!vs) continue; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, vs); + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + if (!fmt_str || *fmt_str == '\0' || strcmp(fmt_str, "ISO") == 0) { + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + /* Return a JSON string so callers can use json_get to extract fields. */ + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"year\":%d,\"month\":%d,\"day\":%d,\"hour\":%d,\"minute\":%d,\"second\":%d,\"ms\":%d}", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* Block the calling thread for `secs` seconds. Negative values are clamped + * to 0. Used by El programs that poll external resources (e.g. RunPod + * /status, Engram readiness probes). */ +el_val_t sleep_secs(el_val_t secs) { + int64_t s = (int64_t)secs; + if (s < 0) s = 0; + struct timespec ts; + ts.tv_sec = (time_t)s; + ts.tv_nsec = 0; + nanosleep(&ts, NULL); + return 0; +} + +el_val_t sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); + return 0; +} + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * El's substrate (Neuron) is a temporal cognition system. Memory salience + * decay, the six-tier pacemaker, TTL caches, and supersession are all + * temporal. Treating time as a raw Int (now() returning ms-since-epoch and + * arithmetic done with mixed unit literals) lets bugs through the type + * system: `(now - cached_at) < 60` cannot tell ms from sec, and `sleep(30)` + * is ambiguous. This block introduces two dedicated representations. + * + * Representation: + * Instant — int64 nanoseconds since the Unix epoch + * Duration — int64 nanoseconds (signed; negative durations are legal, + * e.g. when a deadline has passed) + * + * Both share the el_val_t (int64) slot the rest of the runtime uses, so no + * boxing / arena allocation is needed. Type discipline is enforced at the + * codegen layer: `let x: Duration = ...` registers `x` in __duration_names, + * and BinOp dispatches through typed wrappers (el_duration_add, etc.) that + * make intent explicit in the generated C. Mismatched ops (Instant+Instant, + * Duration+Int) are surfaced via #error directives at codegen time so the + * downstream cc step fails with a clear El-source-level message. + * + * Nanosecond precision matches POSIX clock_gettime / nanosleep granularity. + * 2^63 nanos covers ~292 years from epoch — comfortably past 2200, plenty + * for a memory-system runtime that never schedules outside a human lifespan. + */ + +/* now() — current Instant. Wraps clock_gettime(CLOCK_REALTIME) for nanosecond + * precision. Falls back to gettimeofday on systems where clock_gettime is + * unavailable (defensive — every supported platform has it). */ +el_val_t el_now_instant(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ns = (int64_t)tv.tv_sec * 1000000000LL + + (int64_t)tv.tv_usec * 1000LL; + return (el_val_t)ns; +} + +el_val_t now(void) { + return el_now_instant(); +} + +/* now_ns — return current Unix time as nanoseconds (Int). + * Thin wrapper over el_now_instant for use in test timing. */ +el_val_t now_ns(void) { + return el_now_instant(); +} + +/* unix_seconds(n) — Instant from a Unix-epoch second count. + * unix_millis(n) — Instant from a Unix-epoch millisecond count. */ +el_val_t unix_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t unix_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +/* instant_from_iso8601 — parse a strict subset: + * YYYY-MM-DDTHH:MM:SS[.fff]Z + * Returns 0 (the Unix-epoch sentinel) on parse failure. Callers that need to + * distinguish epoch-zero from a parse error should use a wider sentinel + * representation; the current zero-on-failure choice matches existing El + * runtime conventions for parse builtins (str_to_int, parse_int). */ +el_val_t instant_from_iso8601(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return (el_val_t)0; + int Y, M, D, h, m, sec, frac = 0; + int n = sscanf(str, "%d-%d-%dT%d:%d:%d.%3d", &Y, &M, &D, &h, &m, &sec, &frac); + if (n < 6) { + n = sscanf(str, "%d-%d-%dT%d:%d:%dZ", &Y, &M, &D, &h, &m, &sec); + if (n < 6) return (el_val_t)0; + } + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_year = Y - 1900; + tm.tm_mon = M - 1; + tm.tm_mday = D; + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = sec; + /* timegm — UTC. POSIX-Y but available on macOS and glibc. */ + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (int64_t)frac * 1000000LL; + return (el_val_t)ns; +} + +/* Duration constructors. The El-side postfix literals (30.seconds, 1.hour) + * are lowered by the codegen directly into a literal int64 of nanoseconds — + * these constructors are for runtime values where the count is dynamic. */ +el_val_t el_duration_from_nanos(el_val_t ns) { + return (el_val_t)(int64_t)ns; +} + +el_val_t duration_seconds(el_val_t n) { + int64_t s = (int64_t)n; + return (el_val_t)(s * 1000000000LL); +} + +el_val_t duration_millis(el_val_t n) { + int64_t m = (int64_t)n; + return (el_val_t)(m * 1000000LL); +} + +el_val_t duration_nanos(el_val_t n) { + return (el_val_t)(int64_t)n; +} + +/* Arithmetic — typed wrappers. At the C level these are no-op casts, but + * the codegen routes Instant/Duration BinOps through them so the generated + * C says `el_instant_add_dur(start, dur)` rather than `start + dur`. The + * intent is explicit, the operand order is documented, and a future change + * to the underlying representation (saturating arithmetic, overflow guards) + * has a single chokepoint. */ +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst + (int64_t)dur); +} + +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur) { + return (el_val_t)((int64_t)inst - (int64_t)dur); +} + +el_val_t el_instant_diff(el_val_t a, el_val_t b) { + /* a - b — yields a Duration (negative if b is later than a). */ + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_add(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a + (int64_t)b); +} + +el_val_t el_duration_sub(el_val_t a, el_val_t b) { + return (el_val_t)((int64_t)a - (int64_t)b); +} + +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar) { + return (el_val_t)((int64_t)dur * (int64_t)scalar); +} + +el_val_t el_duration_div(el_val_t dur, el_val_t scalar) { + int64_t s = (int64_t)scalar; + if (s == 0) return (el_val_t)0; + return (el_val_t)((int64_t)dur / s); +} + +/* Comparisons. Return 1/0 in el_val_t convention. */ +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a < (int64_t)b ? 1 : 0); } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a <= (int64_t)b ? 1 : 0); } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a > (int64_t)b ? 1 : 0); } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a >= (int64_t)b ? 1 : 0); } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a == (int64_t)b ? 1 : 0); } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return (el_val_t)((int64_t)a != (int64_t)b ? 1 : 0); } + +/* Conversions. */ +el_val_t instant_to_unix_seconds(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000000LL); +} + +el_val_t instant_to_unix_millis(el_val_t i) { + return (el_val_t)((int64_t)i / 1000000LL); +} + +el_val_t instant_to_iso8601(el_val_t i) { + int64_t ns = (int64_t)i; + time_t s = (time_t)(ns / 1000000000LL); + int msec = (int)((ns / 1000000LL) % 1000LL); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t duration_to_seconds(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000000LL); +} + +el_val_t duration_to_millis(el_val_t d) { + return (el_val_t)((int64_t)d / 1000000LL); +} + +el_val_t duration_to_nanos(el_val_t d) { + return (el_val_t)(int64_t)d; +} + +/* sleep(Duration) — Phase 1 replacement for ambiguous sleep(Int). The runtime + * still exposes sleep_secs/sleep_ms for legacy call sites; codegen lowers + * sleep(Duration) to el_sleep_duration(d). Negative durations clamp to 0 so a + * stale deadline doesn't block forever. */ +el_val_t el_sleep_duration(el_val_t dur) { + int64_t ns = (int64_t)dur; + if (ns < 0) ns = 0; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000LL); + ts.tv_nsec = (long)(ns % 1000000000LL); + nanosleep(&ts, NULL); + return (el_val_t)0; +} + +/* unix_timestamp() — back-compat. Existing El callers expect an Int seconds + * value; this stays an Int returner so the type system isn't disturbed for + * legacy code. New code should call now() and convert when needed. */ +el_val_t unix_timestamp(void) { + return instant_to_unix_seconds(el_now_instant()); +} + +/* TTL cache helpers. Backed by the existing process-wide K/V (state_set/get) + * with a sibling __ttl_set_at_ entry recording the Instant of the last + * write. ttl_cache_get returns "" if the entry is missing or stale, so call + * sites can branch on `if v == "" { miss } else { hit }` — the same shape + * existing get-with-default code uses. No more (now - cached_at) < 60. */ +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)0; + /* Store the value at the user's key. */ + state_set(key, value); + /* Stamp set_at — opaque schema, namespaced under __ttl: prefix so user + * keys can't collide with stamps. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)0; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + int64_t now_ns = (int64_t)el_now_instant(); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)now_ns); + state_set(EL_STR(stamp_key), EL_STR(buf)); + free(stamp_key); + return (el_val_t)1; +} + +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + /* Look up stamp. */ + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return el_wrap_str(el_strdup("")); + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return el_wrap_str(el_strdup("")); + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + int64_t age = now_ns - set_at; + int64_t max_ns = (int64_t)max_age; + if (age < 0) return el_wrap_str(el_strdup("")); /* clock skew — treat as miss */ + if (age > max_ns) return el_wrap_str(el_strdup("")); /* expired */ + return state_get(key); +} + +el_val_t ttl_cache_age(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return (el_val_t)INT64_MAX; + size_t klen = strlen(k); + char* stamp_key = (char*)malloc(klen + 16); + if (!stamp_key) return (el_val_t)INT64_MAX; + snprintf(stamp_key, klen + 16, "__ttl_at:%s", k); + el_val_t stamp = state_get(EL_STR(stamp_key)); + free(stamp_key); + const char* sv = EL_CSTR(stamp); + if (!sv || !*sv) return (el_val_t)INT64_MAX; + int64_t set_at = (int64_t)atoll(sv); + int64_t now_ns = (int64_t)el_now_instant(); + return (el_val_t)(now_ns - set_at); +} + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ────────────── + * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian + + * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar, + * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar; + * UTC and IANA zones are themselves Earth-parochial and cannot live at the + * lowest type layer. + * + * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday, + * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase + * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a + * station or 300y on a long-cycle world. */ + +/* Magic headers — used by the runtime to recognize boxed temporal values + * arriving through el_val_t. Distinct constants so accidental misuse fails + * loudly rather than silently. */ +#define EL_CAL_MAGIC 0xE1CA1EDDU +#define EL_CALTIME_MAGIC 0xE1CA1747U +#define EL_RHYTHM_MAGIC 0xE1287A11U +#define EL_LDATE_MAGIC 0xE1DA7E00U +#define EL_LDT_MAGIC 0xE1DA7E1DU +#define EL_ZONE_MAGIC 0xE12017E0U + +typedef enum { + EL_CALENDAR_EARTH = 1, + EL_CALENDAR_MARS = 2, + EL_CALENDAR_CYCLE = 3, + EL_CALENDAR_NO_CYCLE = 4, + EL_CALENDAR_RELATIVE = 5 +} el_calendar_kind_t; + +typedef struct { + uint32_t magic; + char* id; /* IANA name or "+HH:MM" / "-HH:MM" */ + int fixed; /* 1 for fixed offset, 0 for IANA */ + int64_t offset_ns; /* fixed offset in nanos (only when fixed) */ +} el_zone_t; + +typedef struct { + uint32_t magic; + el_calendar_kind_t kind; + el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */ + int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */ + int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */ +} el_calendar_t; + +typedef struct { + uint32_t magic; + int64_t instant_ns; + el_calendar_t* cal; +} el_caltime_t; + +/* Rhythm AST. */ +typedef enum { + EL_RHYTHM_CYCLE_START = 1, + EL_RHYTHM_CYCLE_PHASE = 2, + EL_RHYTHM_DURATION = 3, + EL_RHYTHM_SESSION_START = 4, + EL_RHYTHM_EVENT = 5, + EL_RHYTHM_AND = 6, + EL_RHYTHM_OR = 7, + EL_RHYTHM_WEEKDAY = 8, + EL_RHYTHM_WEEKLY_AT = 9 +} el_rhythm_kind_t; + +typedef struct el_rhythm_s { + uint32_t magic; + el_rhythm_kind_t kind; + double phase; /* CYCLE_PHASE */ + int64_t period_ns; /* DURATION */ + int weekday; /* 1..7 Mon..Sun */ + int hour; + int minute; + char* event_name; /* EVENT */ + struct el_rhythm_s* a; /* AND/OR */ + struct el_rhythm_s* b; +} el_rhythm_t; + +typedef struct { + uint32_t magic; + int year; + int month; + int day; +} el_localdate_t; + +typedef struct { + uint32_t magic; + el_localdate_t* date; + int64_t time_ns; /* nanos since midnight */ +} el_localdt_t; + +/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer + * and compare against the expected magic. Strings are NUL-terminated and + * never start with our magic byte sequence, so this is safe. */ +static int el_is_magic(el_val_t v, uint32_t want) { + if (v == 0) return 0; + /* Defensive: only follow pointers in plausible address space. + * On 64-bit unix processes pointers are above 0x10000. */ + if ((uint64_t)v < 0x10000ULL) return 0; + uint32_t got = *(volatile uint32_t*)(uintptr_t)v; + return got == want; +} + +/* Sol length on Mars in nanoseconds: 88775.244 seconds. */ +#define EL_MARS_SOL_NS ((int64_t)88775244000000LL) +/* Earth solar day in nanoseconds: 86400 seconds. */ +#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL) + +/* ── Zone construction ────────────────────────────────────────────────────── + * Zones intern by id string so equality comparisons are pointer-compares. */ + +#define EL_ZONE_TABLE_CAP 64 +static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP]; +static int _el_zone_count = 0; + +static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) { + for (int i = 0; i < _el_zone_count; i++) { + el_zone_t* z = _el_zone_table[i]; + if (z->fixed == fixed && z->offset_ns == offset_ns && + strcmp(z->id ? z->id : "", id ? id : "") == 0) { + return z; + } + } + if (_el_zone_count >= EL_ZONE_TABLE_CAP) { + /* Out of slots: build a non-interned zone. Equality will fail across + * such zones but the program still runs. */ + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + return z; + } + el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t)); + z->magic = EL_ZONE_MAGIC; + z->id = el_strdup_persist(id ? id : ""); + z->fixed = fixed; + z->offset_ns = offset_ns; + _el_zone_table[_el_zone_count++] = z; + return z; +} + +el_val_t zone(el_val_t id) { + const char* s = EL_CSTR(id); + if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0); + /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */ + if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') { + int sign = (s[0] == '-') ? -1 : 1; + int hh = (s[1] - '0') * 10 + (s[2] - '0'); + int mm = (s[4] - '0') * 10 + (s[5] - '0'); + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off); + } + return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0); +} + +el_val_t zone_utc(void) { + return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0); +} + +el_val_t zone_local(void) { + /* Resolve the local zone via TZ env or system default. tzset() picks + * up TZ if set; otherwise the C library reads /etc/localtime. We store + * the zone id as "LOCAL" so subsequent equality holds; resolution is + * lazy at use time. */ + return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0); +} + +el_val_t zone_offset(el_val_t hours, el_val_t minutes) { + int hh = (int)(int64_t)hours; + int mm = (int)(int64_t)minutes; + int sign = (hh < 0 || mm < 0) ? -1 : 1; + if (hh < 0) hh = -hh; + if (mm < 0) mm = -mm; + int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL; + char buf[16]; + snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm); + return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off); +} + +/* ── Calendar interning ──────────────────────────────────────────────────── */ + +#define EL_CAL_TABLE_CAP 64 +static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP]; +static int _el_cal_count = 0; + +static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z, + int64_t period_ns, int64_t epoch_ns) { + for (int i = 0; i < _el_cal_count; i++) { + el_calendar_t* c = _el_cal_table[i]; + if (c->kind == kind && c->zone == z && + c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) { + return c; + } + } + el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t)); + c->magic = EL_CAL_MAGIC; + c->kind = kind; + c->zone = z; + c->cycle_period_ns = period_ns; + c->epoch_ns = epoch_ns; + if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c; + return c; +} + +el_val_t earth_calendar(el_val_t z_val) { + el_zone_t* z = NULL; + if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) { + z = (el_zone_t*)(uintptr_t)z_val; + } else { + z = (el_zone_t*)(uintptr_t)zone_local(); + } + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0); +} + +el_val_t earth_calendar_default(void) { + return earth_calendar(zone_local()); +} + +el_val_t mars_calendar(void) { + el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0); + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0); +} + +el_val_t cycle_calendar(el_val_t period_dur) { + int64_t period = (int64_t)period_dur; + if (period <= 0) period = 1; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0); +} + +el_val_t no_cycle_calendar(void) { + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0); +} + +el_val_t relative_calendar(el_val_t epoch_inst) { + int64_t ep = (int64_t)epoch_inst; + return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep); +} + +/* ── CalendarTime ───────────────────────────────────────────────────────── */ + +static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) { + el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t)); + ct->magic = EL_CALTIME_MAGIC; + ct->instant_ns = inst; + ct->cal = c; + return ct; +} + +static el_calendar_t* _el_resolve_cal(el_val_t cal_val) { + if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) { + return (el_calendar_t*)(uintptr_t)earth_calendar_default(); + } + return (el_calendar_t*)(uintptr_t)cal_val; +} + +el_val_t now_in(el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t ns = (int64_t)el_now_instant(); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t in_calendar(el_val_t inst, el_val_t cal_val) { + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c); +} + +el_val_t cal_to_instant(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + return (el_val_t)ct->instant_ns; +} + +el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c); +} + +el_val_t cal_cycle_phase(el_val_t ct_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + el_calendar_t* c = ct->cal; + if (c->kind == EL_CALENDAR_NO_CYCLE) { + return el_from_float(0.0/0.0); /* NaN sentinel */ + } + int64_t period = c->cycle_period_ns; + if (period <= 0) return el_from_float(0.0); + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + return el_from_float(phase); +} + +/* ── Earth zone resolution: TZ-based offset lookup ────────────────────────── + * For an EarthCalendar(zone), we want to convert an instant_ns into local + * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use + * localtime_r, then restore. This is not thread-safe by design — El's + * runtime is single-threaded for the request handler path. Cache the + * computed (instant -> tm) to avoid the syscall churn on repeat formats. */ + +static void _el_apply_zone(el_zone_t* z) { + if (!z) { unsetenv("TZ"); tzset(); return; } + if (z->fixed && strcmp(z->id, "UTC") == 0) { + setenv("TZ", "UTC0", 1); + tzset(); + return; + } + if (z->fixed) { + /* Fixed offset: POSIX TZ uses inverted sign (sign convention of + * "hours WEST of UTC" rather than east). Build the spec accordingly. */ + char buf[32]; + int neg_secs = (int)(-z->offset_ns / 1000000000LL); + int sign = neg_secs < 0 ? -1 : 1; + int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs; + int hh = abs_secs / 3600; + int mm = (abs_secs % 3600) / 60; + snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm); + setenv("TZ", buf, 1); + tzset(); + return; + } + if (strcmp(z->id, "LOCAL") == 0) { + unsetenv("TZ"); + tzset(); + return; + } + setenv("TZ", z->id, 1); + tzset(); +} + +static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) { + el_calendar_t* c = ct->cal; + el_zone_t* z = c->zone; + _el_apply_zone(z); + time_t s = (time_t)(ct->instant_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + *tm_out = tm; + if (abbr_buf && abbr_cap > 0) { + const char* z_str = tm.tm_zone ? tm.tm_zone : ""; + size_t n = strlen(z_str); + if (n >= abbr_cap) n = abbr_cap - 1; + memcpy(abbr_buf, z_str, n); + abbr_buf[n] = '\0'; + if (abbr_len) *abbr_len = (int)n; + } + return 0; +} + +/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern. + * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for + * the acceptance tests. Single quotes denote literal text. */ +static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; +static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + +static char* _el_format_earth(el_caltime_t* ct, const char* pattern) { + struct tm tm; + char abbr[16] = {0}; + int abbr_len = 0; + _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr)); + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + size_t i = 0; + size_t plen = strlen(pattern); + while (i < plen) { + char ch = pattern[i]; + /* Quoted literal */ + if (ch == '\'') { + i++; + while (i < plen && pattern[i] != '\'') { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i++]; + } + if (i < plen) i++; + continue; + } + /* Count run of same letter */ + size_t run = 1; + while (i + run < plen && pattern[i + run] == ch) run++; + char tmp[64]; + tmp[0] = '\0'; + if (ch == 'y') { + if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900); + else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100); + } else if (ch == 'M') { + if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]); + else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1); + } else if (ch == 'd') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday); + } else if (ch == 'H') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour); + } else if (ch == 'h') { + int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12; + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12); + else snprintf(tmp, sizeof(tmp), "%d", h12); + } else if (ch == 'm') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min); + } else if (ch == 's') { + if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec); + else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec); + } else if (ch == 'a') { + snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM"); + } else if (ch == 'E') { + snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]); + } else if (ch == 'z') { + snprintf(tmp, sizeof(tmp), "%s", abbr); + } else { + for (size_t k = 0; k < run; k++) { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = ch; + } + i += run; + continue; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + i += run; + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a Mars CalendarTime: %sol prints the integer sol number since + * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a + * 0..1 decimal. Other %-specifiers fall through. */ +static char* _el_format_mars(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t sol = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; sol -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "sol", 3) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 3; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)sol); + i += 1; + } else { + tmp[0] = pattern[i+1]; tmp[1] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +/* Format a CycleCalendar CalendarTime: %cycle and %phase. */ +static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) { + el_calendar_t* c = ct->cal; + int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1; + int64_t base = ct->instant_ns - c->epoch_ns; + int64_t cycle = base / period; + int64_t phase_ns = base % period; + if (phase_ns < 0) { phase_ns += period; cycle -= 1; } + double phase = (double)phase_ns / (double)period; + size_t cap = strlen(pattern) * 4 + 64; + char* out = (char*)malloc(cap); + size_t pos = 0; + for (size_t i = 0; pattern[i]; i++) { + if (pattern[i] == '%' && pattern[i+1]) { + char tmp[64]; + tmp[0] = '\0'; + if (strncmp(pattern + i + 1, "cycle", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 5; + } else if (strncmp(pattern + i + 1, "phase", 5) == 0) { + snprintf(tmp, sizeof(tmp), "%.4f", phase); + i += 5; + } else if (pattern[i+1] == 'd') { + snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle); + i += 1; + } else if (pattern[i+1] == 'f') { + snprintf(tmp, sizeof(tmp), "%.2f", phase); + i += 1; + } else { + /* Pass through unknown specifier */ + tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0'; + i += 1; + } + size_t tl = strlen(tmp); + if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); } + memcpy(out + pos, tmp, tl); + pos += tl; + } else { + if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); } + out[pos++] = pattern[i]; + } + } + out[pos] = '\0'; + char* result = el_strdup(out); + free(out); + return result; +} + +el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) { + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup("")); + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + const char* pat = EL_CSTR(pattern_val); + if (!pat) pat = ""; + char* result = NULL; + switch (ct->cal->kind) { + case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break; + case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break; + case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break; + case EL_CALENDAR_NO_CYCLE: { + char buf[64]; + snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns); + result = el_strdup(buf); + break; + } + default: result = el_strdup(""); + } + return el_wrap_str(result); +} + +/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */ + +static int _el_days_in_month(int y, int m) { + static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2) { + int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); + return 28 + (leap ? 1 : 0); + } + if (m < 1 || m > 12) return 30; + return dim[m - 1]; +} + +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { + el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t)); + ld->magic = EL_LDATE_MAGIC; + ld->year = (int)(int64_t)y; + ld->month = (int)(int64_t)m; + ld->day = (int)(int64_t)d; + return (el_val_t)(uintptr_t)ld; +} + +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { + int64_t hh = (int64_t)h; + int64_t mm = (int64_t)m; + int64_t ss = (int64_t)s; + int64_t nn = (int64_t)ns; + int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn; + return (el_val_t)total; +} + +el_val_t local_datetime(el_val_t date_val, el_val_t time_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t)); + ldt->magic = EL_LDT_MAGIC; + ldt->date = (el_localdate_t*)(uintptr_t)date_val; + ldt->time_ns = (int64_t)time_val; + return (el_val_t)(uintptr_t)ldt; +} + +el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) { + if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t time_ns = (int64_t)time_val; + /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant. + * For non-Earth calendars we use day-anchored conversion: treat the + * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via + * mktime under the calendar's zone, then add nanos-since-midnight. */ + if (c->kind == EL_CALENDAR_EARTH) { + _el_apply_zone(c->zone); + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + tm.tm_isdst = -1; + time_t t = mktime(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); + } + /* Non-Earth fallback: project as if Earth UTC then attach calendar. */ + struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_year = ld->year - 1900; + tm.tm_mon = ld->month - 1; + tm.tm_mday = ld->day; + tm.tm_hour = (int)(time_ns / 3600000000000LL); + tm.tm_min = (int)((time_ns / 60000000000LL) % 60); + tm.tm_sec = (int)((time_ns / 1000000000LL) % 60); + time_t t = timegm(&tm); + if (t == (time_t)-1) return (el_val_t)0; + int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL); + return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c); +} + +el_val_t local_date_year(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year; +} +el_val_t local_date_month(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month; +} +el_val_t local_date_day(el_val_t v) { + if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0; + return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day; +} +el_val_t local_time_hour(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t / 3600000000000LL); +} +el_val_t local_time_minute(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 60000000000LL) % 60); +} +el_val_t local_time_second(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)((t / 1000000000LL) % 60); +} +el_val_t local_time_nanos(el_val_t v) { + int64_t t = (int64_t)v; + return (el_val_t)(t % 1000000000LL); +} + +el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) { + if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val; + el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val; + int64_t dur_ns = (int64_t)dur_val; + int64_t days = dur_ns / EL_EARTH_DAY_NS; + int y = ld->year, m = ld->month, d = ld->day; + /* Walk days forward/backward in canonical Gregorian. */ + while (days > 0) { + int dim = _el_days_in_month(y, m); + if (d + days <= dim) { d += (int)days; days = 0; break; } + days -= (dim - d + 1); + d = 1; + m++; + if (m > 12) { m = 1; y++; } + } + while (days < 0) { + if (d + days >= 1) { d += (int)days; days = 0; break; } + days += d; + m--; + if (m < 1) { m = 12; y--; } + d = _el_days_in_month(y, m); + } + return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d); +} + +el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) { + int64_t t = (int64_t)lt_val + (int64_t)dur_val; + /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the + * caller to use cal_in / cal_format for the right modulus. */ + int64_t day = EL_EARTH_DAY_NS; + int64_t r = t % day; + if (r < 0) r += day; + return (el_val_t)r; +} + +el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0); + if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0); + return (el_val_t)(a->day < b->day ? 1 : 0); +} + +el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) { + if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0; + el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val; + el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val; + return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0); +} + +/* ── Rhythm ──────────────────────────────────────────────────────────────── */ + +static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) { + el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t)); + r->magic = EL_RHYTHM_MAGIC; + r->kind = k; + return r; +} + +el_val_t rhythm_cycle_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START); +} + +el_val_t rhythm_cycle_phase(el_val_t phase_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE); + r->phase = el_to_float(phase_val); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_duration(el_val_t d_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION); + r->period_ns = (int64_t)d_val; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_session_start(void) { + return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START); +} + +el_val_t rhythm_event(el_val_t name_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT); + const char* n = EL_CSTR(name_val); + r->event_name = el_strdup_persist(n ? n : ""); + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR); + r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL; + r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekday(el_val_t day) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY); + r->weekday = (int)(int64_t)day; + return (el_val_t)(uintptr_t)r; +} + +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) { + el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT); + r->weekday = (int)(int64_t)day; + r->hour = (int)(int64_t)hour; + r->minute = (int)(int64_t)minute; + return (el_val_t)(uintptr_t)r; +} + +/* Compute the next instant on or after `after` when rhythm `r` matches, + * under calendar `cal`. */ +static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) { + if (!r) return after_ns; + int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc = (base / period) + 1; + return cal->epoch_ns + cyc * period; + } + case EL_RHYTHM_CYCLE_PHASE: { + int64_t base = after_ns - cal->epoch_ns; + int64_t cyc_ns = (int64_t)(r->phase * (double)period); + int64_t cur_cyc = base / period; + int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns; + if (candidate <= after_ns) candidate += period; + return candidate; + } + case EL_RHYTHM_DURATION: { + return after_ns + (r->period_ns > 0 ? r->period_ns : 1); + } + case EL_RHYTHM_WEEKDAY: + case EL_RHYTHM_WEEKLY_AT: { + if (cal->kind != EL_CALENDAR_EARTH) { + /* Non-Earth calendars: fall back to cycle math, treating + * weekday as a 7-cycle-per-period proxy. */ + return after_ns + period; + } + _el_apply_zone(cal->zone); + time_t s = (time_t)(after_ns / 1000000000LL); + struct tm tm; + localtime_r(&s, &tm); + /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */ + int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1; + int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */ + int days_ahead = (target_wday - tm.tm_wday + 7) % 7; + int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0; + int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0; + struct tm cand = tm; + cand.tm_mday += days_ahead; + cand.tm_hour = hour; + cand.tm_min = minute; + cand.tm_sec = 0; + cand.tm_isdst = -1; + time_t cand_t = mktime(&cand); + int64_t cand_ns = (int64_t)cand_t * 1000000000LL; + if (cand_ns <= after_ns) { + cand.tm_mday += 7; + cand.tm_isdst = -1; + cand_t = mktime(&cand); + cand_ns = (int64_t)cand_t * 1000000000LL; + } + return cand_ns; + } + case EL_RHYTHM_AND: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a > b ? a : b; + } + case EL_RHYTHM_OR: { + int64_t a = _el_next_after(r->a, after_ns, cal); + int64_t b = _el_next_after(r->b, after_ns, cal); + return a < b ? a : b; + } + case EL_RHYTHM_SESSION_START: + case EL_RHYTHM_EVENT: + default: + return after_ns; + } +} + +el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_calendar_t* c = _el_resolve_cal(cal_val); + int64_t out = _el_next_after(r, (int64_t)after_val, c); + return (el_val_t)out; +} + +el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) { + if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0; + if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0; + el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val; + el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val; + int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS; + int64_t base = ct->instant_ns - ct->cal->epoch_ns; + int64_t phase_ns = base % period; + if (phase_ns < 0) phase_ns += period; + double phase = (double)phase_ns / (double)period; + switch (r->kind) { + case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0); + case EL_RHYTHM_CYCLE_PHASE: { + double diff = phase - r->phase; + if (diff < 0) diff = -diff; + return (el_val_t)(diff < 0.001 ? 1 : 0); + } + default: return (el_val_t)0; + } +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; +/* Mutex protecting all _state_entries access. state_set/state_get are called + * concurrently from 64 HTTP worker threads — without this lock, realloc and + * free race, producing corruption, double-free, and segfaults. */ +static pthread_mutex_t _state_mu = PTHREAD_MUTEX_INITIALIZER; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup_persist(v); + pthread_mutex_unlock(&_state_mu); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + StateEntry* grown = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!grown) { pthread_mutex_unlock(&_state_mu); fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_entries = grown; + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup_persist(k); + _state_entries[_state_count].value = el_strdup_persist(v); + _state_count++; + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + char* result = el_strdup_persist(e ? e->value : ""); + pthread_mutex_unlock(&_state_mu); + /* wrap in arena-tracked copy for the caller's request lifetime */ + char* copy = el_strdup(result); + return el_wrap_str(copy); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + pthread_mutex_unlock(&_state_mu); + return 1; + } + } + pthread_mutex_unlock(&_state_mu); + return 1; +} + +el_val_t state_keys(void) { + pthread_mutex_lock(&_state_mu); + /* Build a JSON array string: ["key1","key2",...] */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (size_t i = 0; i < _state_count; i++) { + if (i > 0) jb_putc(&b, ','); + jb_putc(&b, '"'); + jb_emit_escaped(&b, _state_entries[i].key); + jb_putc(&b, '"'); + } + jb_putc(&b, ']'); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(b.buf); +} + +/* Returns 1 (true) if the key is present in the state store, else 0 (false). */ +el_val_t state_has(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + int found = (e != NULL) ? 1 : 0; + pthread_mutex_unlock(&_state_mu); + return (el_val_t)found; +} + +/* Returns the value for key, or default_val if the key is absent. */ +el_val_t state_get_or(el_val_t key, el_val_t default_val) { + const char* k = EL_CSTR(key); + if (!k) return default_val; + pthread_mutex_lock(&_state_mu); + StateEntry* e = state_find(k); + if (e) { + char* copy = el_strdup(e->value); + pthread_mutex_unlock(&_state_mu); + return el_wrap_str(copy); + } + pthread_mutex_unlock(&_state_mu); + return default_val; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + double v = el_to_float(f); + /* Normalize NaN to "nan" regardless of sign — platform-independent. */ + if (isnan(v)) { + snprintf(buf, sizeof(buf), "nan"); + } else { + snprintf(buf, sizeof(buf), "%g", v); + } + return el_wrap_str(el_strdup(buf)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t fmt, el_val_t data) { + const char* tpl = EL_CSTR(fmt); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * + * Phase 1 covers the operations every text-handling caller used to roll by + * hand on top of str_index_of + str_slice. The character-class predicates + * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness, + * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the + * first byte; multi-char input requires ALL bytes to match (false otherwise). + * + * Counting: + * str_count non-overlapping occurrences of sub in s + * str_count_chars codepoint count (UTF-8 leading-byte count) + * str_count_bytes explicit byte length (alias of str_len) + * str_count_lines \n-delimited line count (\r\n folded to \n) + * str_count_words whitespace-delimited tokens, non-empty only + * str_count_letters ASCII [A-Za-z] + * str_count_digits ASCII [0-9] + * + * Find / position: + * str_index_of_all all byte offsets of sub, [] if none + * str_last_index_of last byte offset of sub, -1 if not found + * str_find_chars first index of any char in any_of, -1 if none + * + * Transform: + * str_repeat s * n (non-negative) + * str_reverse codepoint-reversed (NOT grapheme-aware) + * str_strip_prefix s without prefix if present, else s + * str_strip_suffix s without suffix if present, else s + * str_strip_chars strip leading+trailing chars matching any in chars + * str_lstrip strip leading whitespace + * str_rstrip strip trailing whitespace + * + * Char classification (Bool): + * is_letter, is_digit, is_alphanumeric, is_whitespace, + * is_punctuation, is_uppercase, is_lowercase + * + * Splitting: + * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped. + * str_split_chars alias of native_string_chars in str_ namespace + * str_split_n split into at most n parts (last part keeps the + * rest verbatim, including any further separators) + * + * Joining: + * str_join [String] -> String, sep between elements + */ + +/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */ +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lp = strlen(sub); + int64_t count = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { + count++; + p += lp; /* non-overlapping advance */ + } + return (el_val_t)count; +} + +/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */ +el_val_t str_count_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p & 0xC0) != 0x80) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_bytes(el_val_t sv) { + return str_len(sv); +} + +el_val_t str_count_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + int64_t count = 0; + int has_content = 0; + for (const char* p = s; *p; p++) { + has_content = 1; + if (*p == '\n') { + count++; + has_content = 0; /* the \n closed the line */ + } + } + if (has_content) count++; /* trailing line with no terminator */ + return (el_val_t)count; +} + +el_val_t str_count_words(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + int in_word = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (isspace(*p)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + count++; + } + } + return (el_val_t)count; +} + +el_val_t str_count_letters(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++; + } + return (el_val_t)count; +} + +el_val_t str_count_digits(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t count = 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p >= '0' && *p <= '9') count++; + } + return (el_val_t)count; +} + +el_val_t str_index_of_all(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + el_val_t lst = el_list_empty(); + if (!s || !sub || !*sub) return lst; + size_t lp = strlen(sub); + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s)); + p = hit + lp; + } + return lst; +} + +el_val_t str_last_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return -1; + size_t lp = strlen(sub); + int64_t last = -1; + const char* p = s; + const char* hit; + while ((hit = strstr(p, sub)) != NULL) { + last = (int64_t)(hit - s); + p = hit + lp; + } + return (el_val_t)last; +} + +el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) { + const char* s = EL_CSTR(sv); + const char* any = EL_CSTR(any_of_v); + if (!s || !any || !*any) return -1; + for (const char* p = s; *p; p++) { + if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s); + } + return -1; +} + +el_val_t str_repeat(el_val_t sv, el_val_t nv) { + const char* s = EL_CSTR(sv); + int64_t n = (int64_t)nv; + if (!s || n <= 0) return el_wrap_str(el_strdup("")); + size_t ls = strlen(s); + if (ls == 0) return el_wrap_str(el_strdup("")); + size_t total = ls * (size_t)n; + char* out = el_strbuf(total); + for (int64_t i = 0; i < n; i++) { + memcpy(out + i * ls, s, ls); + } + out[total] = '\0'; + return el_wrap_str(out); +} + +/* Reverse by codepoint: walk codepoints, copy each backwards into the output. + * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint + * will detach. ASCII strings are byte-reverse equivalent. */ +el_val_t str_reverse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + /* Walk forward, find each codepoint's byte length, then copy from the end. */ + size_t out_pos = n; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + int cp_len; + if ((*p & 0x80) == 0x00) cp_len = 1; + else if ((*p & 0xE0) == 0xC0) cp_len = 2; + else if ((*p & 0xF0) == 0xE0) cp_len = 3; + else if ((*p & 0xF8) == 0xF0) cp_len = 4; + else cp_len = 1; /* invalid byte: passthrough */ + out_pos -= cp_len; + memcpy(out + out_pos, p, cp_len); + p += cp_len; + } + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* pref = EL_CSTR(prefv); + if (!s) return el_wrap_str(el_strdup("")); + if (!pref || !*pref) return el_wrap_str(el_strdup(s)); + size_t lp = strlen(pref); + size_t ls = strlen(s); + if (lp <= ls && strncmp(s, pref, lp) == 0) { + char* out = el_strbuf(ls - lp); + memcpy(out, s + lp, ls - lp); + out[ls - lp] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s) return el_wrap_str(el_strdup("")); + if (!suf || !*suf) return el_wrap_str(el_strdup(s)); + size_t ls = strlen(s); + size_t lsuf = strlen(suf); + if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) { + char* out = el_strbuf(ls - lsuf); + memcpy(out, s, ls - lsuf); + out[ls - lsuf] = '\0'; + return el_wrap_str(out); + } + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) { + const char* s = EL_CSTR(sv); + const char* chars = EL_CSTR(charsv); + if (!s) return el_wrap_str(el_strdup("")); + if (!chars || !*chars) return el_wrap_str(el_strdup(s)); + const char* start = s; + while (*start && strchr(chars, *start)) start++; + size_t n = strlen(start); + while (n > 0 && strchr(chars, start[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, start, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_lstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + return el_wrap_str(el_strdup(s)); +} + +el_val_t str_rstrip(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +/* Character classification. + * Empty input returns false. Multi-char input requires ALL bytes to match. + * ASCII range only; Phase 2 will widen to Unicode. */ +static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return 0; + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (!pred(*p)) return 0; + } + return 1; +} + +static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; } +static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); } +static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; } +static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; } +static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; } + +el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); } +el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); } +el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); } +el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); } +el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); } +el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); } +el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); } + +/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n + * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */ +el_val_t str_split_lines(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t lst = el_list_empty(); + if (!s) return lst; + size_t n = strlen(s); + /* Pre-scan: build into a normalized buffer with \r\n folded. */ + const char* line_start = s; + for (size_t i = 0; i <= n; i++) { + if (s[i] == '\n' || s[i] == '\0') { + size_t len = (size_t)(s + i - line_start); + /* Drop trailing \r if this was \r\n. */ + if (len > 0 && line_start[len - 1] == '\r') len--; + /* Drop final trailing-empty-after-newline. */ + if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break; + char* out = el_strbuf(len); + memcpy(out, line_start, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + if (s[i] == '\0') break; + line_start = s + i + 1; + } + } + return lst; +} + +el_val_t str_split_chars(el_val_t s) { + return native_string_chars(s); +} + +/* Split into at most n parts. The (n-1)th split point is the LAST split; + * after it, the remainder is appended verbatim including any further + * separators. n <= 0 returns an empty list. n == 1 returns [s]. */ +el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) { + const char* s = EL_CSTR(sv); + const char* sep = EL_CSTR(sepv); + int64_t n = (int64_t)nv; + el_val_t lst = el_list_empty(); + if (!s) return lst; + if (n <= 0) return lst; + if (n == 1 || !sep || !*sep) { + lst = el_list_append(lst, el_wrap_str(el_strdup(s))); + return lst; + } + size_t lp = strlen(sep); + const char* p = s; + int64_t parts = 0; + const char* hit; + while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) { + size_t len = (size_t)(hit - p); + char* out = el_strbuf(len); + memcpy(out, p, len); + out[len] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + parts++; + } + /* Remainder verbatim. */ + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +/* Join a [String] with a separator. Empty list -> "". Single-element -> + * that element. Non-string elements are stringified via int_to_str. */ +el_val_t str_join(el_val_t listv, el_val_t sepv) { + return list_join(listv, sepv); +} + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +/* parse_int — strtoll with a default. str_to_int already exists but does not + * distinguish "0" from a parse failure, so callers that need a sentinel use + * this. Skips leading whitespace; accepts an optional leading +/-; returns + * default_val on empty input or no consumed digits. Trailing junk is ignored + * (atoi-style). */ +el_val_t parse_int(el_val_t sv, el_val_t default_val) { + const char* s = EL_CSTR(sv); + if (!s) return default_val; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == '\0') return default_val; + char* end = NULL; + long long n = strtoll(s, &end, 10); + if (end == s) return default_val; + return (el_val_t)n; +} + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code) { + exit((int)code); + return 0; /* unreachable */ +} + +/* getpid_now — current process id. Named with the _now suffix to avoid + * colliding with the libc `getpid` declaration that the runtime already + * sees via (calling it `getpid` would fight the prototype). */ +el_val_t getpid_now(void) { + return (el_val_t)getpid(); +} + +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + +/* ── args() — command-line argument access ────────────────────────────────── + * Compiled El programs call args() to get a list of CLI arguments. + * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + + +/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */ +/* + * Single global EngramStore allocated lazily on first call. All node and + * edge content strings are owned (strdup'd) by the store. Linear arrays + * with doubling capacity for both nodes and edges. + * + * Two-layer activation algorithm (engram_activate): + * + * LAYER 1 — Broad fan-out (background activation): + * 1. Find seed nodes whose content/label/tags contain query (case-insens). + * 2. BFS up to `depth` hops along ALL edges (excitatory and inhibitory). + * Every reachable node fires — nothing is filtered at this layer. + * 3. bg_act = seed.salience * temporal_decay * dampening + * propagated as: new_bg = parent_bg * edge_weight * 0.7 * (1 + tbonus) + * where tbonus ∈ {0, 0.10, 0.20} for co-temporal nodes. + * 4. If reached by multiple paths, take max background_activation. + * 5. Persist background_activation to EngramNode.background_activation. + * + * LAYER 2 — Executive filter (working memory promotion): + * 6. For each inhibitory edge where source has background_activation > 0: + * inhibition[target] = max(bg[source] * e->weight) + * 7. For each background-activated node: + * raw_wm = bg * goal_bias(node, query) * confidence + * * (1 - (1 - INHIBITION_FACTOR) * inhibition) + * 8. Per-type threshold gate: raw_wm >= type_threshold → promoted. + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + * 9. If not promoted: suppression_count++. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH suppressions → force breakthrough + * at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension surfacing). + * 10. Persist working_memory_weight to EngramNode.working_memory_weight. + * 11. Sort: promoted nodes (wm > 0) first by wm desc, then background- + * only by bg desc. Context compilation uses ONLY promoted nodes. + * + * Temporal decay: + * decay_factor = exp(-lambda * age_hours / T_half) + * T_half = 168.0 h (one week), lambda = ln(2) + * + * Activation dampening: + * dampen = 1.0 / (1.0 + log(1 + activation_count)) + * + * engram_query_range(start_ms, end_ms): + * Returns nodes whose created_at OR last_activated falls within + * [start_ms, end_ms], sorted by created_at ascending. + */ + +/* Temporal decay constants. + * T_HALF_HOURS: half-life in hours — one week. After one week of no + * activation a node retains 50% of its base salience contribution. + * DECAY_LAMBDA: ln(2) ≈ 0.693147 */ +#define ENGRAM_T_HALF_HOURS 168.0 +#define ENGRAM_DECAY_LAMBDA 0.693147 + +/* Two-layer activation constants. + * ENGRAM_WM_THRESHOLD: minimum background_activation for a node to be + * considered for working-memory promotion (layer 2 candidate gate). + * ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for + * nodes NOT re-activated in the current turn (conversational thread + * continuity: a node promoted in turn N persists with reduced weight + * into turn N+1 without re-activation cost). + * ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions + * a latent node forces itself into working memory at reduced weight, + * modelling the brain's "intrusive thought" / unresolved-tension surfacing. + * ENGRAM_BREAKTHROUGH_WEIGHT: the reduced working_memory_weight assigned + * when a suppressed node breaks through. + * ENGRAM_INHIBITION_FACTOR: multiplier applied to working_memory_weight when + * an inhibitory edge fires against a node (0 = full suppress, 0.3 = partial). */ +#define ENGRAM_WM_THRESHOLD 0.15 +#define ENGRAM_WM_DECAY 0.7 +#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +#define ENGRAM_INHIBITION_FACTOR 0.1 + +/* ── Layered consciousness architecture ────────────────────────────────────── + * + * The engram graph is stratified into LAYERS that gate which suppressions + * apply during the executive filter pass. Layers are ordered shallow-to-deep + * by `activation_priority`; the deepest layer (priority 0, conventionally + * "safety") is the structural floor of the soul: nodes here cannot be + * silenced by inhibitory edges from any other layer. Higher layers + * (core-identity, domain-knowledge, imprint, suit) are normally + * suppressible — they participate in attentional inhibition and goal + * focus the way the prior single-graph implementation did. + * + * The five canonical layers (see engram_init_layers): + * 0. safety — structural, transparent, non-injectable, non-suppressible + * 1. core-identity — default for legacy nodes; suppressible + * 2. domain-knowledge— suppressible + * 3. imprint — runtime-injectable (an Imprint package can add/remove) + * 4. suit — runtime-injectable (a Suit overlays domain skill) + * + * Three-pass activation (engram_activate): + * Pass 1 — Background fan-out: BFS spreads activation across ALL layers + * (existing behavior preserved). Inhibitory edges propagate at + * this layer too; no filtering happens here. + * Pass 2 — Working memory promotion: type-threshold gate, goal bias, + * confidence weighting, inhibitory suppression. Inhibitory edges + * ONLY apply against nodes whose layer is `suppressible == 1`. + * Nodes in non-suppressible layers (Layer 0) ignore inhibition. + * Pass 3 — Layer 0 override: every node in a non-suppressible layer that + * received background activation has its working_memory_weight + * forced to >= ENGRAM_LAYER0_OVERRIDE_WEIGHT. The sacred fire — + * safety nodes that touched any seed unconditionally surface, + * even when the executive filter would have silenced them. + * + * Layer fields: + * suppressible : 0 → inhibitory edges are ignored against nodes in this + * layer during pass 2. Pass 3 also force-promotes them. + * 1 → standard behavior (most layers). + * transparent : 1 → emitted into the prompt context so its content shapes + * output, but filtered out of "what do you know about + * yourself?" introspection queries (engram_search and + * friends do not return transparent-layer nodes by + * default). 0 → fully visible to introspection. + * injectable : 1 → can be added/removed at runtime via engram_add_layer + * and engram_remove_layer (imprints, suits). + * 0 → built-in, fixed at engram_get() initialization. + * + * Backward compatibility: + * Nodes and edges loaded from snapshots without a `layer_id` field default + * to layer 1 (core-identity). The five canonical layers are always present. + */ +#define ENGRAM_LAYER_SAFETY 0u +#define ENGRAM_LAYER_CORE_IDENTITY 1u +#define ENGRAM_LAYER_DOMAIN 2u +#define ENGRAM_LAYER_IMPRINT 3u +#define ENGRAM_LAYER_SUIT 4u +#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY + +/* Pass 3 override floor. Layer 0 nodes that received any background + * activation are force-promoted to AT LEAST this working_memory_weight, + * regardless of inhibitory suppression in pass 2. */ +#define ENGRAM_LAYER0_OVERRIDE_WEIGHT 1.0 + +/* Per-node-type activation thresholds. + * Lower tier / safety-critical nodes fire more readily. */ +static double engram_type_threshold(const char* node_type, const char* tier) { + if (node_type) { + if (strcmp(node_type, "DharmaSelf") == 0) return 0.05; + if (strcmp(node_type, "Safety") == 0) return 0.05; + } + if (tier) { + if (strcmp(tier, "Canonical") == 0) return 0.15; + if (strcmp(tier, "Lesson") == 0) return 0.25; + } + if (node_type) { + if (strcmp(node_type, "Belief") == 0) return 0.30; + if (strcmp(node_type, "Entity") == 0) return 0.30; + } + return 0.40; /* Note / Memory / Working (most nodes) */ +} + +typedef struct EngramNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; /* per-node override for lambda; 0 = use default */ + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + /* Two-layer activation fields ───────────────────────────────────────── + * background_activation: Layer 1. Set by BFS fan-out on every query. + * Every reachable node fires here — nothing is filtered at this stage. + * Models the brain's massive parallel sub-threshold activation of all + * associated content in response to a stimulus. + * working_memory_weight: Layer 2. Executive filter output. Only nodes + * that survive goal-state / attentional-bias scoring receive a + * non-zero weight here. Context compilation ONLY uses this field. + * Background-activated nodes with working_memory_weight == 0 remain + * latent — real, available, but silent. + * suppression_count: Consecutive turn count where this node was + * background-activated but NOT promoted to working memory. High + * values signal the node "wants to surface." After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressions the node + * is force-promoted at a reduced weight (breakthrough activation). */ + double background_activation; + double working_memory_weight; + int32_t suppression_count; + /* Layered consciousness — see ENGRAM_LAYER_* macros and engram_init_layers. + * Defaults to ENGRAM_LAYER_DEFAULT (1, core-identity) for legacy nodes + * created via engram_node / engram_node_full and for snapshots that + * predate the layered schema. */ + uint32_t layer_id; +} EngramNode; + +typedef struct EngramEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + /* Inhibitory flag: when 1, activating the source node SUPPRESSES the + * working_memory_weight of the target node rather than exciting it. + * Models attentional inhibition: "I am focused on code work" creates + * inhibitory edges to personal/emotional nodes, preventing them from + * surfacing even if they have high background_activation. */ + int inhibitory; + /* Layered consciousness — edges carry a layer assignment for + * categorization/visualization. Pass 2 inhibitory gating is decided by + * the TARGET node's layer (whether it's suppressible), not by the edge + * layer. Defaults to ENGRAM_LAYER_DEFAULT. */ + uint32_t layer_id; +} EngramEdge; + +/* Layered consciousness — runtime layer registry entry. */ +typedef struct EngramLayer { + uint32_t layer_id; /* 0 = deepest (safety/limbic) */ + char* name; /* persistent — owned by the store */ + uint32_t activation_priority; /* lower = fires earlier; safety = 0 */ + int suppressible; /* can higher layers suppress nodes here? */ + int transparent; /* invisible to introspection queries? */ + int injectable; /* can be added/removed at runtime? */ +} EngramLayer; + +typedef struct EngramStore { + EngramNode* nodes; + int64_t node_count; + int64_t node_capacity; + EngramEdge* edges; + int64_t edge_count; + int64_t edge_capacity; + /* Layer registry — see engram_init_layers. The five canonical layers + * are always present; injectable layers (imprint, suit) are extended + * via engram_add_layer at runtime. layer_id values are assigned + * monotonically; removed injectable layers leave a NULL `name` slot + * (tombstone) so existing layer_id references on nodes stay stable. */ + EngramLayer* layers; + size_t layer_count; + size_t layer_capacity; +} EngramStore; + +static EngramStore* engram_global = NULL; + +/* Initialize the five canonical layers on a fresh store. Called once from + * engram_get(). Layer ids 0..4 are reserved; runtime-injected imprint/suit + * layers (engram_add_layer) get ids 5+. */ +static void engram_init_layers(EngramStore* g) { + g->layer_capacity = 16; + g->layers = calloc(g->layer_capacity, sizeof(EngramLayer)); + if (!g->layers) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + g->layer_count = 0; + + /* Layer 0 — safety. Structural floor. Non-suppressible; transparent + * (filtered out of introspection but still shapes output); not + * runtime-injectable. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SAFETY, + .name = el_strdup_persist("safety"), + .activation_priority = 0, + .suppressible = 0, + .transparent = 1, + .injectable = 0 + }; + /* Layer 1 — core-identity. The default home for legacy nodes. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_CORE_IDENTITY, + .name = el_strdup_persist("core-identity"), + .activation_priority = 10, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 2 — domain-knowledge. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_DOMAIN, + .name = el_strdup_persist("domain-knowledge"), + .activation_priority = 20, + .suppressible = 1, + .transparent = 0, + .injectable = 0 + }; + /* Layer 3 — imprint. Injectable: an imprint package adds/removes this + * layer (and the nodes assigned to it) as a unit. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_IMPRINT, + .name = el_strdup_persist("imprint"), + .activation_priority = 30, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; + /* Layer 4 — suit. Injectable: a Suit overlays domain skill (e.g. + * "enterprise advisor", "divorce lawyer") and can be detached. */ + g->layers[g->layer_count++] = (EngramLayer){ + .layer_id = ENGRAM_LAYER_SUIT, + .name = el_strdup_persist("suit"), + .activation_priority = 40, + .suppressible = 1, + .transparent = 0, + .injectable = 1 + }; +} + +static EngramStore* engram_get(void) { + if (engram_global) return engram_global; + engram_global = calloc(1, sizeof(EngramStore)); + if (!engram_global) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + engram_global->node_capacity = 16; + engram_global->nodes = calloc((size_t)engram_global->node_capacity, sizeof(EngramNode)); + engram_global->edge_capacity = 16; + engram_global->edges = calloc((size_t)engram_global->edge_capacity, sizeof(EngramEdge)); + engram_init_layers(engram_global); + return engram_global; +} + +/* Resolve a layer record by id. Returns NULL if no layer with that id + * exists (e.g. a removed injectable layer or a malformed snapshot). */ +static EngramLayer* engram_find_layer(uint32_t layer_id) { + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; /* tombstone for removed injectable layer */ + if (L->layer_id == layer_id) return L; + } + return NULL; +} + +/* Resolve a layer record by name. Returns NULL if not found. */ +static EngramLayer* engram_find_layer_by_name(const char* name) { + if (!name || !*name) return NULL; + EngramStore* g = engram_get(); + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (strcmp(L->name, name) == 0) return L; + } + return NULL; +} + +/* Allocate the next layer id. Skips ids that are still in use. */ +static uint32_t engram_next_layer_id(void) { + EngramStore* g = engram_get(); + uint32_t maxid = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].layer_id > maxid) maxid = g->layers[i].layer_id; + } + return maxid + 1; +} + +/* Whether a node in `layer_id` may be silenced by inhibitory edges in pass 2. */ +static int engram_layer_is_suppressible(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 1; /* unknown layer → safe default: standard suppression */ + return L->suppressible ? 1 : 0; +} + +/* Whether a layer is transparent (its content shapes output but is filtered + * from introspection queries). Currently used to mark Layer 0 as invisible + * to "what do you know about yourself" lookups while still letting it + * dominate the prompt context. */ +static int engram_layer_is_transparent(uint32_t layer_id) { + EngramLayer* L = engram_find_layer(layer_id); + if (!L) return 0; + return L->transparent ? 1 : 0; +} + +static int64_t engram_now_ms(void) { + struct timeval tv; gettimeofday(&tv, NULL); + return (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; +} + +static EngramNode* engram_find_node(const char* id) { + if (!id) return NULL; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return &g->nodes[i]; + } + return NULL; +} + +static int64_t engram_find_node_index(const char* id) { + if (!id) return -1; + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].id && strcmp(g->nodes[i].id, id) == 0) return i; + } + return -1; +} + +static void engram_grow_nodes(void) { + EngramStore* g = engram_get(); + if (g->node_count < g->node_capacity) return; + int64_t nc = g->node_capacity * 2; + g->nodes = realloc(g->nodes, (size_t)nc * sizeof(EngramNode)); + if (!g->nodes) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->nodes + g->node_capacity, 0, + (size_t)(nc - g->node_capacity) * sizeof(EngramNode)); + g->node_capacity = nc; +} + +static void engram_grow_edges(void) { + EngramStore* g = engram_get(); + if (g->edge_count < g->edge_capacity) return; + int64_t nc = g->edge_capacity * 2; + g->edges = realloc(g->edges, (size_t)nc * sizeof(EngramEdge)); + if (!g->edges) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(g->edges + g->edge_capacity, 0, + (size_t)(nc - g->edge_capacity) * sizeof(EngramEdge)); + g->edge_capacity = nc; +} + +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +static char* engram_new_id(void) { + el_val_t v = uuid_new(); + const char* s = EL_CSTR(v); + return el_strdup(s ? s : ""); +} + +/* Convert a node into an ElMap of its fields. */ +static el_val_t engram_node_to_map(const EngramNode* n) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(n->id ? n->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("content")), EL_STR(el_strdup(n->content ? n->content : ""))); + m = el_map_set(m, EL_STR(el_strdup("node_type")), EL_STR(el_strdup(n->node_type ? n->node_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("label")), EL_STR(el_strdup(n->label ? n->label : ""))); + m = el_map_set(m, EL_STR(el_strdup("tier")), EL_STR(el_strdup(n->tier ? n->tier : "Working"))); + m = el_map_set(m, EL_STR(el_strdup("tags")), EL_STR(el_strdup(n->tags ? n->tags : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(n->metadata ? n->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("salience")), el_from_float(n->salience)); + m = el_map_set(m, EL_STR(el_strdup("importance")), el_from_float(n->importance)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(n->confidence)); + m = el_map_set(m, EL_STR(el_strdup("temporal_decay_rate")), el_from_float(n->temporal_decay_rate)); + m = el_map_set(m, EL_STR(el_strdup("activation_count")), (el_val_t)n->activation_count); + m = el_map_set(m, EL_STR(el_strdup("last_activated")), (el_val_t)n->last_activated); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)n->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)n->updated_at); + m = el_map_set(m, EL_STR(el_strdup("background_activation")), el_from_float(n->background_activation)); + m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight)); + m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id); + return m; +} + +/* (Node JSON serialization is provided by `engram_emit_node_json` further + * down in the persistence section — reused by the *_json builtins below.) */ +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n); +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e); + +/* Salience may arrive either as a float bit-pattern or as a small integer + * (e.g. 1, meaning 1.0). Heuristic: if interpreted as double it's in + * [0.0, 100.0] use it; otherwise treat as int and convert. */ +static double engram_decode_score(el_val_t v) { + double f = el_to_float(v); + if (!isnan(f) && !isinf(f) && f >= 0.0 && f <= 100.0) return f; + int64_t n = (int64_t)v; + return (double)n; +} + +static char* engram_first_n_chars(const char* s, size_t n) { + if (!s) return el_strdup(""); + size_t l = strlen(s); + if (l > n) l = n; + char* out = el_strbuf(l); + memcpy(out, s, l); + out[l] = '\0'; + return out; +} + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = engram_first_n_chars(c, 60); + n->tier = el_strdup("Working"); + n->tags = el_strdup(""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + n->importance = 0.5; + n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* ti = EL_CSTR(tier); + const char* tg = EL_CSTR(tags); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup(ti && *ti ? ti : "Working"); + n->tags = el_strdup(tg ? tg : ""); + n->metadata = el_strdup("{}"); + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(importance); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */ + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* engram_node_layered — like engram_node_full but with explicit layer + * assignment and an additional `status` slot reserved for callers that + * track lifecycle state in metadata. The signature mirrors the public API + * defined in the layered consciousness design doc: + * + * engram_node_layered(content, node_type, label, + * salience, certainty, confidence, + * status, tags, layer_id) + * + * `certainty` is folded into `importance` (it occupies the same axis in + * the existing schema). `status` is recorded under metadata.status; an + * empty status leaves metadata as the default "{}". + * + * If `layer_id` does not resolve to a known layer the call falls back to + * ENGRAM_LAYER_DEFAULT — better to keep the node addressable than to drop + * it because of a stale layer reference. Callers wanting strict validation + * should engram_list_layers first. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + EngramStore* g = engram_get(); + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = engram_new_id(); + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + const char* lb = EL_CSTR(label); + const char* tg = EL_CSTR(tags); + const char* st = EL_CSTR(status); + n->content = el_strdup(c ? c : ""); + n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup("Working"); + n->tags = el_strdup(tg ? tg : ""); + if (st && *st) { + /* Minimal metadata payload: {"status":"..."}. Keep it cheap so + * callers using `status` don't pay JSON parse cost on every read. */ + size_t sl = strlen(st) + 16; + char* meta = el_strbuf(sl); + snprintf(meta, sl, "{\"status\":\"%s\"}", st); + n->metadata = meta; + } else { + n->metadata = el_strdup("{}"); + } + n->salience = engram_decode_score(salience); + n->importance = engram_decode_score(certainty); + n->confidence = engram_decode_score(confidence); + if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; + if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5; + if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0; + n->temporal_decay_rate = 0.0; + n->activation_count = 0; + int64_t now = engram_now_ms(); + n->last_activated = now; + n->created_at = now; + n->updated_at = now; + /* Resolve layer assignment. Caller passes either a numeric layer_id or + * a stringified id; el_to_float / int cast tolerates both. */ + int64_t lid = (int64_t)layer_id; + if (lid < 0) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; + n->layer_id = (uint32_t)lid; + g->node_count++; + return el_wrap_str(el_strdup(n->id)); +} + +/* ── Layer registry public API ────────────────────────────────────────────── + * + * The five canonical layers are seeded at engram_get() initialization. + * Runtime code (typically imprint/suit injection logic at the EL level) + * can extend the registry with engram_add_layer() — only layers marked + * `injectable=1` may be removed via engram_remove_layer(). Removing a + * layer leaves a tombstone slot so existing layer_id references on nodes + * stay valid; orphaned references resolve to "unknown layer" and inherit + * the default suppression behavior. + */ + +/* engram_add_layer — register a new layer at runtime. + * Returns the assigned layer_id as an el_val_t int (cast back via int64_t). + * Conflicting names are rejected (returns 0). */ +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + EngramStore* g = engram_get(); + const char* nm = EL_CSTR(name); + if (!nm || !*nm) return (el_val_t)0; + if (engram_find_layer_by_name(nm)) { + /* Name collision — return existing id so callers are idempotent. */ + return (el_val_t)(int64_t)engram_find_layer_by_name(nm)->layer_id; + } + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count++]; + L->layer_id = engram_next_layer_id(); + L->name = el_strdup_persist(nm); + L->activation_priority = (uint32_t)(int64_t)priority; + L->suppressible = (int)(int64_t)suppressible ? 1 : 0; + L->transparent = (int)(int64_t)transparent ? 1 : 0; + L->injectable = (int)(int64_t)injectable ? 1 : 0; + return (el_val_t)(int64_t)L->layer_id; +} + +/* engram_remove_layer — remove an injectable layer by id. + * Built-in (non-injectable) layers cannot be removed. Nodes still tagged + * with the removed layer's id keep their tag but resolve to "unknown + * layer" thereafter and inherit standard (suppressible) behavior. + * Returns 1 on success, 0 on failure (unknown id, non-injectable). */ +el_val_t engram_remove_layer(el_val_t layer_id) { + EngramStore* g = engram_get(); + int64_t lid = (int64_t)layer_id; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if ((int64_t)L->layer_id != lid) continue; + if (!L->injectable) return (el_val_t)0; + free(L->name); + L->name = NULL; /* tombstone */ + /* Leave layer_id, priority, flags intact so debug snapshots can + * still distinguish "removed at runtime" from "never existed". */ + return (el_val_t)1; + } + return (el_val_t)0; +} + +/* engram_list_layers — enumerate the active layer registry. + * Returns an ElList of maps, one per non-tombstone layer, sorted by + * activation_priority ascending (deepest layer first). */ +el_val_t engram_list_layers(void) { + EngramStore* g = engram_get(); + el_val_t lst = el_list_empty(); + if (g->layer_count == 0) return lst; + /* Build an index sorted by activation_priority ascending. */ + size_t* idx = malloc(g->layer_count * sizeof(size_t)); + if (!idx) return lst; + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + /* Insertion sort — N is small (≤ a few dozen layers). */ + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), + (el_val_t)(int64_t)L->layer_id); + m = el_map_set(m, EL_STR(el_strdup("name")), + EL_STR(el_strdup(L->name ? L->name : ""))); + m = el_map_set(m, EL_STR(el_strdup("activation_priority")), + (el_val_t)(int64_t)L->activation_priority); + m = el_map_set(m, EL_STR(el_strdup("suppressible")), + (el_val_t)(int64_t)(L->suppressible ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("transparent")), + (el_val_t)(int64_t)(L->transparent ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("injectable")), + (el_val_t)(int64_t)(L->injectable ? 1 : 0)); + lst = el_list_append(lst, m); + } + free(idx); + return lst; +} + +el_val_t engram_get_node(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_map_new(0); + return engram_node_to_map(n); +} + +void engram_strengthen(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + EngramNode* n = engram_find_node(sid); + if (!n) return; + n->salience += 0.05; + if (n->salience > 1.0) n->salience = 1.0; + n->activation_count++; + n->last_activated = engram_now_ms(); + n->updated_at = n->last_activated; +} + +void engram_forget(el_val_t node_id) { + const char* sid = EL_CSTR(node_id); + if (!sid) return; + EngramStore* g = engram_get(); + int64_t idx = engram_find_node_index(sid); + if (idx < 0) return; + /* Free node strings */ + EngramNode* n = &g->nodes[idx]; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down */ + for (int64_t i = idx + 1; i < g->node_count; i++) { + g->nodes[i - 1] = g->nodes[i]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + /* Remove all incident edges */ + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int incident = (e->from_id && strcmp(e->from_id, sid) == 0) || + (e->to_id && strcmp(e->to_id, sid) == 0); + if (incident) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; +} + +el_val_t engram_node_count(void) { + return (el_val_t)engram_get()->node_count; +} + +static int istr_contains(const char* hay, const char* needle) { + if (!hay || !needle || !*needle) return 0; + size_t nl = strlen(needle); + for (const char* p = hay; *p; p++) { + if (strncasecmp(p, needle, nl) == 0) return 1; + } + return 0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + el_val_t lst = el_list_empty(); + if (!q || !*q) return lst; + int64_t found = 0; + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers: nodes whose layer is `transparent=1` + * shape output but are invisible to introspection ("what do you + * know about yourself"). They still surface via engram_activate + * + engram_compile_layered_json — that's the legitimate path. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + lst = el_list_append(lst, engram_node_to_map(n)); + found++; + } + } + return lst; +} + +/* Sort node indices by salience desc (small N, insertion sort is fine). */ +static void engram_sort_indices_by_salience(int64_t* arr, int64_t n, + const EngramNode* nodes) { + for (int64_t i = 1; i < n; i++) { + int64_t key = arr[i]; + double ks = nodes[key].salience; + int64_t j = i - 1; + while (j >= 0 && nodes[arr[j]].salience < ks) { + arr[j + 1] = arr[j]; + j--; + } + arr[j + 1] = key; + } +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + el_val_t lst = el_list_empty(); + if (g->node_count == 0) return lst; + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return lst; + /* Skip transparent layers — same introspection-filter rationale as + * engram_search above. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + for (int64_t i = off; i < end; i++) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[idx[i]])); + } + free(idx); + return lst; +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f || !t) return; + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(f); + e->to_id = el_strdup(t); + e->relation = el_strdup(r && *r ? r : "associate"); + e->metadata = el_strdup("{}"); + e->weight = engram_decode_score(weight); + if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; + e->updated_at = now; + e->last_fired = 0; + e->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + EngramStore* g = engram_get(); + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + if (!f || !t) return 0; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, f) == 0 && strcmp(e->to_id, t) == 0) return 1; + } + return 0; +} + +/* Reserved helper: edge -> ElMap. Kept around for future builtins. */ +static el_val_t engram_edge_to_map(const EngramEdge* e) __attribute__((unused)); +static el_val_t engram_edge_to_map(const EngramEdge* e) { + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("id")), EL_STR(el_strdup(e->id ? e->id : ""))); + m = el_map_set(m, EL_STR(el_strdup("from_id")), EL_STR(el_strdup(e->from_id ? e->from_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("to_id")), EL_STR(el_strdup(e->to_id ? e->to_id : ""))); + m = el_map_set(m, EL_STR(el_strdup("relation")), EL_STR(el_strdup(e->relation ? e->relation : ""))); + m = el_map_set(m, EL_STR(el_strdup("metadata")), EL_STR(el_strdup(e->metadata ? e->metadata : "{}"))); + m = el_map_set(m, EL_STR(el_strdup("weight")), el_from_float(e->weight)); + m = el_map_set(m, EL_STR(el_strdup("confidence")), el_from_float(e->confidence)); + m = el_map_set(m, EL_STR(el_strdup("created_at")), (el_val_t)e->created_at); + m = el_map_set(m, EL_STR(el_strdup("updated_at")), (el_val_t)e->updated_at); + m = el_map_set(m, EL_STR(el_strdup("last_fired")), (el_val_t)e->last_fired); + m = el_map_set(m, EL_STR(el_strdup("inhibitory")), (el_val_t)(e->inhibitory ? 1 : 0)); + m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)e->layer_id); + return m; +} + +el_val_t engram_neighbors(el_val_t node_id) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + el_val_t lst = el_list_empty(); + if (!sid) return lst; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, sid) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, sid) == 0) other = e->from_id; + if (!other) continue; + EngramNode* n = engram_find_node(other); + if (n) lst = el_list_append(lst, engram_node_to_map(n)); + } + return lst; +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t md = (int64_t)max_depth; if (md <= 0) md = 1; + const char* dir = EL_CSTR(direction); /* "out" | "in" | "both" (default) */ + el_val_t lst = el_list_empty(); + if (!sid || g->node_count == 0) return lst; + int64_t start = engram_find_node_index(sid); + if (start < 0) return lst; + /* BFS with depth tracking */ + int64_t* visited = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* queue = calloc((size_t)g->node_count, sizeof(int64_t)); + int64_t* depths = calloc((size_t)g->node_count, sizeof(int64_t)); + if (!visited || !queue || !depths) { + free(visited); free(queue); free(depths); return lst; + } + int64_t qh = 0, qt = 0; + queue[qt++] = start; + visited[start] = 1; + depths[start] = 0; + while (qh < qt) { + int64_t cur = queue[qh++]; + const char* cur_id = g->nodes[cur].id; + int64_t cur_depth = depths[cur]; + if (cur_depth >= md) continue; + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* other = NULL; + int outgoing = e->from_id && strcmp(e->from_id, cur_id) == 0; + int incoming = e->to_id && strcmp(e->to_id, cur_id) == 0; + if (dir && strcmp(dir, "out") == 0 && !outgoing) continue; + if (dir && strcmp(dir, "in") == 0 && !incoming) continue; + if (outgoing) other = e->to_id; + else if (incoming) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0 || visited[oi]) continue; + visited[oi] = 1; + depths[oi] = cur_depth + 1; + queue[qt++] = oi; + } + } + /* Emit all visited except the seed */ + for (int64_t i = 0; i < g->node_count; i++) { + if (visited[i] && i != start) { + lst = el_list_append(lst, engram_node_to_map(&g->nodes[i])); + } + } + free(visited); free(queue); free(depths); + return lst; +} + +el_val_t engram_edge_count(void) { + return (el_val_t)engram_get()->edge_count; +} + +/* Compute temporal decay factor for a node given current time. + * effective contribution = salience * exp(-lambda * age_hours / T_half) + * Clamped to [0.05, 1.0] so very old nodes retain a meaningful floor. */ +static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) { + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) return 1.0; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + if (factor < 0.05) factor = 0.05; + return factor; +} + +/* Activation dampening: high activation_count nodes are "well-known" context + * and get less marginal boost per firing. + * count=0 → 1.0, count=2 → ~0.74, count=9 → ~0.59, count=99 → ~0.43 */ +static double engram_activation_dampen(const EngramNode* n) { + return 1.0 / (1.0 + log(1.0 + (double)n->activation_count)); +} + +/* Temporal proximity bonus: boost propagation along edges connecting + * co-temporal nodes. Returns a multiplier bonus in [0, 0.2]. */ +static double engram_temporal_proximity_bonus(int64_t node_created, + int64_t seed_epoch) { + int64_t diff = node_created - seed_epoch; + if (diff < 0) diff = -diff; + if (diff < 86400000LL) return 0.20; /* within 1 day */ + if (diff < 604800000LL) return 0.10; /* within 7 days */ + return 0.0; +} + +/* ── Two-layer activation (biologically-motivated) ─────────────────────────── + * + * Layer 1 — Broad fan-out (background activation): + * BFS + spreading activation fires on ALL nodes reachable from seeds, + * regardless of relevance to the current goal. Every reachable node gets + * a background_activation score. Nothing is filtered here. Models the + * brain's massive parallel sub-threshold activation of all associated + * content in response to a stimulus. Temporal decay and activation + * dampening are applied at this layer (as before), but no threshold gate. + * + * Layer 2 — Executive filter (working memory promotion): + * A second pass asks: given the query (goal intent), attentional bias, + * and inhibitory edge topology — which background-activated nodes should + * break through into working memory? + * + * wm_weight = bg_activation * goal_bias(node, query) * confidence + * * inhibitory_suppression_factor + * + * Only nodes where wm_weight >= ENGRAM_WM_THRESHOLD are promoted to + * working memory (working_memory_weight > 0). Background-activated nodes + * that don't cross the threshold accumulate suppression_count. After + * ENGRAM_SUPPRESSION_BREAKTHROUGH consecutive suppressed turns, the node + * force-breaks through at ENGRAM_BREAKTHROUGH_WEIGHT (latent tension + * surfacing — models intrusive memory / unresolved cognitive load). + * + * Inhibitory edges: + * An edge with inhibitory=1 suppresses the TARGET node's working memory + * promotion when the SOURCE is background-activated. Background activation + * of the target is NOT affected — the node fires in layer 1. Only the + * executive filter (layer 2) is gated. Models attentional inhibition: + * "focused on code work" suppresses personal memories from surfacing + * even if they have high background_activation. + * + * Goal bias: + * A lightweight heuristic rates how well each background-activated node + * aligns with the apparent intent of the current query. Technical queries + * boost Belief/Canonical/Lesson nodes; relational queries boost Memory/ + * Entity nodes. Direct lexical overlap gives a 50% bonus. + * + * Working memory persistence (turn continuity): + * Nodes promoted in the previous turn retain a decayed working_memory_weight + * (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models + * conversational thread continuity — once a topic is in working memory, + * it persists slightly into the next turn. + * + * Returns ElList of {node, activation_strength, working_memory_weight, + * epistemic_confidence, hops, promoted}. + * "promoted" = 1 if working_memory_weight > 0, 0 if background-only. + * Context compilation uses ONLY nodes with promoted=1. + * + * Temporal decay (preserved from prior implementation): + * effective_salience = salience * exp(-lambda * age_hours / T_half) + * where T_half = 168 h (one week), lambda = ln(2) + * + * Activation dampening (preserved): + * dampen = 1 / (1 + log(1 + activation_count)) + * + * Temporal proximity bonus (preserved): + * edge_strength *= (1 + tbonus) where tbonus ∈ {0, 0.10, 0.20} + * + * Per-type threshold gates apply only to working memory promotion (layer 2): + * Safety/DharmaSelf: 0.05 Canonical: 0.15 Lesson: 0.25 + * Belief/Entity: 0.30 Note/Memory/Working: 0.40 + */ + +/* Compute goal-state bias multiplier for a node given the query. + * Returns a value in [0.3, 2.0]. This is a lightweight heuristic — + * a production implementation may use LLM-derived intent classification. */ +static double engram_goal_bias(const EngramNode* n, const char* query) { + if (!query || !*query) return 1.0; + double bias = 1.0; + /* Direct lexical overlap: node content/label/tags share text with query. */ + if (istr_contains(n->content, query) || istr_contains(n->label, query) || + istr_contains(n->tags, query)) { + bias += 0.5; + } + /* Node-type resonance with query intent. */ + int technical_query = istr_contains(query, "code") || + istr_contains(query, "function") || + istr_contains(query, "implement") || + istr_contains(query, "error") || + istr_contains(query, "bug") || + istr_contains(query, "build") || + istr_contains(query, "system") || + istr_contains(query, "design") || + istr_contains(query, "architecture"); + int personal_query = istr_contains(query, "feel") || + istr_contains(query, "emotion") || + istr_contains(query, "remember") || + istr_contains(query, "personal") || + istr_contains(query, "story") || + istr_contains(query, "relationship"); + if (n->node_type) { + int is_knowledge = (strcmp(n->node_type, "Belief") == 0) || + (strcmp(n->node_type, "DharmaSelf") == 0) || + (strcmp(n->node_type, "Safety") == 0); + int is_personal = (strcmp(n->node_type, "Memory") == 0) || + (strcmp(n->node_type, "Entity") == 0); + if (technical_query && is_knowledge) bias += 0.3; + if (technical_query && is_personal) bias -= 0.3; + if (personal_query && is_personal) bias += 0.3; + if (personal_query && is_knowledge) bias -= 0.1; + } + /* Tier-based bonus: promote higher-confidence knowledge nodes. */ + if (n->tier) { + if (strcmp(n->tier, "Canonical") == 0) bias += 0.2; + if (strcmp(n->tier, "Lesson") == 0) bias += 0.1; + } + if (bias < 0.3) bias = 0.3; + if (bias > 2.0) bias = 2.0; + return bias; +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; + el_val_t out = el_list_empty(); + if (!q || g->node_count == 0) return out; + + int64_t now_ms = engram_now_ms(); + + /* Per-node layer-1 tracking. */ + double* best_bg = calloc((size_t)g->node_count, sizeof(double)); + int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t)); + int* reached = calloc((size_t)g->node_count, sizeof(int)); + if (!best_bg || !best_hops || !reached) { + free(best_bg); free(best_hops); free(reached); return out; + } + + /* ── LAYER 1: broad fan-out (background activation) ───────────────── + * Find seeds, apply temporal decay + dampening, BFS with edge weights. + * Inhibitory edges propagate activation normally at this layer — they + * only gate working memory promotion in layer 2. */ + typedef struct { int64_t idx; double act; int64_t created_at; } SeedEntry; + SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry)); + int64_t seed_count = 0; + if (!seeds) { + free(best_bg); free(best_hops); free(reached); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + double tdecay = engram_temporal_decay(n, now_ms); + double dampen = engram_activation_dampen(n); + double act = n->salience * tdecay * dampen; + seeds[seed_count].idx = i; + seeds[seed_count].act = act; + seeds[seed_count].created_at = n->created_at; + seed_count++; + best_bg[i] = act; + best_hops[i] = 0; + reached[i] = 1; + } + } + /* Compute mean seed created_at for temporal proximity bonus. */ + int64_t seed_epoch = 0; + if (seed_count > 0) { + seed_epoch = seeds[0].created_at; + for (int64_t s = 1; s < seed_count; s++) + seed_epoch = (seed_epoch + seeds[s].created_at) / 2; + } + typedef struct { int64_t idx; int64_t hops; double act; } Frontier; + Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier)); + if (!fr) { + free(best_bg); free(best_hops); free(reached); free(seeds); return out; + } + int64_t fhead = 0, ftail = 0; + int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); + for (int64_t s = 0; s < seed_count; s++) { + if (ftail >= fcap) break; + fr[ftail].idx = seeds[s].idx; + fr[ftail].hops = 0; + fr[ftail].act = seeds[s].act; + ftail++; + } + const double SPREAD_DECAY = 0.7; + while (fhead < ftail) { + Frontier f = fr[fhead++]; + if (f.hops >= max_depth) continue; + const char* cur_id = g->nodes[f.idx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + int64_t new_hops = f.hops + 1; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = new_hops; + reached[oi] = 1; + if (ftail < fcap) { + fr[ftail].idx = oi; + fr[ftail].hops = new_hops; + fr[ftail].act = new_act; + ftail++; + } + } + } + } + /* Persist layer-1 background_activation to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; + } + + /* ── TRAVERSAL INFERENCE: infer A→C edges when A→B→C was traversed ── + * For each pair of edges (A→B, B→C) where all three nodes were reached, + * create an inferred A→C edge with weight = w(A→B) * w(B→C) * 0.8 + * if no A→C edge already exists. Cap at 64 new edges per call. + * + * IMPORTANT: collect candidates FIRST into a flat array (no pointers into + * g->edges held across the apply pass), then apply after — this avoids + * dangling pointer bugs if engram_grow_edges() reallocs the array. */ + { + const int64_t INFER_CAP = 64; + typedef struct { char from[64]; char to[64]; double weight; } InferCandidate; + InferCandidate* cands = malloc((size_t)INFER_CAP * sizeof(InferCandidate)); + int64_t ncands = 0; + int64_t snap_ec = g->edge_count; + if (cands) { + for (int64_t e1 = 0; e1 < snap_ec && ncands < INFER_CAP; e1++) { + EngramEdge* ea = &g->edges[e1]; + if (!ea->from_id || !ea->to_id) continue; + int64_t ai = engram_find_node_index(ea->from_id); + int64_t bi = engram_find_node_index(ea->to_id); + if (ai < 0 || bi < 0) continue; + if (!reached[ai] || !reached[bi]) continue; + for (int64_t e2 = 0; e2 < snap_ec && ncands < INFER_CAP; e2++) { + if (e2 == e1) continue; + EngramEdge* eb = &g->edges[e2]; + if (!eb->from_id || !eb->to_id) continue; + if (strcmp(eb->from_id, ea->to_id) != 0) continue; + int64_t ci = engram_find_node_index(eb->to_id); + if (ci < 0 || !reached[ci]) continue; + if (ai == ci) continue; + int already = 0; + for (int64_t ex = 0; ex < snap_ec; ex++) { + EngramEdge* ee = &g->edges[ex]; + if (ee->from_id && ee->to_id && + strcmp(ee->from_id, ea->from_id) == 0 && + strcmp(ee->to_id, eb->to_id) == 0) { + already = 1; break; + } + } + if (already) continue; + int dup = 0; + for (int64_t k = 0; k < ncands; k++) { + if (strcmp(cands[k].from, ea->from_id) == 0 && + strcmp(cands[k].to, eb->to_id) == 0) { dup = 1; break; } + } + if (dup) continue; + double inf_w = ea->weight * eb->weight * 0.8; + if (inf_w < 0.05) inf_w = 0.05; + if (inf_w > 1.0) inf_w = 1.0; + strncpy(cands[ncands].from, ea->from_id, 63); cands[ncands].from[63] = '\0'; + strncpy(cands[ncands].to, eb->to_id, 63); cands[ncands].to[63] = '\0'; + cands[ncands].weight = inf_w; + ncands++; + } + } + for (int64_t k = 0; k < ncands; k++) { + engram_grow_edges(); + EngramEdge* ne = &g->edges[g->edge_count]; + memset(ne, 0, sizeof(*ne)); + ne->id = engram_new_id(); + /* Use strdup (not el_strdup) so these persist beyond the request. */ + ne->from_id = strdup(cands[k].from); + ne->to_id = strdup(cands[k].to); + ne->relation = strdup("inferred"); + ne->metadata = strdup("{}"); + ne->weight = cands[k].weight; + ne->confidence = 0.8; + ne->created_at = now_ms; + ne->updated_at = now_ms; + ne->last_fired = now_ms; + ne->layer_id = ENGRAM_LAYER_DEFAULT; + g->edge_count++; + } + free(cands); + } + } + + /* ── PASS 2: executive filter → working memory promotion ──────────── */ + /* Step A: collect inhibitory suppressions from fired inhibitory edges. + * Layered consciousness: inhibition is ONLY recorded against targets + * whose layer is `suppressible == 1`. Nodes in non-suppressible layers + * (Layer 0 / safety) ignore inhibitory edges entirely — their working + * memory weight cannot be silenced by attentional suppression. */ + double* inhibition = calloc((size_t)g->node_count, sizeof(double)); + if (!inhibition) { + free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); + return out; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->inhibitory) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (!reached[src] || best_bg[src] <= 0.0) continue; + /* Skip if target layer is non-suppressible: Layer 0 / safety nodes + * are immune to inhibitory edges from any source. The pass-3 + * override below also force-promotes them, but recording inhibition + * against them at all would be wasted work and could confuse + * downstream debugging output. */ + if (!engram_layer_is_suppressible(g->nodes[tgt].layer_id)) continue; + /* Inhibition strength proportional to source background activation + * and edge weight. Takes the maximum if multiple inhibitory edges + * target the same node. */ + double inh = best_bg[src] * e->weight; + if (inh > inhibition[tgt]) inhibition[tgt] = inh; + } + /* Step B: compute working_memory_weight per candidate node. */ + double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); + if (!wm_weights) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + /* Per-type threshold: safety nodes break through more easily. */ + double type_threshold = engram_type_threshold(n->node_type, n->tier); + /* Goal bias weights the node's relevance to current intent. */ + double bias = engram_goal_bias(n, q); + /* Raw working memory score. */ + double raw_wm = best_bg[i] * bias * n->confidence; + /* Apply inhibitory suppression. Full inhibition → scale by factor. */ + double inh = inhibition[i]; + if (inh > 1.0) inh = 1.0; + double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh; + raw_wm *= suppress; + /* Threshold gate: must exceed per-type threshold to enter working + * memory. Type threshold replaces the old flat 0.2 filter. */ + if (raw_wm >= type_threshold) { + wm_weights[i] = raw_wm > 1.0 ? 1.0 : raw_wm; + if (n->suppression_count > 0) n->suppression_count = 0; + } else { + /* Node didn't make it through — increment suppression counter. + * After N consecutive suppressions: force breakthrough. */ + n->suppression_count++; + if (n->suppression_count >= ENGRAM_SUPPRESSION_BREAKTHROUGH) { + wm_weights[i] = ENGRAM_BREAKTHROUGH_WEIGHT; + n->suppression_count = 0; + } else { + wm_weights[i] = 0.0; + } + } + } + /* ── PASS 3: Layer 0 override (the sacred fire) ───────────────────── + * Every node in a non-suppressible layer that received any background + * activation is force-promoted to AT LEAST ENGRAM_LAYER0_OVERRIDE_WEIGHT. + * This runs LAST and overrides whatever Pass 2 decided — Layer 0 cannot + * be silenced by inhibitory edges, by goal-bias misalignment, by + * confidence weighting, or by per-type threshold gates. If the seed + * fan-out reached a structural-floor node, that node surfaces. + * + * Note: this also clears the suppression_count when an override fires, + * since the node DID surface this turn — it just took the override path + * rather than the standard threshold path. Without this, a Layer 0 + * node with persistent inhibitory pressure would accumulate + * suppression_count forever and never reach the breakthrough state. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i] || best_bg[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (wm_weights[i] < ENGRAM_LAYER0_OVERRIDE_WEIGHT) { + wm_weights[i] = ENGRAM_LAYER0_OVERRIDE_WEIGHT; + } + n->suppression_count = 0; + } + + /* Persist working_memory_weight (post Pass 3) to node store. */ + for (int64_t i = 0; i < g->node_count; i++) { + g->nodes[i].working_memory_weight = wm_weights[i]; + } + + /* ── HEBBIAN STRENGTHENING: fire together, wire together ───────────── + * For each pair of co-promoted nodes (working_memory_weight > 0) that + * share an edge, boost that edge's weight by 0.05 (capped at 1.0). + * Also increment activation_count and update last_activated on promoted + * nodes — this is what drives tier migration below. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; + EngramNode* n = &g->nodes[i]; + n->activation_count++; + n->last_activated = now_ms; + n->updated_at = now_ms; + } + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + if (!e->from_id || !e->to_id) continue; + int64_t src = engram_find_node_index(e->from_id); + int64_t tgt = engram_find_node_index(e->to_id); + if (src < 0 || tgt < 0) continue; + if (wm_weights[src] > 0.0 && wm_weights[tgt] > 0.0) { + e->weight += 0.05; + if (e->weight > 1.0) e->weight = 1.0; + e->last_fired = now_ms; + e->updated_at = now_ms; + } + } + + /* ── TIER MIGRATION: promote nodes based on activation_count thresholds ─ + * 0–4 → Working + * 5–19 → Episodic + * 20–49 → Semantic + * 50+ → Procedural + * Only upgrade (never downgrade) to preserve earned tier. */ + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + const char* target_tier = NULL; + int64_t ac = n->activation_count; + if (ac >= 50) target_tier = "Procedural"; + else if (ac >= 20) target_tier = "Semantic"; + else if (ac >= 5) target_tier = "Episodic"; + else target_tier = "Working"; + if (target_tier && n->tier && strcmp(n->tier, target_tier) != 0) { + /* Only upgrade (Working < Episodic < Semantic < Procedural). */ + int cur_rank = 0, new_rank = 0; + if (strcmp(n->tier, "Working") == 0) cur_rank = 0; + else if (strcmp(n->tier, "Episodic") == 0) cur_rank = 1; + else if (strcmp(n->tier, "Semantic") == 0) cur_rank = 2; + else if (strcmp(n->tier, "Procedural") == 0) cur_rank = 3; + if (strcmp(target_tier, "Working") == 0) new_rank = 0; + else if (strcmp(target_tier, "Episodic") == 0) new_rank = 1; + else if (strcmp(target_tier, "Semantic") == 0) new_rank = 2; + else if (strcmp(target_tier, "Procedural") == 0) new_rank = 3; + if (new_rank > cur_rank) { + free(n->tier); + /* Use strdup (not el_strdup) so tier string persists beyond the request. */ + n->tier = strdup(target_tier); + n->updated_at = now_ms; + } + } + } + + /* ── Collect all background-activated nodes for the return value ──── + * Callers see both layers. Context compilation uses only promoted nodes + * (working_memory_weight > 0). Sort: promoted first by wm_weight desc, + * then background-only by background_activation desc. */ + typedef struct { int64_t idx; double bg; double wm; double epist; int64_t hops; } Result; + Result* results = malloc((size_t)g->node_count * sizeof(Result)); + int64_t rcount = 0; + if (!results) { + free(best_bg); free(best_hops); free(reached); free(seeds); + free(fr); free(inhibition); free(wm_weights); return out; + } + for (int64_t i = 0; i < g->node_count; i++) { + if (!reached[i]) continue; + double epist = best_bg[i] * g->nodes[i].confidence; + /* Include if promoted to working memory OR if background activation + * is meaningful enough to report (epist >= 0.1). */ + if (epist < 0.1 && wm_weights[i] <= 0.0) continue; + results[rcount].idx = i; + results[rcount].bg = best_bg[i]; + results[rcount].wm = wm_weights[i]; + results[rcount].epist = epist; + results[rcount].hops = best_hops[i]; + rcount++; + } + /* Sort: promoted nodes first (by wm_weight desc), then background-only + * by background_activation desc. */ + for (int64_t i = 1; i < rcount; i++) { + Result key = results[i]; + int64_t j = i - 1; + while (j >= 0 && (results[j].wm < key.wm || + (results[j].wm == key.wm && results[j].bg < key.bg))) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } + for (int64_t i = 0; i < rcount; i++) { + el_val_t entry = el_map_new(0); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + engram_node_to_map(&g->nodes[results[i].idx])); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(results[i].bg)); + entry = el_map_set(entry, EL_STR(el_strdup("working_memory_weight")), + el_from_float(results[i].wm)); + entry = el_map_set(entry, EL_STR(el_strdup("epistemic_confidence")), + el_from_float(results[i].epist)); + entry = el_map_set(entry, EL_STR(el_strdup("hops")), + (el_val_t)results[i].hops); + entry = el_map_set(entry, EL_STR(el_strdup("promoted")), + (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); + out = el_list_append(out, entry); + } + free(best_bg); free(best_hops); free(reached); + free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + return out; +} + +/* ── Engram persistence (JSON snapshot) ─────────────────────────────────── */ + +static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, n->id ? n->id : ""); + jb_puts(b, ",\"content\":"); jb_emit_escaped(b, n->content ? n->content : ""); + jb_puts(b, ",\"node_type\":"); jb_emit_escaped(b, n->node_type ? n->node_type : ""); + jb_puts(b, ",\"label\":"); jb_emit_escaped(b, n->label ? n->label : ""); + jb_puts(b, ",\"tier\":"); jb_emit_escaped(b, n->tier ? n->tier : "Working"); + jb_puts(b, ",\"tags\":"); jb_emit_escaped(b, n->tags ? n->tags : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, n->metadata ? n->metadata : "{}"); + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"salience\":%g", n->salience); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"importance\":%g", n->importance); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", n->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"temporal_decay_rate\":%g", n->temporal_decay_rate); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"activation_count\":%lld", (long long)n->activation_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_activated\":%lld", (long long)n->last_activated); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)n->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)n->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"background_activation\":%g", n->background_activation); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +static void engram_emit_edge_json(JsonBuf* b, const EngramEdge* e) { + jb_putc(b, '{'); + jb_puts(b, "\"id\":"); jb_emit_escaped(b, e->id ? e->id : ""); + jb_puts(b, ",\"from_id\":"); jb_emit_escaped(b, e->from_id ? e->from_id : ""); + jb_puts(b, ",\"to_id\":"); jb_emit_escaped(b, e->to_id ? e->to_id : ""); + jb_puts(b, ",\"relation\":"); jb_emit_escaped(b, e->relation ? e->relation : ""); + jb_puts(b, ",\"metadata\":"); jb_emit_escaped(b, e->metadata ? e->metadata : "{}"); + char tmp[64]; + snprintf(tmp, sizeof(tmp), ",\"weight\":%g", e->weight); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"confidence\":%g", e->confidence); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"created_at\":%lld", (long long)e->created_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"updated_at\":%lld", (long long)e->updated_at); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"last_fired\":%lld", (long long)e->last_fired); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"inhibitory\":%d", e->inhibitory ? 1 : 0); jb_puts(b, tmp); + snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", e->layer_id); jb_puts(b, tmp); + jb_putc(b, '}'); +} + +el_val_t engram_save(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"nodes\":["); + for (int64_t i = 0; i < g->node_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[i]); + } + jb_puts(&b, "],\"edges\":["); + for (int64_t i = 0; i < g->edge_count; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_edge_json(&b, &g->edges[i]); + } + /* Layered consciousness — emit the layer registry under "layers". + * Older readers that don't know about this top-level key will simply + * ignore it (forward compatible). Tombstoned (removed-injectable) + * layers are skipped — they have no name and can't be re-created + * meaningfully on load anyway. */ + jb_puts(&b, "],\"layers\":["); + int first_layer = 1; + for (size_t i = 0; i < g->layer_count; i++) { + EngramLayer* L = &g->layers[i]; + if (!L->name) continue; + if (!first_layer) jb_putc(&b, ','); + first_layer = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); + jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + jb_puts(&b, "]}"); + FILE* f = fopen(p, "wb"); + if (!f) { free(b.buf); return 0; } + size_t w = fwrite(b.buf, 1, b.len, f); + fclose(f); + int ok = (w == b.len); + free(b.buf); + return ok ? 1 : 0; +} + +/* Helper: extract a string field from a JSON object substring. */ +static char* eg_get_str_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p) return el_strdup(""); + if (*p != '"') return el_strdup(""); + JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; + char* out = jp_parse_string_raw(&jp); + if (jp.err) { free(out); return el_strdup(""); } + return out; +} + +static double eg_get_num_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0.0; + return strtod(p, NULL); +} + +static int64_t eg_get_int_field(const char* obj, const char* key) { + const char* p = json_find_key(obj, key); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return strtoll(p, NULL, 10); +} + +/* Iterate the top-level nodes/edges arrays in a saved snapshot. */ +static const char* eg_skip_ws(const char* p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +el_val_t engram_load(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return 0; } + char* data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return 0; } + size_t got = fread(data, 1, (size_t)sz, f); + fclose(f); + data[got] = '\0'; + + /* Reset store */ + EngramStore* g = engram_get(); + for (int64_t i = 0; i < g->node_count; i++) { + free(g->nodes[i].id); free(g->nodes[i].content); free(g->nodes[i].node_type); + free(g->nodes[i].label); free(g->nodes[i].tier); free(g->nodes[i].tags); + free(g->nodes[i].metadata); + } + g->node_count = 0; + for (int64_t i = 0; i < g->edge_count; i++) { + free(g->edges[i].id); free(g->edges[i].from_id); free(g->edges[i].to_id); + free(g->edges[i].relation); free(g->edges[i].metadata); + } + g->edge_count = 0; + + /* Walk nodes array */ + const char* nodes_p = json_find_key(data, "nodes"); + if (nodes_p) { + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == '[') { + nodes_p++; + nodes_p = eg_skip_ws(nodes_p); + while (*nodes_p && *nodes_p != ']') { + if (*nodes_p != '{') { nodes_p++; continue; } + const char* end = json_skip_value(nodes_p); + size_t n = (size_t)(end - nodes_p); + char* obj = malloc(n + 1); + memcpy(obj, nodes_p, n); obj[n] = '\0'; + engram_grow_nodes(); + EngramNode* nn = &g->nodes[g->node_count]; + memset(nn, 0, sizeof(*nn)); + nn->id = eg_get_str_field(obj, "id"); + nn->content = eg_get_str_field(obj, "content"); + nn->node_type = eg_get_str_field(obj, "node_type"); + nn->label = eg_get_str_field(obj, "label"); + nn->tier = eg_get_str_field(obj, "tier"); + nn->tags = eg_get_str_field(obj, "tags"); + nn->metadata = eg_get_str_field(obj, "metadata"); + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); } + nn->salience = eg_get_num_field(obj, "salience"); + nn->importance = eg_get_num_field(obj, "importance"); + nn->confidence = eg_get_num_field(obj, "confidence"); + nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate"); + /* temporal_decay_rate defaults to 0 (use global) if absent in snapshot */ + nn->activation_count = eg_get_int_field(obj, "activation_count"); + nn->last_activated = eg_get_int_field(obj, "last_activated"); + nn->created_at = eg_get_int_field(obj, "created_at"); + nn->updated_at = eg_get_int_field(obj, "updated_at"); + nn->background_activation = eg_get_num_field(obj, "background_activation"); + nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight"); + nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count"); + /* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity) + * for snapshots that predate the layered schema. We can't + * tell "explicit 0" from "missing field" using the helper + * directly, so probe for the key — if absent, fall back. */ + if (json_find_key(obj, "layer_id")) { + nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + nn->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->node_count++; + free(obj); + nodes_p = end; + nodes_p = eg_skip_ws(nodes_p); + if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); } + } + } + } + /* Walk edges array */ + const char* edges_p = json_find_key(data, "edges"); + if (edges_p) { + edges_p = eg_skip_ws(edges_p); + if (*edges_p == '[') { + edges_p++; + edges_p = eg_skip_ws(edges_p); + while (*edges_p && *edges_p != ']') { + if (*edges_p != '{') { edges_p++; continue; } + const char* end = json_skip_value(edges_p); + size_t n = (size_t)(end - edges_p); + char* obj = malloc(n + 1); + memcpy(obj, edges_p, n); obj[n] = '\0'; + engram_grow_edges(); + EngramEdge* ee = &g->edges[g->edge_count]; + memset(ee, 0, sizeof(*ee)); + ee->id = eg_get_str_field(obj, "id"); + ee->from_id = eg_get_str_field(obj, "from_id"); + ee->to_id = eg_get_str_field(obj, "to_id"); + ee->relation = eg_get_str_field(obj, "relation"); + ee->metadata = eg_get_str_field(obj, "metadata"); + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + ee->weight = eg_get_num_field(obj, "weight"); + ee->confidence = eg_get_num_field(obj, "confidence"); + ee->created_at = eg_get_int_field(obj, "created_at"); + ee->updated_at = eg_get_int_field(obj, "updated_at"); + ee->last_fired = eg_get_int_field(obj, "last_fired"); + ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory"); + if (json_find_key(obj, "layer_id")) { + ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + } else { + ee->layer_id = ENGRAM_LAYER_DEFAULT; + } + g->edge_count++; + free(obj); + edges_p = end; + edges_p = eg_skip_ws(edges_p); + if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); } + } + } + } + /* Walk layers array (optional — older snapshots omit this). + * If present we replace the canonical registry entirely; if absent we + * keep whatever the engram_get() init established. */ + const char* layers_p = json_find_key(data, "layers"); + if (layers_p) { + layers_p = eg_skip_ws(layers_p); + if (*layers_p == '[') { + /* Reset existing layer registry. Free strdup'd names; the + * struct array itself can be reused. */ + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) free(g->layers[i].name); + g->layers[i].name = NULL; + } + g->layer_count = 0; + + layers_p++; + layers_p = eg_skip_ws(layers_p); + while (*layers_p && *layers_p != ']') { + if (*layers_p != '{') { layers_p++; continue; } + const char* end = json_skip_value(layers_p); + size_t n = (size_t)(end - layers_p); + char* obj = malloc(n + 1); + memcpy(obj, layers_p, n); obj[n] = '\0'; + if (g->layer_count >= g->layer_capacity) { + size_t nc = g->layer_capacity ? g->layer_capacity * 2 : 16; + EngramLayer* grown = realloc(g->layers, nc * sizeof(EngramLayer)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(grown + g->layer_capacity, 0, + (nc - g->layer_capacity) * sizeof(EngramLayer)); + g->layers = grown; + g->layer_capacity = nc; + } + EngramLayer* L = &g->layers[g->layer_count]; + memset(L, 0, sizeof(*L)); + L->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id"); + L->activation_priority = (uint32_t)eg_get_int_field(obj, "activation_priority"); + L->suppressible = (int)eg_get_int_field(obj, "suppressible") ? 1 : 0; + L->transparent = (int)eg_get_int_field(obj, "transparent") ? 1 : 0; + L->injectable = (int)eg_get_int_field(obj, "injectable") ? 1 : 0; + char* nm = eg_get_str_field(obj, "name"); + if (nm && *nm) { + L->name = el_strdup_persist(nm); + free(nm); + } else { + free(nm); + L->name = el_strdup_persist(""); + } + g->layer_count++; + free(obj); + layers_p = end; + layers_p = eg_skip_ws(layers_p); + if (*layers_p == ',') { layers_p++; layers_p = eg_skip_ws(layers_p); } + } + } + } + free(data); + return 1; +} + +/* ── Engram JSON-string accessors ───────────────────────────────────────── + * These return pre-serialized JSON strings so callers (especially HTTP + * handlers) don't have to round-trip ElList/ElMap through json_stringify + * — which can't reliably distinguish those structures from raw pointers + * due to el_val_t's type erasure. The runtime knows the real C types and + * can serialize directly. */ + +el_val_t engram_get_node_json(el_val_t id) { + const char* sid = EL_CSTR(id); + EngramNode* n = engram_find_node(sid); + if (!n) return el_wrap_str(el_strdup("{}")); + JsonBuf b; jb_init(&b); + engram_emit_node_json(&b, n); + return el_wrap_str(b.buf); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + EngramStore* g = engram_get(); + const char* q = EL_CSTR(query); + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + int64_t found = 0; + if (q && *q) { + for (int64_t i = 0; i < g->node_count && found < lim; i++) { + EngramNode* n = &g->nodes[i]; + /* Filter transparent layers — same as engram_search. */ + if (engram_layer_is_transparent(n->layer_id)) continue; + if (istr_contains(n->content, q) || + istr_contains(n->label, q) || + istr_contains(n->tags, q)) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, n); + first = 0; + found++; + } + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_scan_nodes_by_type_json — filter by node_type before paginating. + * Empty / NULL type_v falls back to the unfiltered scan (existing behaviour). + * Result is JSON array, salience-sorted, transparent layers skipped. */ +el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_t offset) { + const char* type_filter = EL_CSTR(type_v); + if (!type_filter || !*type_filter) { + return engram_scan_nodes_json(limit, offset); + } + EngramStore* g = engram_get(); + int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100; + int64_t off = (int64_t)offset; if (off < 0) off = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + int64_t live = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; + const char* nt = g->nodes[i].node_type; + if (!nt || strcmp(nt, type_filter) != 0) continue; + idx[live++] = i; + } + engram_sort_indices_by_salience(idx, live, g->nodes); + int64_t end = off + lim; + if (end > live) end = live; + int first = 1; + for (int64_t i = off; i < end; i++) { + if (!first) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + first = 0; + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + /* Re-implement here directly so we serialize without going through + * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} + * triples. */ + EngramStore* g = engram_get(); + const char* sid = EL_CSTR(node_id); + int64_t depth = (int64_t)max_depth; if (depth <= 0) depth = 1; + const char* dir = EL_CSTR(direction); if (!dir) dir = "both"; + int allow_out = (strcmp(dir, "out") == 0) || (strcmp(dir, "both") == 0); + int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + + /* Frontier of (node_id, hops). Cap to a sane size. */ + char** frontier = calloc(1024, sizeof(char*)); + int64_t* frontier_h = calloc(1024, sizeof(int64_t)); + int64_t fc = 0; + char** visited = calloc(1024, sizeof(char*)); + int64_t vc = 0; + if (!frontier || !frontier_h || !visited) { + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); return el_wrap_str(b.buf); + } + /* Use plain strdup (not el_strdup) so arena doesn't track these pointers. + * The BFS loop manually frees them below — arena would double-free them. */ + frontier[fc] = strdup(sid); frontier_h[fc] = 0; fc++; + visited[vc++] = strdup(sid); + + int first = 1; + while (fc > 0) { + char* cur = frontier[0]; int64_t h = frontier_h[0]; + for (int64_t k = 1; k < fc; k++) { frontier[k-1] = frontier[k]; frontier_h[k-1] = frontier_h[k]; } + fc--; + if (h >= depth) { free(cur); continue; } + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + const char* peer = NULL; + if (allow_out && e->from_id && strcmp(e->from_id, cur) == 0) peer = e->to_id; + else if (allow_in && e->to_id && strcmp(e->to_id, cur) == 0) peer = e->from_id; + if (!peer) continue; + int seen = 0; + for (int64_t v = 0; v < vc; v++) { + if (strcmp(visited[v], peer) == 0) { seen = 1; break; } + } + if (seen) continue; + EngramNode* n = engram_find_node(peer); + if (!n) continue; + if (!first) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + engram_emit_node_json(&b, n); + jb_puts(&b, ",\"edge\":"); + engram_emit_edge_json(&b, e); + char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1)); + jb_puts(&b, tmp); + first = 0; + if (vc < 1024) visited[vc++] = strdup(peer); + if (fc < 1024 && h + 1 < depth) { frontier[fc] = strdup(peer); frontier_h[fc] = h + 1; fc++; } + } + free(cur); + } + for (int64_t i = 0; i < fc; i++) free(frontier[i]); + for (int64_t i = 0; i < vc; i++) free(visited[i]); + free(frontier); free(frontier_h); free(visited); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_activate_json(el_val_t query, el_val_t depth) { + /* Run two-layer engram_activate and serialize the result list to JSON. + * Each entry includes both activation_strength (layer 1 background) and + * working_memory_weight (layer 2 executive filter), plus promoted flag. + * Callers performing context compilation should filter to promoted=1. */ + el_val_t lst = engram_activate(query, depth); + ElList* arr = (ElList*)(uintptr_t)lst; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + if (arr) { + for (int64_t i = 0; i < arr->length; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t strength_v = el_map_get(arr->elems[i], EL_STR("activation_strength")); + el_val_t wm_v = el_map_get(arr->elems[i], EL_STR("working_memory_weight")); + el_val_t epist_v = el_map_get(arr->elems[i], EL_STR("epistemic_confidence")); + el_val_t hops_v = el_map_get(arr->elems[i], EL_STR("hops")); + el_val_t promoted_v = el_map_get(arr->elems[i], EL_STR("promoted")); + /* Look up underlying EngramNode by id to emit canonical JSON. */ + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (i > 0) jb_putc(&b, ','); + jb_puts(&b, "{\"node\":"); + if (n) { + engram_emit_node_json(&b, n); + } else { + jb_puts(&b, "{}"); + } + char tmp[80]; + snprintf(tmp, sizeof(tmp), ",\"activation_strength\":%g", el_to_float(strength_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", el_to_float(wm_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"epistemic_confidence\":%g", el_to_float(epist_v)); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"hops\":%lld", (long long)(int64_t)hops_v); jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"promoted\":%d}", (int)(int64_t)promoted_v); jb_puts(&b, tmp); + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +el_val_t engram_stats_json(void) { + EngramStore* g = engram_get(); + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}", + (long long)g->node_count, (long long)g->edge_count, g->layer_count); + return el_wrap_str(el_strdup(buf)); +} + +/* engram_list_layers_json — serialized counterpart of engram_list_layers. + * Returns a JSON array, sorted by activation_priority ascending. */ +el_val_t engram_list_layers_json(void) { + EngramStore* g = engram_get(); + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + /* Build a sorted index over live layers. */ + size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + size_t live = 0; + for (size_t i = 0; i < g->layer_count; i++) { + if (g->layers[i].name) idx[live++] = i; + } + for (size_t i = 1; i < live; i++) { + size_t key = idx[i]; + uint32_t kp = g->layers[key].activation_priority; + size_t j = i; + while (j > 0 && g->layers[idx[j - 1]].activation_priority > kp) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + int first = 1; + for (size_t i = 0; i < live; i++) { + EngramLayer* L = &g->layers[idx[i]]; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + char tmp[80]; + snprintf(tmp, sizeof(tmp), "\"layer_id\":%u", L->layer_id); jb_puts(&b, tmp); + jb_puts(&b, ",\"name\":"); + jb_emit_escaped(&b, L->name ? L->name : ""); + snprintf(tmp, sizeof(tmp), ",\"activation_priority\":%u", L->activation_priority); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"suppressible\":%d", L->suppressible ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"transparent\":%d", L->transparent ? 1 : 0); + jb_puts(&b, tmp); + snprintf(tmp, sizeof(tmp), ",\"injectable\":%d", L->injectable ? 1 : 0); + jb_puts(&b, tmp); + jb_putc(&b, '}'); + } + free(idx); + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* engram_compile_layered_json — produce a prompt-ready context block split + * by layer. + * + * Runs the three-pass activation, then partitions promoted nodes by layer + * suppressibility: + * - Non-suppressible (Layer 0 / structural-floor) layers go FIRST under + * the heading "[LAYER 0 — STRUCTURAL]". These are the sacred-fire + * nodes that surfaced via the pass-3 override. + * - All other promoted layers go SECOND under "[ENGRAM CONTEXT]". + * + * Output is a single JSON-string el_val_t: a UTF-8 text block ready to be + * concatenated into a system prompt. Returns "" if no nodes promoted. + * + * Transparent layers (Layer 0) are emitted into the prompt — they shape + * the model's output — but engram_search and friends still hide them from + * introspection-style queries. The split heading lets the LLM weight them + * appropriately without revealing their internal label. + * + * Each emitted line for a node is its raw JSON (matching engram_emit_node_json) + * so downstream JSON parsers can still walk individual records inside the + * formatted block. The block is plain text, not a JSON document — callers + * concatenating it into a prompt should treat it as opaque markdown. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { + EngramStore* g = engram_get(); + /* Run the three-pass activator. We need the persisted node fields, so + * call engram_activate (it writes background_activation and + * working_memory_weight back into the store). */ + (void)engram_activate(intent, depth); + + /* Walk the store and partition by suppressibility. */ + JsonBuf b; jb_init(&b); + int wrote_layer0 = 0; + int wrote_normal = 0; + + /* Sort indices by working_memory_weight descending so the most + * confidently promoted nodes appear first within each section. */ + int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) idx[mc++] = i; + } + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + double kw = g->nodes[key].working_memory_weight; + int64_t j = i; + while (j > 0 && g->nodes[idx[j - 1]].working_memory_weight < kw) { + idx[j] = idx[j - 1]; + j--; + } + idx[j] = key; + } + + /* Section 1: structural floor (non-suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_layer0) { + jb_puts(&b, "[LAYER 0 — STRUCTURAL]\n"); + wrote_layer0 = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + /* Section 2: standard engram context (suppressible layers). */ + for (int64_t i = 0; i < mc; i++) { + EngramNode* n = &g->nodes[idx[i]]; + if (!engram_layer_is_suppressible(n->layer_id)) continue; + if (!wrote_normal) { + if (wrote_layer0) jb_putc(&b, '\n'); + jb_puts(&b, "[ENGRAM CONTEXT]\n"); + wrote_normal = 1; + } + engram_emit_node_json(&b, n); + jb_putc(&b, '\n'); + } + + free(idx); + if (b.len == 0) { + free(b.buf); + return el_wrap_str(el_strdup("")); + } + return el_wrap_str(b.buf); +} + +/* engram_query_range — temporal range query. + * Returns a JSON array of nodes whose created_at OR last_activated falls + * within [start_ms, end_ms], sorted by created_at ascending. + * Enables "what was I working on last Tuesday?" style queries by passing + * unix-millisecond timestamps for the start and end of the target interval. + * Both endpoints are inclusive. Pass 0 for start_ms to mean "beginning of + * time"; pass 0 for end_ms to mean "now". */ +el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { + EngramStore* g = engram_get(); + int64_t start_ms = (int64_t)start_ms_v; + int64_t end_ms = (int64_t)end_ms_v; + if (end_ms <= 0) end_ms = engram_now_ms(); + + /* Collect matching indices. */ + int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); + if (!idx) return el_wrap_str(el_strdup("[]")); + int64_t mc = 0; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + int in_created = (n->created_at >= start_ms && n->created_at <= end_ms); + int in_activated = (n->last_activated >= start_ms && n->last_activated <= end_ms); + if (in_created || in_activated) idx[mc++] = i; + } + /* Sort by created_at ascending (insertion sort — N is small in practice). */ + for (int64_t i = 1; i < mc; i++) { + int64_t key = idx[i]; + int64_t kts = g->nodes[key].created_at; + int64_t j = i - 1; + while (j >= 0 && g->nodes[idx[j]].created_at > kts) { + idx[j + 1] = idx[j]; + j--; + } + idx[j + 1] = key; + } + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + for (int64_t i = 0; i < mc; i++) { + if (i > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, &g->nodes[idx[i]]); + } + jb_putc(&b, ']'); + free(idx); + return el_wrap_str(b.buf); +} + +/* ── engram_apply_decay_json — temporal decay maintenance ──────────────────── + * + * Iterates ALL nodes and applies temporal decay to their stored `salience` + * field based on time elapsed since `last_activated`: + * + * new_salience = current_salience * decay_rate ^ hours_since_activation + * + * where decay_rate defaults to 0.5^(1/168) per hour (half-life one week), + * or the node's own `temporal_decay_rate` if non-zero. + * + * Nodes with temporal_decay_rate == 0 are NOT immune — the global default + * applies. To make a node truly immune, set temporal_decay_rate to a very + * small positive value (e.g. 0.0001). Nodes that are "pinned" can be + * identified by a tier of "Procedural" — those are skipped. + * + * After updating salience, nodes with salience < 0.05 AND tier == "Working" + * are pruned (deleted) unless they have no content (guard against garbage). + * + * Returns a JSON summary: {"updated": N, "pruned": N} */ +el_val_t engram_apply_decay_json(void) { + EngramStore* g = engram_get(); + int64_t now_ms = engram_now_ms(); + int64_t updated = 0, pruned = 0; + + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + /* Skip Procedural nodes (they are "locked in"). */ + if (n->tier && strcmp(n->tier, "Procedural") == 0) continue; + int64_t age_ms = now_ms - n->last_activated; + if (age_ms <= 0) continue; + double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate + : ENGRAM_DECAY_LAMBDA; + double age_hours = (double)age_ms / 3600000.0; + double decay_factor = exp(-lambda * age_hours / ENGRAM_T_HALF_HOURS); + double new_salience = n->salience * decay_factor; + if (new_salience < 0.0) new_salience = 0.0; + if (new_salience != n->salience) { + n->salience = new_salience; + n->updated_at = now_ms; + updated++; + } + } + + /* Prune low-salience Working nodes. Walk backwards to allow in-place + * removal without invalidating indices. */ + for (int64_t i = g->node_count - 1; i >= 0; i--) { + EngramNode* n = &g->nodes[i]; + if (n->salience >= 0.05) continue; + /* Only prune Working tier nodes — higher tiers are protected. */ + if (!n->tier || strcmp(n->tier, "Working") != 0) continue; + /* Guard: skip nodes with no content. */ + if (!n->content || !*n->content) continue; + /* Free node strings. */ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); + /* Shift remaining nodes down. */ + for (int64_t j = i + 1; j < g->node_count; j++) { + g->nodes[j - 1] = g->nodes[j]; + } + g->node_count--; + memset(&g->nodes[g->node_count], 0, sizeof(EngramNode)); + pruned++; + } + + /* Remove dangling edges for pruned nodes (any edge whose endpoint no + * longer exists in the node list). */ + if (pruned > 0) { + int64_t w = 0; + for (int64_t r = 0; r < g->edge_count; r++) { + EngramEdge* e = &g->edges[r]; + int dangling = 0; + if (e->from_id && engram_find_node_index(e->from_id) < 0) dangling = 1; + if (e->to_id && engram_find_node_index(e->to_id) < 0) dangling = 1; + if (dangling) { + free(e->id); free(e->from_id); free(e->to_id); + free(e->relation); free(e->metadata); + } else { + if (w != r) g->edges[w] = g->edges[r]; + w++; + } + } + g->edge_count = w; + } + + char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"updated\":%lld,\"pruned\":%lld}", + (long long)updated, (long long)pruned); + return el_wrap_str(el_strdup(buf)); +} + +#ifdef HAVE_CURL +/* ── DHARMA network ───────────────────────────────────────────────────────── + * Real implementation. Peers are addressed by `dharma_id` — either bare + * (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or + * "@" where is the peer's Engram-exposed daemon. + * + * Channels are logical handles cached per-cgi: `dharma_connect` is + * idempotent and returns "ch:". The channel registry below tracks + * every cgi_id we've connected to and its resolved transport URL. + * + * Relationship weights live in the local Engram graph: edges of type + * "dharma-relation" between a synthetic local node ("dharma:self") and + * synthetic peer nodes ("dharma:peer:"). Hebbian increments + * accumulate in EngramEdge.weight, clamped to [0.0, 1.0]. + * + * Events arrive over HTTP via the application's request handler, which is + * expected to call el_runtime_dharma_event_arrive() when it sees a + * /dharma/event POST. dharma_field() blocks on a per-event-type queue. + */ + +#define DHARMA_DEFAULT_URL "http://localhost:7770" + +/* Channel registry — one entry per known peer. */ +typedef struct DharmaChannel { + char* cgi_id; /* full dharma_id including any @ suffix */ + char* base_id; /* registry-id portion (before @) for relationship lookup */ + char* url; /* resolved transport URL */ + char* channel_id; /* "ch:" */ +} DharmaChannel; + +static DharmaChannel* _dharma_channels = NULL; +static size_t _dharma_channel_count = 0; +static size_t _dharma_channel_cap = 0; +static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER; + +/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */ +typedef struct DharmaEvent { + char* event_type; + char* payload; + char* source; + int64_t timestamp; + struct DharmaEvent* next; +} DharmaEvent; + +static DharmaEvent* _dharma_event_head = NULL; +static DharmaEvent* _dharma_event_tail = NULL; +static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER; + +/* Split "@" → (base_id, url). If no "@", base_id = full, url = default. + * Returned strings are heap-allocated; caller must free. */ +static void dharma_parse_id(const char* full, char** out_base, char** out_url) { + if (!full) full = ""; + const char* at = strchr(full, '@'); + if (at) { + size_t bn = (size_t)(at - full); + char* b = malloc(bn + 1); + memcpy(b, full, bn); b[bn] = '\0'; + *out_base = b; + *out_url = el_strdup(at + 1); + if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); } + } else { + *out_base = el_strdup(full); + *out_url = el_strdup(DHARMA_DEFAULT_URL); + } +} + +/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) { + if (!cgi_id) return NULL; + for (size_t i = 0; i < _dharma_channel_count; i++) { + if (_dharma_channels[i].cgi_id && + strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) { + return &_dharma_channels[i]; + } + } + return NULL; +} + +/* Add a new channel entry. Caller must hold _dharma_channel_mu. */ +static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) { + if (_dharma_channel_count >= _dharma_channel_cap) { + size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8; + _dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel)); + if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + memset(_dharma_channels + _dharma_channel_cap, 0, + (nc - _dharma_channel_cap) * sizeof(DharmaChannel)); + _dharma_channel_cap = nc; + } + DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++]; + char* base = NULL; char* url = NULL; + dharma_parse_id(cgi_id, &base, &url); + ch->cgi_id = el_strdup(cgi_id ? cgi_id : ""); + ch->base_id = base; + ch->url = url; + size_t cn = strlen(ch->cgi_id) + 4; + ch->channel_id = malloc(cn); + snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id); + return ch; +} + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_wrap_str(el_strdup("")); + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(id); + if (!ch) ch = dharma_add_channel_locked(id); + char* out = el_strdup(ch->channel_id); + pthread_mutex_unlock(&_dharma_channel_mu); + return el_wrap_str(out); +} + +/* Build an error JSON body — same shape http_error_json uses. */ +static el_val_t dharma_error_json(const char* msg) { + return http_error_json(msg); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch_id = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) { + return dharma_error_json("invalid channel"); + } + const char* peer_id = ch_id + 3; + /* Look up channel; if unknown (caller fabricated), auto-register. */ + pthread_mutex_lock(&_dharma_channel_mu); + DharmaChannel* ch = dharma_find_channel_locked(peer_id); + if (!ch) ch = dharma_add_channel_locked(peer_id); + char* url = el_strdup(ch->url); + pthread_mutex_unlock(&_dharma_channel_mu); + /* Build /dharma/recv body. */ + const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + char* esc_ch = json_escape_alloc(ch_id); + char* esc_from = json_escape_alloc(from); + char* esc_msg = json_escape_alloc(msg ? msg : ""); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch); + jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from); + jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg); + jb_puts(&b, "\"}"); + free(esc_ch); free(esc_from); free(esc_msg); + size_t ul = strlen(url) + 16; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/recv", url); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); + free(b.buf); free(full_url); free(url); + return resp; +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = ""; + el_val_t out = el_list_empty(); + char* esc_q = json_escape_alloc(q); + JsonBuf body; jb_init(&body); + jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}"); + free(esc_q); + + /* Snapshot the channel list under lock so we can iterate without + * holding the mutex during network I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + char** ids = calloc(n ? n : 1, sizeof(char*)); + char** bases = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) { + urls[i] = el_strdup(_dharma_channels[i].url); + ids[i] = el_strdup(_dharma_channels[i].cgi_id); + bases[i] = el_strdup(_dharma_channels[i].base_id); + } + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/api/activate", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t resp = http_do("POST", full_url, body.buf, h); + curl_slist_free_all(h); + free(full_url); + const char* rs = EL_CSTR(resp); + if (!rs || !*rs) continue; + if (rs[0] == '{' && strstr(rs, "\"error\"")) continue; + + /* Look up relationship weight (attenuation). */ + double rel_weight = 1.0; + { + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]); + EngramStore* g = engram_get(); + for (int64_t k = 0; k < g->edge_count; k++) { + EngramEdge* e = &g->edges[k]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + rel_weight = e->weight; + break; + } + } + } + + /* Iterate the response array. Expect either a top-level array + * or an object whose "results" field is an array. */ + const char* arr = rs; + while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++; + char* arr_owned = NULL; + if (*arr == '{') { + el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results")); + const char* rr = EL_CSTR(r); + if (rr && *rr == '[') { + arr_owned = el_strdup(rr); + arr = arr_owned; + } else { + continue; + } + } + if (*arr != '[') { free(arr_owned); continue; } + const char* p = arr + 1; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + while (*p && *p != ']') { + const char* end = json_skip_value(p); + size_t en = (size_t)(end - p); + char* obj = el_strbuf(en); + memcpy(obj, p, en); obj[en] = '\0'; + + /* Pull activation_strength if present, else 1.0. */ + el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength")); + double act = el_to_float(act_v); + if (!(act > 0.0 && act <= 100.0)) act = 1.0; + double final_act = act * rel_weight; + + el_val_t entry = el_map_new(0); + /* node = the inner JSON if present, else the entire obj. */ + el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node")); + const char* nr = EL_CSTR(node_raw); + entry = el_map_set(entry, EL_STR(el_strdup("node")), + (nr && *nr) ? node_raw : EL_STR(el_strdup(obj))); + entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(ids[i]))); + entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")), + el_from_float(final_act)); + out = el_list_append(out, entry); + free(obj); + p = end; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + } + free(arr_owned); + } + for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); } + free(urls); free(ids); free(bases); + free(body.buf); + return out; +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = ""; + if (!pay) pay = ""; + const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)"; + int64_t ts = engram_now_ms(); + + char* esc_et = json_escape_alloc(et); + char* esc_pay = json_escape_alloc(pay); + char* esc_src = json_escape_alloc(src); + JsonBuf b; jb_init(&b); + jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et); + jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay); + jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src); + jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts); + jb_putc(&b, '}'); + free(esc_et); free(esc_pay); free(esc_src); + + /* Snapshot URLs to avoid holding the channel mutex during I/O. */ + pthread_mutex_lock(&_dharma_channel_mu); + size_t n = _dharma_channel_count; + char** urls = calloc(n ? n : 1, sizeof(char*)); + for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url); + pthread_mutex_unlock(&_dharma_channel_mu); + + for (size_t i = 0; i < n; i++) { + size_t ul = strlen(urls[i]) + 32; + char* full_url = malloc(ul); + snprintf(full_url, ul, "%s/dharma/event", urls[i]); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + el_val_t r = http_do("POST", full_url, b.buf, h); + (void)r; /* fire-and-forget — emit is not synchronous */ + curl_slist_free_all(h); + free(full_url); + } + for (size_t i = 0; i < n; i++) free(urls[i]); + free(urls); + free(b.buf); +} + +void el_runtime_dharma_event_arrive(const char* event_type, const char* payload, + const char* source) { + DharmaEvent* ev = calloc(1, sizeof(DharmaEvent)); + if (!ev) return; + ev->event_type = el_strdup(event_type ? event_type : ""); + ev->payload = el_strdup(payload ? payload : ""); + ev->source = el_strdup(source ? source : ""); + ev->timestamp = engram_now_ms(); + ev->next = NULL; + pthread_mutex_lock(&_dharma_event_mu); + if (_dharma_event_tail) _dharma_event_tail->next = ev; + else _dharma_event_head = ev; + _dharma_event_tail = ev; + pthread_cond_broadcast(&_dharma_event_cv); + pthread_mutex_unlock(&_dharma_event_mu); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = ""; + + /* Compute deadline: now + 30 seconds. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 30; + + DharmaEvent* found = NULL; + pthread_mutex_lock(&_dharma_event_mu); + while (1) { + /* Scan queue for matching type; pop and return first match. */ + DharmaEvent* prev = NULL; + DharmaEvent* cur = _dharma_event_head; + while (cur) { + if (cur->event_type && strcmp(cur->event_type, et) == 0) { + if (prev) prev->next = cur->next; + else _dharma_event_head = cur->next; + if (_dharma_event_tail == cur) _dharma_event_tail = prev; + cur->next = NULL; + found = cur; + break; + } + prev = cur; cur = cur->next; + } + if (found) break; + int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline); + if (rc == ETIMEDOUT) break; + } + pthread_mutex_unlock(&_dharma_event_mu); + + if (!found) return el_map_new(0); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("type")), + EL_STR(el_strdup(found->event_type ? found->event_type : ""))); + m = el_map_set(m, EL_STR(el_strdup("payload")), + EL_STR(el_strdup(found->payload ? found->payload : ""))); + m = el_map_set(m, EL_STR(el_strdup("source_cgi")), + EL_STR(el_strdup(found->source ? found->source : ""))); + m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp); + free(found->event_type); free(found->payload); free(found->source); free(found); + return m; +} + +/* Locate (or create) the local "dharma:self" node and the synthetic peer + * node "dharma:peer:". Returns the index of the dharma-relation + * edge, or -1 if not found. If `create` is non-zero, ensure the nodes + * and edge exist (creating them as needed) and return the edge index. */ +static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) { + if (!peer_base || !*peer_base) return -1; + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + char peer_node[512]; + snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base); + + /* Look for the edge first. */ + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (e->from_id && e->to_id && + strcmp(e->from_id, self_id) == 0 && + strcmp(e->to_id, peer_node) == 0 && + e->relation && strcmp(e->relation, "dharma-relation") == 0) { + return i; + } + } + if (!create) return -1; + + /* Ensure self node exists. We use a fixed id (not engram_new_id) so + * subsequent calls reuse the same one. */ + if (!engram_find_node(self_id)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(self_id); + n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup("DharmaSelf"); + n->label = el_strdup("dharma:self"); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + if (!engram_find_node(peer_node)) { + engram_grow_nodes(); + EngramNode* n = &g->nodes[g->node_count]; + memset(n, 0, sizeof(*n)); + n->id = el_strdup(peer_node); + n->content = el_strdup(peer_base); + n->node_type = el_strdup("DharmaPeer"); + n->label = el_strdup(peer_node); + n->tier = el_strdup("Working"); + n->tags = el_strdup("dharma"); + n->metadata = el_strdup("{}"); + n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; + int64_t now = engram_now_ms(); + n->created_at = now; n->updated_at = now; n->last_activated = now; + n->layer_id = ENGRAM_LAYER_DEFAULT; + g->node_count++; + } + /* Create the edge with weight 0.0 — caller will increment. */ + engram_grow_edges(); + EngramEdge* e = &g->edges[g->edge_count]; + memset(e, 0, sizeof(*e)); + e->id = engram_new_id(); + e->from_id = el_strdup(self_id); + e->to_id = el_strdup(peer_node); + e->relation = el_strdup("dharma-relation"); + e->metadata = el_strdup("{}"); + e->weight = 0.0; + e->confidence = 1.0; + int64_t now = engram_now_ms(); + e->created_at = now; e->updated_at = now; + e->layer_id = ENGRAM_LAYER_DEFAULT; + int64_t idx = g->edge_count; + g->edge_count++; + return idx; +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return; + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 1); + free(base); + if (ei < 0) return; + EngramStore* g = engram_get(); + double inc = engram_decode_score(weight); + if (!(inc >= 0.0)) inc = 0.0; + double w = g->edges[ei].weight + inc; + if (w < 0.0) w = 0.0; + if (w > 1.0) w = 1.0; + g->edges[ei].weight = w; + g->edges[ei].updated_at = engram_now_ms(); + g->edges[ei].last_fired = g->edges[ei].updated_at; +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id || !*id) return el_from_float(0.0); + char* base = NULL; char* url = NULL; + dharma_parse_id(id, &base, &url); + free(url); + int64_t ei = dharma_find_or_create_relation_edge(base, 0); + free(base); + if (ei < 0) return el_from_float(0.0); + EngramStore* g = engram_get(); + return el_from_float(g->edges[ei].weight); +} + +el_val_t dharma_peers(void) { + /* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */ + EngramStore* g = engram_get(); + const char* self_id = "dharma:self"; + typedef struct { char* peer_base; double weight; } PeerEntry; + PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry)); + int64_t pcount = 0; + if (!peers) return el_list_empty(); + for (int64_t i = 0; i < g->edge_count; i++) { + EngramEdge* e = &g->edges[i]; + if (!e->from_id || !e->to_id) continue; + if (strcmp(e->from_id, self_id) != 0) continue; + if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue; + if (e->weight <= 0.0) continue; + const char* prefix = "dharma:peer:"; + size_t pl = strlen(prefix); + if (strncmp(e->to_id, prefix, pl) != 0) continue; + peers[pcount].peer_base = el_strdup(e->to_id + pl); + peers[pcount].weight = e->weight; + pcount++; + } + /* Sort desc by weight. */ + for (int64_t i = 1; i < pcount; i++) { + PeerEntry key = peers[i]; + int64_t j = i - 1; + while (j >= 0 && peers[j].weight < key.weight) { + peers[j + 1] = peers[j]; j--; + } + peers[j + 1] = key; + } + el_val_t out = el_list_empty(); + for (int64_t i = 0; i < pcount; i++) { + out = el_list_append(out, EL_STR(peers[i].peer_base)); + } + free(peers); + return out; +} +#endif /* HAVE_CURL — DHARMA network */ + +/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */ +/* + * All LLM builtins call https://api.anthropic.com/v1/messages with the API + * key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5" + * when the supplied model is empty/null. + * + * `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop. + * Tool handlers are registered with `llm_register_tool(name, fn_name)`, + * which dlsym()s the named symbol. Each tool handler has the C signature + * el_val_t handler(el_val_t input_json); + * and returns a JSON-string el_val_t result. Iteration is capped at 10. + */ + +#ifdef HAVE_CURL +static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5"; +static const char* LLM_API_URL = "https://api.anthropic.com/v1/messages"; +static const char* LLM_VERSION = "2023-06-01"; + +static const char* llm_resolve_model(const char* m) { + if (!m || !*m) return LLM_DEFAULT_MODEL; + return m; +} + +/* + * ── Configurable LLM provider chain ────────────────────────────────────────── + * + * Providers are configured via indexed env vars. The runtime tries each in + * order (0, 1, 2, ...) and returns the first successful non-empty response. + * + * Per provider (N = 0, 1, 2, ...): + * NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended + * if format is "openai" and not already in URL) + * NEURON_LLM_N_KEY — API key + * NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic" + * NEURON_LLM_N_MODEL — model name override (optional) + * + * Example — Neuron inference primary, Anthropic fallback: + * NEURON_LLM_0_URL=https://soma.../v1/chat/completions + * NEURON_LLM_0_KEY=svc-key + * NEURON_LLM_0_FORMAT=openai + * NEURON_LLM_0_MODEL=neuron + * NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages + * NEURON_LLM_1_KEY=sk-ant-... + * NEURON_LLM_1_FORMAT=anthropic + * + * If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY. + */ + +#define LLM_MAX_PROVIDERS 16 + +/* forward declarations */ +static el_val_t llm_extract_text(el_val_t resp_val); +static el_val_t llm_extract_text_openai(el_val_t resp_val); + +static el_val_t llm_extract_text_openai(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup("")); + const char* choices = json_find_key(resp, "choices"); + if (!choices || *choices != '[') return el_wrap_str(el_strdup("")); + choices++; + while (*choices == ' ' || *choices == '\t') choices++; + if (*choices != '{') return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(choices); + size_t n = (size_t)(end - choices); + char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0'; + const char* msg = json_find_key(obj, "message"); + if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); } + const char* msg_end = json_skip_value(msg); + size_t mn = (size_t)(msg_end - msg); + char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0'; + const char* content = json_find_key(msg_obj, "content"); + el_val_t result = el_wrap_str(el_strdup("")); + if (content && *content == '"') { + JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 }; + char* text = jp_parse_string_raw(&jp); + if (!jp.err && text) result = el_wrap_str(text); + } + free(msg_obj); free(obj); + return result; +} + +/* Send a request to one provider. Returns the raw response string. + * format: 0 = openai, 1 = anthropic */ +static el_val_t llm_provider_request(const char* url, const char* key, + int format, const char* model, + const char* system_str, + const char* user_str) { + char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL; + char* esc_user = json_escape_alloc(user_str ? user_str : ""); + JsonBuf b; jb_init(&b); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + + if (format == 0) { /* OpenAI */ + char full_url[1024]; + if (strstr(url, "/chat/completions") || strstr(url, "/messages")) { + snprintf(full_url, sizeof(full_url), "%s", url); + } else { + snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url); + } + { size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron"); + jb_puts(&b, ",\"max_tokens\":4096,\"messages\":["); + if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); } + jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", full_url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text_openai(resp); + } else { /* Anthropic */ + { size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); } + { size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL); + jb_puts(&b, ",\"max_tokens\":4096"); + if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}"); + el_val_t resp = http_do("POST", url, b.buf, h); + curl_slist_free_all(h); free(b.buf); + if (esc_sys) free(esc_sys); free(esc_user); + return llm_extract_text(resp); + } +} + +static el_val_t llm_chain_call(const char* system_str, const char* user_str) { + char url_key[64], key_key[64], fmt_key[64], model_key[64]; + for (int i = 0; i < LLM_MAX_PROVIDERS; i++) { + snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i); + snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i); + snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i); + snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i); + const char* url = getenv(url_key); + const char* key = getenv(key_key); + if (!url || !*url || !key || !*key) break; /* end of chain */ + const char* fmt_s = getenv(fmt_key); + int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0; + const char* model = getenv(model_key); + fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url); + el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str); + const char* t = EL_CSTR(result); + if (t && *t && t[0] != '{') return result; /* success */ + fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i); + } + /* Legacy fallback: ANTHROPIC_API_KEY */ + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("no LLM providers configured"); + fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n"); + return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str); +} + +/* Legacy llm_request — kept for backward compat with agentic loop internals */ +static el_val_t llm_request(const char* json_body) { + const char* api_key = getenv("ANTHROPIC_API_KEY"); + if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set"); + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + { size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); } + { size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); } + el_val_t resp = http_do("POST", LLM_API_URL, json_body, h); + curl_slist_free_all(h); + return resp; +} + +/* Extract concatenated assistant text from an Anthropic /v1/messages + * response. The response shape is: + * {"content":[{"type":"text","text":"..."}, ...], ...} + * If parsing fails, returns the raw response so the caller can inspect. + */ +static el_val_t llm_extract_text(el_val_t resp_val) { + const char* resp = EL_CSTR(resp_val); + if (!resp || !*resp) return el_wrap_str(el_strdup("")); + /* If error JSON, propagate as-is. */ + if (resp[0] == '{' && strstr(resp, "\"error\"")) { + return el_wrap_str(el_strdup(resp)); + } + /* Find "content":[ ... ] */ + const char* p = json_find_key(resp, "content"); + if (!p) return el_wrap_str(el_strdup(resp)); + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return el_wrap_str(el_strdup(resp)); + p++; + JsonBuf out; jb_init(&out); + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* type_p = json_find_key(obj, "type"); + if (type_p && *type_p == '"') { + JsonParser jp = { .p = type_p, .end = type_p + strlen(type_p), .err = 0 }; + char* type_s = jp_parse_string_raw(&jp); + if (!jp.err && type_s && strcmp(type_s, "text") == 0) { + const char* tp = json_find_key(obj, "text"); + if (tp && *tp == '"') { + JsonParser jp2 = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* text_s = jp_parse_string_raw(&jp2); + if (!jp2.err && text_s) jb_puts(&out, text_s); + free(text_s); + } + } + free(type_s); + } + free(obj); + p = end; + } + return el_wrap_str(out.buf); +} + +el_val_t llm_call(el_val_t model, el_val_t prompt) { + const char* u = EL_CSTR(prompt); if (!u) u = ""; + return llm_chain_call(NULL, u); +} + +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) { + const char* s = EL_CSTR(system_prompt); if (!s) s = ""; + const char* u = EL_CSTR(user_prompt); if (!u) u = ""; + return llm_chain_call(s, u); +} + +/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */ + +typedef el_val_t (*llm_tool_fn)(el_val_t input); + +typedef struct LlmToolEntry { + char* name; + llm_tool_fn fn; +} LlmToolEntry; + +static LlmToolEntry _llm_tools[64]; +static size_t _llm_tool_count = 0; +static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER; + +static llm_tool_fn llm_tool_lookup(const char* name) { + if (!name) return NULL; + llm_tool_fn fn = NULL; + pthread_mutex_lock(&_llm_tool_mu); + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; } + } + pthread_mutex_unlock(&_llm_tool_mu); + return fn; +} + +void llm_register_tool(el_val_t name, el_val_t handler_fn_name) { + const char* nm = EL_CSTR(name); + const char* sym = EL_CSTR(handler_fn_name); + if (!nm || !*nm || !sym || !*sym) return; + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym); + return; + } + pthread_mutex_lock(&_llm_tool_mu); + /* Replace existing entry by name. */ + for (size_t i = 0; i < _llm_tool_count; i++) { + if (strcmp(_llm_tools[i].name, nm) == 0) { + _llm_tools[i].fn = (llm_tool_fn)p; + pthread_mutex_unlock(&_llm_tool_mu); + return; + } + } + if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) { + _llm_tools[_llm_tool_count].name = el_strdup(nm); + _llm_tools[_llm_tool_count].fn = (llm_tool_fn)p; + _llm_tool_count++; + } + pthread_mutex_unlock(&_llm_tool_mu); +} + +/* Serialize the El `tools` list into the JSON `tools:[...]` field expected + * by the Anthropic API. Each tool is an ElMap with name/description/ + * input_schema. input_schema is treated as either a JSON-object string + * (passed through verbatim) or a missing field (substitute {}). */ +static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) { + jb_putc(b, '['); + ElList* lst = (ElList*)(uintptr_t)tools_list; + int64_t n = lst ? lst->length : 0; + for (int64_t i = 0; i < n; i++) { + if (i > 0) jb_putc(b, ','); + ElMap* tm = as_map(lst->elems[i]); + const char* name = ""; + const char* desc = ""; + const char* schema = "{}"; + if (tm) { + for (int64_t k = 0; k < tm->count; k++) { + const char* key = EL_CSTR(tm->keys[k]); + const char* val = EL_CSTR(tm->values[k]); + if (!key || !val) continue; + if (strcmp(key, "name") == 0) name = val; + else if (strcmp(key, "description") == 0) desc = val; + else if (strcmp(key, "input_schema") == 0) schema = val; + } + } + char* esc_name = json_escape_alloc(name); + char* esc_desc = json_escape_alloc(desc); + jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name); + jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc); + jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}"); + jb_putc(b, '}'); + free(esc_name); free(esc_desc); + } + jb_putc(b, ']'); +} + +/* Walk the assistant `content` array and emit each block back into b, + * preserving the verbatim JSON of every block — used to re-include the + * assistant turn in the next request. */ +static void llm_emit_content_blocks(JsonBuf* b, const char* resp) { + const char* p = json_find_key(resp, "content"); + jb_putc(b, '['); + if (!p) { jb_putc(b, ']'); return; } + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { jb_putc(b, ']'); return; } + p++; + int first = 1; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + if (!first) jb_putc(b, ','); + first = 0; + size_t n = (size_t)(end - p); + jb_reserve(b, n); + memcpy(b->buf + b->len, p, n); + b->len += n; + b->buf[b->len] = '\0'; + p = end; + } + jb_putc(b, ']'); +} + +/* Concatenate all "text" blocks from a response. Returns owned string. */ +static char* llm_concat_text_blocks(const char* resp) { + JsonBuf out; jb_init(&out); + if (!resp) return out.buf; + const char* p = json_find_key(resp, "content"); + if (!p) return out.buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return out.buf; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + const char* tp = json_find_key(obj, "type"); + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + char* tname = jp_parse_string_raw(&jp); + if (!jp.err && tname && strcmp(tname, "text") == 0) { + const char* xp = json_find_key(obj, "text"); + if (xp && *xp == '"') { + JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 }; + char* txt = jp_parse_string_raw(&jp2); + if (!jp2.err && txt) jb_puts(&out, txt); + free(txt); + } + } + free(tname); + } + free(obj); + p = end; + } + return out.buf; +} + +/* Build tool_result message blocks for every tool_use in a response. + * Appends to `b` an array element for each tool_use; caller wraps. */ +static int llm_build_tool_results(JsonBuf* b, const char* resp) { + int any = 0; + const char* p = json_find_key(resp, "content"); + if (!p) return 0; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') return 0; + p++; + while (*p && *p != ']') { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p != '{') break; + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* obj = malloc(n + 1); + memcpy(obj, p, n); obj[n] = '\0'; + + const char* tp = json_find_key(obj, "type"); + char* type_s = NULL; + if (tp && *tp == '"') { + JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 }; + type_s = jp_parse_string_raw(&jp); + } + if (type_s && strcmp(type_s, "tool_use") == 0) { + /* Extract id, name, input. */ + char* id_s = NULL; char* name_s = NULL; + const char* idp = json_find_key(obj, "id"); + if (idp && *idp == '"') { + JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 }; + id_s = jp_parse_string_raw(&jp); + } + const char* np = json_find_key(obj, "name"); + if (np && *np == '"') { + JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 }; + name_s = jp_parse_string_raw(&jp); + } + el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input")); + const char* input_s = EL_CSTR(input_raw); + if (!input_s || !*input_s) input_s = "{}"; + + llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : ""); + char* result = NULL; + int is_error = 0; + if (!fn) { + size_t en = strlen(name_s ? name_s : "(null)") + 64; + result = malloc(en); + snprintf(result, en, "{\"error\":\"tool not registered: %s\"}", + name_s ? name_s : "(null)"); + is_error = 1; + } else { + el_val_t out = fn(EL_STR(input_s)); + const char* os = EL_CSTR(out); + result = el_strdup(os ? os : ""); + } + + if (any) jb_putc(b, ','); + char* esc_id = json_escape_alloc(id_s ? id_s : ""); + char* esc_res = json_escape_alloc(result ? result : ""); + jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\""); + jb_puts(b, esc_id); + jb_puts(b, "\",\"content\":\""); + jb_puts(b, esc_res); + jb_puts(b, "\""); + if (is_error) jb_puts(b, ",\"is_error\":true"); + jb_putc(b, '}'); + free(esc_id); free(esc_res); free(result); + free(id_s); free(name_s); + any = 1; + } + free(type_s); + free(obj); + p = end; + } + return any; +} + +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) { + /* Empty tools list → degrade to plain system call. */ + ElList* tl = (ElList*)(uintptr_t)tools; + if (!tl || tl->length == 0) { + return llm_call_system(model, system, user); + } + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = ""; + const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = ""; + + /* Build the static parts: tools JSON and system prompt — these don't + * change across iterations. */ + JsonBuf tools_buf; jb_init(&tools_buf); + llm_emit_tools_json(&tools_buf, tools); + char* esc_sys = json_escape_alloc(sys_p); + + /* messages array, accumulated as a mutable JSON fragment (no surrounding + * brackets — emitted at request time). */ + JsonBuf msgs; jb_init(&msgs); + /* First user message. */ + char* esc_user = json_escape_alloc(usr_p); + jb_puts(&msgs, "{\"role\":\"user\",\"content\":\""); + jb_puts(&msgs, esc_user); + jb_puts(&msgs, "\"}"); + free(esc_user); + + char* last_text = el_strdup(""); + el_val_t final_out = 0; + int reached_cap = 1; + + for (int iter = 0; iter < 10; iter++) { + /* Build request body. */ + JsonBuf body; jb_init(&body); + jb_putc(&body, '{'); + jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m); + jb_puts(&body, ",\"max_tokens\":4096"); + if (*sys_p) { + jb_puts(&body, ",\"system\":\""); + jb_puts(&body, esc_sys); + jb_puts(&body, "\""); + } + jb_puts(&body, ",\"tools\":"); + jb_puts(&body, tools_buf.buf); + jb_puts(&body, ",\"messages\":["); + jb_puts(&body, msgs.buf); + jb_puts(&body, "]}"); + + el_val_t resp_v = llm_request(body.buf); + free(body.buf); + const char* resp = EL_CSTR(resp_v); + if (!resp || !*resp) { + final_out = http_error_json("empty response"); + reached_cap = 0; + break; + } + if (resp[0] == '{' && strstr(resp, "\"error\"") && + !json_find_key(resp, "content")) { + final_out = el_wrap_str(el_strdup(resp)); + reached_cap = 0; + break; + } + + /* Update last_text from this response. */ + free(last_text); + last_text = llm_concat_text_blocks(resp); + + /* Inspect stop_reason. */ + el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason")); + const char* sr = EL_CSTR(sr_v); if (!sr) sr = ""; + + if (strcmp(sr, "end_turn") == 0) { + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + if (strcmp(sr, "max_tokens") == 0) { + size_t ln = strlen(last_text) + 16; + char* out = malloc(ln); + snprintf(out, ln, "%s\n[truncated]", last_text); + final_out = el_wrap_str(out); + reached_cap = 0; + break; + } + if (strcmp(sr, "tool_use") != 0) { + /* Unexpected stop reason; return the text we have. */ + final_out = el_wrap_str(el_strdup(last_text)); + reached_cap = 0; + break; + } + + /* Append the assistant turn (raw content blocks) to messages. */ + JsonBuf ab; jb_init(&ab); + jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":"); + llm_emit_content_blocks(&ab, resp); + jb_putc(&ab, '}'); + jb_puts(&msgs, ab.buf); + free(ab.buf); + + /* Build tool_result message. */ + JsonBuf tr; jb_init(&tr); + jb_puts(&tr, ",{\"role\":\"user\",\"content\":["); + int any = llm_build_tool_results(&tr, resp); + jb_puts(&tr, "]}"); + if (any) { + jb_puts(&msgs, tr.buf); + } + free(tr.buf); + } + + if (reached_cap) { + size_t ln = strlen(last_text) + 32; + char* out = malloc(ln); + snprintf(out, ln, "[loop_cap_reached]\n%s", last_text); + final_out = el_wrap_str(out); + } + free(last_text); + free(esc_sys); + free(tools_buf.buf); + free(msgs.buf); + return final_out; +} + +/* base64-encode arbitrary bytes (returns owned C string). + * Internal helper for llm_vision; the public crypto entry point that El + * programs call is `base64_encode(el_val_t)` defined in the crypto block + * at the end of this file. */ +static char* el_b64_encode_internal(const unsigned char* src, size_t n) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t out_len = 4 * ((n + 2) / 3); + char* out = malloc(out_len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n;) { + uint32_t v = 0; int got = 0; + v |= (uint32_t)src[i++] << 16; got++; + if (i < n) { v |= (uint32_t)src[i++] << 8; got++; } + if (i < n) { v |= (uint32_t)src[i++]; got++; } + out[o++] = tbl[(v >> 18) & 0x3f]; + out[o++] = tbl[(v >> 12) & 0x3f]; + out[o++] = (got > 1) ? tbl[(v >> 6) & 0x3f] : '='; + out[o++] = (got > 2) ? tbl[v & 0x3f] : '='; + } + out[o] = '\0'; + return out; +} + +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64) { + const char* m = llm_resolve_model(EL_CSTR(model)); + const char* s = EL_CSTR(system); if (!s) s = ""; + const char* u = EL_CSTR(prompt); if (!u) u = ""; + const char* img = EL_CSTR(image_url_or_b64); if (!img) img = ""; + + /* Choose source mode */ + char* image_block = NULL; + if (strncasecmp(img, "http://", 7) == 0 || strncasecmp(img, "https://", 8) == 0) { + char* esc_url = json_escape_alloc(img); + size_t n = strlen(esc_url) + 128; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":\"%s\"}}", + esc_url); + free(esc_url); + } else if (strncmp(img, "data:", 5) == 0) { + /* Inline data URL: split media-type and base64 */ + const char* semi = strchr(img + 5, ';'); + const char* comma = strchr(img + 5, ','); + char media[64] = "image/png"; + if (semi && comma && semi < comma) { + size_t ml = (size_t)(semi - (img + 5)); + if (ml >= sizeof(media)) ml = sizeof(media) - 1; + memcpy(media, img + 5, ml); media[ml] = '\0'; + } + const char* b64 = comma ? comma + 1 : ""; + char* esc_media = json_escape_alloc(media); + char* esc_b64 = json_escape_alloc(b64); + size_t n = strlen(esc_media) + strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + esc_media, esc_b64); + free(esc_media); free(esc_b64); + } else if (*img) { + /* Treat as file path: read, base64-encode, attach. */ + FILE* f = fopen(img, "rb"); + if (!f) { + char err[256]; snprintf(err, sizeof(err), "cannot open image: %s", img); + return http_error_json(err); + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return http_error_json("empty image file"); } + unsigned char* buf = malloc((size_t)sz); + if (!buf) { fclose(f); return http_error_json("oom"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + char* b64 = el_b64_encode_internal(buf, got); + free(buf); + if (!b64) return http_error_json("base64 encode failed"); + const char* media = "image/png"; + size_t ilen = strlen(img); + if (ilen >= 4) { + if (strcasecmp(img + ilen - 4, ".jpg") == 0 || + (ilen >= 5 && strcasecmp(img + ilen - 5, ".jpeg") == 0)) media = "image/jpeg"; + else if (strcasecmp(img + ilen - 4, ".gif") == 0) media = "image/gif"; + else if (strcasecmp(img + ilen - 4, ".webp") == 0) media = "image/webp"; + } + char* esc_b64 = json_escape_alloc(b64); free(b64); + size_t n = strlen(esc_b64) + 192; + image_block = malloc(n); + snprintf(image_block, n, + "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," + "\"media_type\":\"%s\",\"data\":\"%s\"}}", + media, esc_b64); + free(esc_b64); + } + + char* esc_sys = json_escape_alloc(s); + char* esc_user = json_escape_alloc(u); + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m); + jb_puts(&b, ",\"max_tokens\":4096"); + if (*s) { + jb_puts(&b, ",\"system\":\""); + jb_puts(&b, esc_sys); + jb_puts(&b, "\""); + } + jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":["); + if (image_block) { + jb_puts(&b, image_block); + jb_putc(&b, ','); + } + jb_puts(&b, "{\"type\":\"text\",\"text\":\""); + jb_puts(&b, esc_user); + jb_puts(&b, "\"}]}]}"); + free(esc_sys); free(esc_user); free(image_block); + el_val_t resp = llm_request(b.buf); + free(b.buf); + return llm_extract_text(resp); +} + +el_val_t llm_models(void) { + el_val_t lst = el_list_empty(); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-sonnet-4-5"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-opus-4-7"))); + lst = el_list_append(lst, el_wrap_str(el_strdup("claude-haiku-4-5"))); + return lst; +} +#endif /* HAVE_CURL */ + +/* ── Native VM builtin aliases ────────────────────────────────────────────── + * El source files use native_* names (El VM builtins). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } + +/* ── Crypto primitives ────────────────────────────────────────────────────── + * + * SHA-256 implementation adapted from Brad Conte's public-domain reference + * (https://github.com/B-Con/crypto-algorithms/blob/master/sha256.c, public + * domain per the project's LICENSE). HMAC follows RFC 2104. Base64 encoding + * follows RFC 4648; the URL-safe variant uses the alphabet from §5 of the + * RFC and omits padding (per JWT/JWS convention). + * + * Self-contained: no OpenSSL/libcrypto dependency. The runtime keeps its + * existing `-lcurl -lpthread -ldl -lm` link line. + * + * Binary outputs (sha256_bytes, hmac_sha256_bytes) tag their buffer with a + * magic header so base64_encode/base64url_encode can recover the exact byte + * length even when the payload contains embedded NULs. Plain C strings + * (without the header) fall back to strlen(), preserving the existing API + * shape for normal text inputs. */ + +/* Magic-header for length-tagged binary buffers. Layout: + * [ uint32_t magic = EL_MAGIC_BIN ][ uint32_t length ][ data... ][ \0 ] + * The returned el_val_t points at `data`, so consumers that strlen() it still + * get a sensible (though possibly truncated) view. el_bin_len() recovers the + * true length by sniffing the 8 bytes preceding the pointer. + * + * Magic value chosen with high MSB so it cannot collide with printable ASCII + * (the same discriminator pattern used by EL_MAGIC_LIST / EL_MAGIC_MAP). */ +#define EL_MAGIC_BIN 0xE1B17EAFu + +typedef struct { + uint32_t magic; + uint32_t length; +} el_bin_hdr_t; + +/* Allocate a length-tagged binary buffer; returns pointer to the data area. */ +static unsigned char* el_bin_alloc(size_t len) { + el_bin_hdr_t* hdr = (el_bin_hdr_t*)malloc(sizeof(el_bin_hdr_t) + len + 1); + if (!hdr) { fputs("el_runtime: out of memory (bin)\n", stderr); exit(1); } + hdr->magic = EL_MAGIC_BIN; + hdr->length = (uint32_t)len; + unsigned char* data = (unsigned char*)(hdr + 1); + data[len] = '\0'; /* keep NUL-terminated for accidental strlen calls */ + return data; +} + +/* Recover length from a possibly-tagged buffer. Returns 1 if tagged. */ +static int el_bin_lookup(const void* p, size_t* out_len) { + if (!p) { *out_len = 0; return 0; } + /* Avoid reading off the front of a page on tiny pointers (e.g. NULs + * passed in as int-cast values). 4096 is a safe lower bound on any + * platform we target. */ + if ((uintptr_t)p < 4096) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + if (hdr->magic != EL_MAGIC_BIN) return 0; + *out_len = hdr->length; + return 1; +} + +/* Effective input length: tagged length if present, else strlen. */ +static size_t el_input_len(const char* s) { + size_t n; + if (el_bin_lookup(s, &n)) return n; + return s ? strlen(s) : 0; +} + +/* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ + +typedef struct { + unsigned char data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} el_sha256_ctx_t; + +static const uint32_t el_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define EL_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define EL_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define EL_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EL_EP0(x) (EL_ROTR(x,2) ^ EL_ROTR(x,13) ^ EL_ROTR(x,22)) +#define EL_EP1(x) (EL_ROTR(x,6) ^ EL_ROTR(x,11) ^ EL_ROTR(x,25)) +#define EL_SIG0(x) (EL_ROTR(x,7) ^ EL_ROTR(x,18) ^ ((x) >> 3)) +#define EL_SIG1(x) (EL_ROTR(x,17) ^ EL_ROTR(x,19) ^ ((x) >> 10)) + +static void el_sha256_transform(el_sha256_ctx_t* ctx, const unsigned char* data) { + uint32_t a, b, c, d, e, f, g, h, t1, t2, m[64]; + int i, j; + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) + | ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (; i < 64; ++i) { + m[i] = EL_SIG1(m[i-2]) + m[i-7] + EL_SIG0(m[i-15]) + m[i-16]; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0; i < 64; ++i) { + t1 = h + EL_EP1(e) + EL_CH(e,f,g) + el_sha256_k[i] + m[i]; + t2 = EL_EP0(a) + EL_MAJ(a,b,c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void el_sha256_init(el_sha256_ctx_t* ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; +} + +static void el_sha256_update(el_sha256_ctx_t* ctx, const unsigned char* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + el_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void el_sha256_final(el_sha256_ctx_t* ctx, unsigned char hash[32]) { + uint32_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + el_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (unsigned char)( ctx->bitlen & 0xff); + ctx->data[62] = (unsigned char)((ctx->bitlen >> 8) & 0xff); + ctx->data[61] = (unsigned char)((ctx->bitlen >> 16) & 0xff); + ctx->data[60] = (unsigned char)((ctx->bitlen >> 24) & 0xff); + ctx->data[59] = (unsigned char)((ctx->bitlen >> 32) & 0xff); + ctx->data[58] = (unsigned char)((ctx->bitlen >> 40) & 0xff); + ctx->data[57] = (unsigned char)((ctx->bitlen >> 48) & 0xff); + ctx->data[56] = (unsigned char)((ctx->bitlen >> 56) & 0xff); + el_sha256_transform(ctx, ctx->data); + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0xff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0xff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0xff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0xff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0xff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0xff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0xff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0xff; + } +} + +static void el_sha256_oneshot(const unsigned char* data, size_t len, unsigned char out[32]) { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, data, len); + el_sha256_final(&c, out); +} + +/* ─── HMAC-SHA-256 (RFC 2104) ───────────────────────────────────────────── */ + +static void el_hmac_sha256(const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + unsigned char out[32]) { + unsigned char k[64]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + unsigned char inner[32]; + + if (key_len > 64) { + el_sha256_oneshot(key, key_len, k); + memset(k + 32, 0, 32); + } else { + memcpy(k, key, key_len); + memset(k + key_len, 0, 64 - key_len); + } + for (int i = 0; i < 64; ++i) { + k_ipad[i] = k[i] ^ 0x36; + k_opad[i] = k[i] ^ 0x5c; + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_ipad, 64); + el_sha256_update(&c, msg, msg_len); + el_sha256_final(&c, inner); + } + { + el_sha256_ctx_t c; + el_sha256_init(&c); + el_sha256_update(&c, k_opad, 64); + el_sha256_update(&c, inner, 32); + el_sha256_final(&c, out); + } +} + +/* ─── Hex helper ────────────────────────────────────────────────────────── */ + +static el_val_t el_hex_encode(const unsigned char* data, size_t len) { + static const char digits[] = "0123456789abcdef"; + char* out = el_strbuf(len * 2); + for (size_t i = 0; i < len; ++i) { + out[i * 2] = digits[(data[i] >> 4) & 0xf]; + out[i * 2 + 1] = digits[ data[i] & 0xf]; + } + out[len * 2] = '\0'; + return el_wrap_str(out); +} + +/* ─── Base64 (RFC 4648) ─────────────────────────────────────────────────── */ + +static const char el_b64_std_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char el_b64_url_alphabet[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe) { + const char* alphabet = url_safe ? el_b64_url_alphabet : el_b64_std_alphabet; + /* Standard form is padded to multiple of 4; URL-safe omits padding. */ + size_t out_cap = ((len + 2) / 3) * 4 + 1; + char* out = el_strbuf(out_cap); + size_t i = 0, j = 0; + while (i + 3 <= len) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8) | (uint32_t)data[i+2]; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + out[j++] = alphabet[ v & 0x3f]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint32_t)data[i] << 16; + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + if (!url_safe) { out[j++] = '='; out[j++] = '='; } + } else if (rem == 2) { + uint32_t v = ((uint32_t)data[i] << 16) | ((uint32_t)data[i+1] << 8); + out[j++] = alphabet[(v >> 18) & 0x3f]; + out[j++] = alphabet[(v >> 12) & 0x3f]; + out[j++] = alphabet[(v >> 6) & 0x3f]; + if (!url_safe) { out[j++] = '='; } + } + out[j] = '\0'; + return el_wrap_str(out); +} + +/* Decode either alphabet — accepts both '+/' and '-_' transparently, and + * tolerates missing padding (which JWTs typically omit). Whitespace is + * skipped for robustness. Invalid characters cause the decode to stop and + * the partial result so far is returned. */ +static el_val_t el_base64_decode_any(const char* in) { + if (!in) { + unsigned char* empty = el_bin_alloc(0); + return EL_STR((char*)empty); + } + size_t in_len = strlen(in); + /* Worst case: 3 output bytes per 4 input chars, +1 NUL slack. */ + unsigned char* out = el_bin_alloc(((in_len + 3) / 4) * 3 + 1); + + int8_t lut[256]; + for (int i = 0; i < 256; ++i) lut[i] = -1; + for (int i = 0; i < 64; ++i) lut[(unsigned char)el_b64_std_alphabet[i]] = (int8_t)i; + /* Allow URL-safe characters too (so one decoder handles both forms). */ + lut[(unsigned char)'-'] = 62; + lut[(unsigned char)'_'] = 63; + + uint32_t buf = 0; + int bits = 0; + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = (unsigned char)in[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int8_t v = lut[c]; + if (v < 0) break; /* invalid char — stop */ + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[o++] = (unsigned char)((buf >> bits) & 0xff); + } + } + /* Patch the length header to the actual decoded length. */ + el_bin_hdr_t* hdr = (el_bin_hdr_t*)((char*)out - sizeof(el_bin_hdr_t)); + hdr->length = (uint32_t)o; + out[o] = '\0'; + return EL_STR((char*)out); +} + +/* ─── Public crypto entry points ────────────────────────────────────────── */ + +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len) { + unsigned char* out = el_bin_alloc(32); + el_sha256_oneshot(data, len, out); + return EL_STR((char*)out); +} + +el_val_t sha256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +el_val_t sha256_bytes(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_sha256_bytes_n((const unsigned char*)(s ? s : ""), n); +} + +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char mac[32]; + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + mac); + return el_hex_encode(mac, 32); +} + +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message) { + const char* k = EL_CSTR(key); + const char* m = EL_CSTR(message); + size_t kn = el_input_len(k); + size_t mn = el_input_len(m); + unsigned char* out = el_bin_alloc(32); + el_hmac_sha256((const unsigned char*)(k ? k : ""), kn, + (const unsigned char*)(m ? m : ""), mn, + out); + return EL_STR((char*)out); +} + +el_val_t base64_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/0); +} + +el_val_t base64url_encode(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + return el_base64_encode_n((const unsigned char*)(s ? s : ""), n, /*url_safe=*/1); +} + +el_val_t base64_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +el_val_t base64url_decode(el_val_t input) { + return el_base64_decode_any(EL_CSTR(input)); +} + +/* ── Post-quantum cryptography (liboqs + OpenSSL) ─────────────────────────── + * + * Algorithm choices (per CNSA 2.0 / NIST PQ guidance, as of 2024): + * Signatures: CRYSTALS-Dilithium-3 (NIST security level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST security level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * Hybrid: X25519 || Kyber-768, combined via HKDF-SHA256 + * + * Why hybrid: Kyber is new. X25519 has 20+ years of analysis. Hybridizing + * preserves classical security if Kyber falls to a future cryptanalytic + * advance, and preserves PQ security if X25519 falls to a quantum adversary. + * "Recordable now, decryptable later" already threatens long-lived classical + * key exchange — the only safe move for keys protecting durable doctrine + * (CGI lineage, KindredGrants, Principal-CGI covenants) is to encapsulate + * with PQ today, even if the classical leg is what the wire shows. + * + * Compile-time detection: when is unavailable the pq_* functions + * compile to stubs that return a JSON error envelope. SHA3-256 stays + * available regardless (it's implemented inline, no liboqs dep). This lets + * the runtime build cleanly on dev machines without liboqs while production + * gets the full PQ stack. */ + +/* ─── SHA3-256 (Keccak, FIPS 202) ──────────────────────────────────────────── + * Inline reference implementation. ~120 LoC, no external dependency. + * rate=1088 bits, capacity=512 bits, output=256 bits, padding=0x06. */ + +static const uint64_t el_keccak_rc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +static const unsigned el_keccak_rho[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 +}; + +static const unsigned el_keccak_pi[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 +}; + +#define EL_ROTL64(x, n) (((x) << (n)) | ((x) >> (64 - (n)))) + +static void el_keccak_f1600(uint64_t s[25]) { + for (int round = 0; round < 24; ++round) { + uint64_t bc[5], t; + for (int i = 0; i < 5; ++i) + bc[i] = s[i] ^ s[i+5] ^ s[i+10] ^ s[i+15] ^ s[i+20]; + for (int i = 0; i < 5; ++i) { + t = bc[(i+4) % 5] ^ EL_ROTL64(bc[(i+1) % 5], 1); + for (int j = 0; j < 25; j += 5) s[j+i] ^= t; + } + t = s[1]; + for (int i = 0; i < 24; ++i) { + int j = el_keccak_pi[i]; + bc[0] = s[j]; + s[j] = EL_ROTL64(t, el_keccak_rho[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; ++i) bc[i] = s[j+i]; + for (int i = 0; i < 5; ++i) + s[j+i] = bc[i] ^ ((~bc[(i+1) % 5]) & bc[(i+2) % 5]); + } + s[0] ^= el_keccak_rc[round]; + } +} + +static void el_sha3_256_oneshot(const unsigned char* data, size_t len, + unsigned char out[32]) { + uint64_t st[25] = {0}; + unsigned char* sb = (unsigned char*)st; + const size_t rate = 136; /* 1088 bits / 8 */ + size_t i = 0; + while (len - i >= rate) { + for (size_t k = 0; k < rate; ++k) sb[k] ^= data[i + k]; + el_keccak_f1600(st); + i += rate; + } + size_t rem = len - i; + for (size_t k = 0; k < rem; ++k) sb[k] ^= data[i + k]; + sb[rem] ^= 0x06; /* SHA3 domain-separation byte */ + sb[rate - 1] ^= 0x80; /* final-block padding bit (high bit of last byte) */ + el_keccak_f1600(st); + memcpy(out, sb, 32); +} + +el_val_t sha3_256_hex(el_val_t input) { + const char* s = EL_CSTR(input); + size_t n = el_input_len(s); + unsigned char digest[32]; + el_sha3_256_oneshot((const unsigned char*)(s ? s : ""), n, digest); + return el_hex_encode(digest, 32); +} + +/* ─── Hex decode helper ───────────────────────────────────────────────────── + * Returns a length-tagged binary buffer (so embedded NULs survive); on + * odd-length / invalid input returns NULL with *out_len = 0. Caller is + * responsible for emitting the error envelope. */ + +static int el_hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +__attribute__((unused)) +static unsigned char* el_hex_decode(const char* s, size_t* out_len) { + *out_len = 0; + if (!s) return NULL; + size_t n = strlen(s); + if (n & 1) return NULL; + size_t blen = n / 2; + unsigned char* out = el_bin_alloc(blen); + for (size_t i = 0; i < blen; ++i) { + int hi = el_hex_nibble(s[i*2]); + int lo = el_hex_nibble(s[i*2 + 1]); + if (hi < 0 || lo < 0) return NULL; + out[i] = (unsigned char)((hi << 4) | lo); + } + *out_len = blen; + return out; +} + +/* JSON error envelope reused across all PQ entry points. */ +static el_val_t pq_error(const char* msg) { + return http_error_json(msg); +} + +#if __has_include() +#include +#define EL_HAVE_LIBOQS 1 +#else +#define EL_HAVE_LIBOQS 0 +#endif + +#if EL_HAVE_LIBOQS && __has_include() +#include +#define EL_HAVE_OPENSSL 1 +#else +#define EL_HAVE_OPENSSL 0 +#endif + +#if !EL_HAVE_LIBOQS + +/* ─── Stubs (liboqs unavailable) ─────────────────────────────────────────── + * Each entry point returns the same JSON error so callers can inspect a + * single canonical "missing primitive" string. pq_verify is the lone + * exception — verifying without liboqs simply means "not verified", so + * returning Bool false (0) keeps the type contract intact. */ + +#define EL_PQ_NO_LIB "liboqs not linked, post-quantum primitives unavailable" + +el_val_t pq_keygen_signature(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_sign(el_val_t sk, el_val_t msg) { (void)sk; (void)msg; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_verify(el_val_t pk, el_val_t msg, el_val_t sig) { (void)pk; (void)msg; (void)sig; return EL_INT(0); } +el_val_t pq_kem_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_encaps(el_val_t pk) { (void)pk; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_kem_decaps(el_val_t sk, el_val_t ct) { (void)sk; (void)ct; return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_keygen(void) { return pq_error(EL_PQ_NO_LIB); } +el_val_t pq_hybrid_handshake(el_val_t pub) { (void)pub; return pq_error(EL_PQ_NO_LIB); } + +#else /* EL_HAVE_LIBOQS */ + +/* ─── Dilithium-3 / ML-DSA-65 signatures ──────────────────────────────── + * + * NIST FIPS 204 standardized CRYSTALS-Dilithium as ML-DSA. ML-DSA-65 is the + * FIPS form of what we historically called Dilithium-3 — same algorithm + * family, same security level, identical key/sig sizes, but with a couple + * of standardization-driven tweaks (e.g. domain separation in the message + * binding). liboqs 0.12+ exposes both names; 0.15+ retired the legacy + * "Dilithium" constants in favour of "ML-DSA". We prefer ML-DSA-65 if the + * header advertises it, fall back to Dilithium-3 otherwise. Anything + * already signed with the older constant remains verifiable against that + * same constant — callers should pin the algorithm via the OQS_SIG handle's + * method_name field if they need to interoperate with archival signatures. */ + +#if defined(OQS_SIG_alg_ml_dsa_65) +# define EL_DILITHIUM_ALG OQS_SIG_alg_ml_dsa_65 +#elif defined(OQS_SIG_alg_dilithium_3) +# define EL_DILITHIUM_ALG OQS_SIG_alg_dilithium_3 +#else +# define EL_DILITHIUM_ALG "ML-DSA-65" /* string fallback; runtime probe catches misconfig */ +#endif + +el_val_t pq_keygen_signature(void) { + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + unsigned char* pk = (unsigned char*)malloc(sig->length_public_key); + unsigned char* sk = (unsigned char*)malloc(sig->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_SIG_free(sig); return pq_error("oom"); } + if (OQS_SIG_keypair(sig, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_SIG_free(sig); + return pq_error("dilithium-3 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, sig->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, sig->length_secret_key); + OQS_MEM_secure_free(sk, sig->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_SIG_free(sig); + return el_wrap_str(buf); +} + +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message) { + size_t sk_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + if (!sk) return pq_error("invalid hex in secret_key"); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return pq_error("OQS_SIG_new(dilithium-3) failed"); + if (sk_len != sig->length_secret_key) { + OQS_SIG_free(sig); + return pq_error("secret_key length mismatch for dilithium-3"); + } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + unsigned char* signature = (unsigned char*)malloc(sig->length_signature); + size_t signature_len = sig->length_signature; + if (!signature) { OQS_SIG_free(sig); return pq_error("oom"); } + + if (OQS_SIG_sign(sig, signature, &signature_len, + (const unsigned char*)(msg ? msg : ""), msg_len, sk) != OQS_SUCCESS) { + free(signature); OQS_SIG_free(sig); + return pq_error("dilithium-3 sign failed"); + } + el_val_t sig_hex = el_hex_encode(signature, signature_len); + free(signature); OQS_SIG_free(sig); + return sig_hex; +} + +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex) { + size_t pk_len = 0, sig_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + unsigned char* signature = el_hex_decode(EL_CSTR(signature_hex), &sig_len); + if (!pk || !signature) return EL_INT(0); + + OQS_SIG* sig = OQS_SIG_new(EL_DILITHIUM_ALG); + if (!sig) return EL_INT(0); + if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return EL_INT(0); } + + const char* msg = EL_CSTR(message); + size_t msg_len = el_input_len(msg); + OQS_STATUS rc = OQS_SIG_verify(sig, + (const unsigned char*)(msg ? msg : ""), msg_len, + signature, sig_len, pk); + OQS_SIG_free(sig); + return (rc == OQS_SUCCESS) ? EL_INT(1) : EL_INT(0); +} + +/* ─── Kyber-768 / ML-KEM-768 KEM ──────────────────────────────────────── + * + * NIST FIPS 203 standardized CRYSTALS-Kyber as ML-KEM. ML-KEM-768 is the + * FIPS form of what we historically called Kyber-768. Same situation as + * Dilithium → ML-DSA: prefer the standardized constant, fall back to the + * legacy name. liboqs 0.15.0 still exposes OQS_KEM_alg_kyber_768; the + * algorithm is identical at the wire level to ML-KEM-768 except for FIPS + * domain-separation tweaks, so the two ciphertexts/keys are NOT + * cross-compatible. Pin the constant for archival material. */ + +#if defined(OQS_KEM_alg_ml_kem_768) +# define EL_KYBER_ALG OQS_KEM_alg_ml_kem_768 +#elif defined(OQS_KEM_alg_kyber_768) +# define EL_KYBER_ALG OQS_KEM_alg_kyber_768 +#else +# define EL_KYBER_ALG "ML-KEM-768" +#endif + +el_val_t pq_kem_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + unsigned char* pk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* sk = (unsigned char*)malloc(kem->length_secret_key); + if (!pk || !sk) { free(pk); free(sk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, pk, sk) != OQS_SUCCESS) { + free(pk); free(sk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + el_val_t pk_hex = el_hex_encode(pk, kem->length_public_key); + el_val_t sk_hex = el_hex_encode(sk, kem->length_secret_key); + OQS_MEM_secure_free(sk, kem->length_secret_key); + free(pk); + + const char* pks = EL_CSTR(pk_hex); + const char* sks = EL_CSTR(sk_hex); + char* buf = el_strbuf(strlen(pks) + strlen(sks) + 64); + sprintf(buf, "{\"public_key\":\"%s\",\"secret_key\":\"%s\"}", pks, sks); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_encaps(el_val_t public_key_hex) { + size_t pk_len = 0; + unsigned char* pk = el_hex_decode(EL_CSTR(public_key_hex), &pk_len); + if (!pk) return pq_error("invalid hex in public_key"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pk_len != kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("public_key length mismatch for kyber-768"); + } + unsigned char* ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ct || !ss) { free(ct); free(ss); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_encaps(kem, ct, ss, pk) != OQS_SUCCESS) { + free(ct); free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + el_val_t ct_hex = el_hex_encode(ct, kem->length_ciphertext); + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + free(ct); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + OQS_KEM_free(kem); + return el_wrap_str(buf); +} + +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex) { + size_t sk_len = 0, ct_len = 0; + unsigned char* sk = el_hex_decode(EL_CSTR(secret_key_hex), &sk_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!sk || !ct) return pq_error("invalid hex in inputs"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (sk_len != kem->length_secret_key || ct_len != kem->length_ciphertext) { + OQS_KEM_free(kem); + return pq_error("input length mismatch for kyber-768"); + } + unsigned char* ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!ss) { OQS_KEM_free(kem); return pq_error("oom"); } + /* Kyber is IND-CCA via Fujisaki-Okamoto: decaps always returns *some* + * shared_secret even on tampered ciphertext (an implicit-rejection value + * derived from sk). Protocols MUST confirm the shared_secret matches via + * a subsequent step (e.g. AEAD tag, key-confirmation MAC) — do not + * assume decaps success implies authenticity. */ + if (OQS_KEM_decaps(kem, ss, ct, sk) != OQS_SUCCESS) { + free(ss); OQS_KEM_free(kem); + return pq_error("kyber-768 decapsulation failed"); + } + el_val_t ss_hex = el_hex_encode(ss, kem->length_shared_secret); + OQS_MEM_secure_free(ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return ss_hex; +} + +/* ─── Hybrid handshake (X25519 + Kyber-768, HKDF-SHA256 combined) ─────── */ + +#if !EL_HAVE_OPENSSL + +el_val_t pq_hybrid_keygen(void) { + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} +el_val_t pq_hybrid_handshake(el_val_t pub) { + (void)pub; + return pq_error("hybrid handshake requires OpenSSL (X25519); rebuild with -lcrypto"); +} + +#else /* EL_HAVE_OPENSSL */ + +/* HKDF-SHA256 (RFC 5869) — Extract+Expand. Reuses the inline HMAC-SHA256 + * already in this file. Empty salt → 32 zero bytes per the RFC. */ +static void el_hkdf_sha256(const unsigned char* salt, size_t salt_len, + const unsigned char* ikm, size_t ikm_len, + const unsigned char* info, size_t info_len, + unsigned char* out, size_t out_len) { + unsigned char zero_salt[32] = {0}; + if (salt_len == 0) { salt = zero_salt; salt_len = 32; } + unsigned char prk[32]; + el_hmac_sha256(salt, salt_len, ikm, ikm_len, prk); + + unsigned char t[32]; + size_t produced = 0; + unsigned char counter = 1; + unsigned char* buf = (unsigned char*)malloc(32 + info_len + 1); + if (!buf) { fputs("el_runtime: hkdf oom\n", stderr); return; } + while (produced < out_len) { + size_t off = 0; + if (counter > 1) { memcpy(buf, t, 32); off = 32; } + if (info && info_len) { memcpy(buf + off, info, info_len); off += info_len; } + buf[off++] = counter; + el_hmac_sha256(prk, 32, buf, off, t); + size_t chunk = (out_len - produced > 32) ? 32 : (out_len - produced); + memcpy(out + produced, t, chunk); + produced += chunk; + counter++; + } + free(buf); +} + +/* X25519 keygen via OpenSSL EVP. Returns 1 on success. + * Fills pk[32] and sk[32] (raw X25519 byte strings, no DER wrapper). */ +static int el_x25519_keygen(unsigned char pk[32], unsigned char sk[32]) { + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY* key = NULL; + if (EVP_PKEY_keygen(pctx, &key) != 1) { EVP_PKEY_CTX_free(pctx); return 0; } + EVP_PKEY_CTX_free(pctx); + + size_t plen = 32, slen = 32; + if (EVP_PKEY_get_raw_public_key (key, pk, &plen) != 1 || plen != 32) { + EVP_PKEY_free(key); return 0; + } + if (EVP_PKEY_get_raw_private_key(key, sk, &slen) != 1 || slen != 32) { + EVP_PKEY_free(key); return 0; + } + EVP_PKEY_free(key); + return 1; +} + +/* X25519 ECDH: derive 32-byte shared secret from local sk and remote pk. */ +static int el_x25519_derive(const unsigned char sk[32], const unsigned char rpk[32], + unsigned char ss[32]) { + EVP_PKEY* my = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, 32); + EVP_PKEY* rem = EVP_PKEY_new_raw_public_key (EVP_PKEY_X25519, NULL, rpk, 32); + if (!my || !rem) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my, NULL); + if (!dctx) { EVP_PKEY_free(my); EVP_PKEY_free(rem); return 0; } + int ok = 0; + size_t out_len = 32; + if (EVP_PKEY_derive_init(dctx) == 1 && + EVP_PKEY_derive_set_peer(dctx, rem) == 1 && + EVP_PKEY_derive(dctx, ss, &out_len) == 1 && + out_len == 32) ok = 1; + EVP_PKEY_CTX_free(dctx); + EVP_PKEY_free(my); + EVP_PKEY_free(rem); + return ok; +} + +/* Hybrid wire layout (binary form, before hex encode): + * public_key = x25519_pub (32) || kyber_pub (1184) → 1216 bytes + * secret_key = x25519_sec (32) || kyber_sec (2400) → 2432 bytes + * ciphertext = ephem_x25519_pub (32) || kyber_ct (1088) → 1120 bytes + * shared_secret = HKDF-SHA256(x25519_ss || kyber_ss, info="el-pq-hybrid-v1", 32 bytes) + * The keygen result also exposes the four component hex fields for callers + * that prefer to handle the legs independently. */ + +el_val_t pq_hybrid_keygen(void) { + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + + unsigned char xpk[32], xsk[32]; + if (!el_x25519_keygen(xpk, xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 keygen failed"); + } + + unsigned char* kpk = (unsigned char*)malloc(kem->length_public_key); + unsigned char* ksk = (unsigned char*)malloc(kem->length_secret_key); + if (!kpk || !ksk) { free(kpk); free(ksk); OQS_KEM_free(kem); return pq_error("oom"); } + if (OQS_KEM_keypair(kem, kpk, ksk) != OQS_SUCCESS) { + free(kpk); free(ksk); OQS_KEM_free(kem); + return pq_error("kyber-768 keypair generation failed"); + } + + size_t pub_len = 32 + kem->length_public_key; + size_t sec_len = 32 + kem->length_secret_key; + unsigned char* pub_buf = (unsigned char*)malloc(pub_len); + unsigned char* sec_buf = (unsigned char*)malloc(sec_len); + if (!pub_buf || !sec_buf) { + free(pub_buf); free(sec_buf); free(kpk); + OQS_MEM_secure_free(ksk, kem->length_secret_key); + OQS_KEM_free(kem); return pq_error("oom"); + } + memcpy(pub_buf, xpk, 32); memcpy(pub_buf + 32, kpk, kem->length_public_key); + memcpy(sec_buf, xsk, 32); memcpy(sec_buf + 32, ksk, kem->length_secret_key); + + el_val_t x_pub_hex = el_hex_encode(xpk, 32); + el_val_t x_sec_hex = el_hex_encode(xsk, 32); + el_val_t k_pub_hex = el_hex_encode(kpk, kem->length_public_key); + el_val_t k_sec_hex = el_hex_encode(ksk, kem->length_secret_key); + el_val_t pub_hex = el_hex_encode(pub_buf, pub_len); + el_val_t sec_hex = el_hex_encode(sec_buf, sec_len); + + OQS_MEM_secure_free(ksk, kem->length_secret_key); + free(kpk); free(pub_buf); free(sec_buf); + OQS_KEM_free(kem); + memset(xsk, 0, 32); /* best-effort wipe of stack copy */ + + const char* xph = EL_CSTR(x_pub_hex); + const char* xsh = EL_CSTR(x_sec_hex); + const char* kph = EL_CSTR(k_pub_hex); + const char* ksh = EL_CSTR(k_sec_hex); + const char* pubh = EL_CSTR(pub_hex); + const char* sech = EL_CSTR(sec_hex); + + char* buf = el_strbuf(strlen(xph) + strlen(xsh) + strlen(kph) + strlen(ksh) + + strlen(pubh) + strlen(sech) + 256); + sprintf(buf, + "{\"x25519_pub\":\"%s\",\"x25519_sec\":\"%s\"," + "\"kyber_pub\":\"%s\",\"kyber_sec\":\"%s\"," + "\"public_key\":\"%s\",\"secret_key\":\"%s\"}", + xph, xsh, kph, ksh, pubh, sech); + return el_wrap_str(buf); +} + +/* Initiator-side handshake. Caller supplies the responder's combined public + * key (x25519_pub || kyber_pub, hex-encoded). The runtime: + * 1. Generates an ephemeral X25519 keypair, runs ECDH against the + * responder's static x25519_pub. + * 2. Runs Kyber-768 encaps against the responder's kyber_pub → kyber_ct, + * kyber_ss. + * 3. Combined shared = HKDF-SHA256(salt="", ikm = x25519_ss || kyber_ss, + * info = "el-pq-hybrid-v1", L = 32). + * 4. Returns combined ciphertext (= ephemeral_x25519_pub || kyber_ct) and + * the derived shared_secret. + * + * Responder side composition (intentionally not a separate runtime fn — + * trivial to express in El given pq_kem_decaps + a future x25519_derive + * primitive): split the ciphertext into ephem_xpk (32) and kyber_ct, run + * X25519(static_xsk, ephem_xpk) and pq_kem_decaps(static_kyber_sk, kyber_ct), + * then HKDF-SHA256 with the same salt/info to recover the same shared_secret. + * If a separate x25519 entry point becomes valuable, add `pq_hybrid_open` + * here taking (secret_key_combined, ciphertext_combined). */ +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined) { + size_t pub_len = 0; + unsigned char* rpub = el_hex_decode(EL_CSTR(remote_pub_combined), &pub_len); + if (!rpub) return pq_error("invalid hex in remote_pub_combined"); + + OQS_KEM* kem = OQS_KEM_new(EL_KYBER_ALG); + if (!kem) return pq_error("OQS_KEM_new(kyber-768) failed"); + if (pub_len != 32 + kem->length_public_key) { + OQS_KEM_free(kem); + return pq_error("remote_pub_combined length mismatch (expected x25519_pub || kyber_pub)"); + } + + unsigned char e_xpk[32], e_xsk[32], x_ss[32]; + if (!el_x25519_keygen(e_xpk, e_xsk)) { + OQS_KEM_free(kem); + return pq_error("X25519 ephemeral keygen failed"); + } + if (!el_x25519_derive(e_xsk, rpub, x_ss)) { + memset(e_xsk, 0, 32); + OQS_KEM_free(kem); + return pq_error("X25519 derive failed"); + } + memset(e_xsk, 0, 32); /* ephemeral; not needed after derive */ + + unsigned char* k_ct = (unsigned char*)malloc(kem->length_ciphertext); + unsigned char* k_ss = (unsigned char*)malloc(kem->length_shared_secret); + if (!k_ct || !k_ss) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("oom"); + } + if (OQS_KEM_encaps(kem, k_ct, k_ss, rpub + 32) != OQS_SUCCESS) { + free(k_ct); free(k_ss); OQS_KEM_free(kem); + return pq_error("kyber-768 encapsulation failed"); + } + + /* HKDF combine: ikm = x_ss || k_ss. */ + size_t ikm_len = 32 + kem->length_shared_secret; + unsigned char* ikm = (unsigned char*)malloc(ikm_len); + if (!ikm) { + free(k_ct); OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_KEM_free(kem); + return pq_error("oom"); + } + memcpy(ikm, x_ss, 32); + memcpy(ikm + 32, k_ss, kem->length_shared_secret); + unsigned char combined[32]; + static const char info_str[] = "el-pq-hybrid-v1"; + el_hkdf_sha256(NULL, 0, ikm, ikm_len, + (const unsigned char*)info_str, sizeof(info_str) - 1, + combined, 32); + + memset(x_ss, 0, 32); + OQS_MEM_secure_free(k_ss, kem->length_shared_secret); + OQS_MEM_secure_free(ikm, ikm_len); + + /* Combined ciphertext = ephemeral_x25519_pub || kyber_ct. */ + size_t ct_len = 32 + kem->length_ciphertext; + unsigned char* combined_ct = (unsigned char*)malloc(ct_len); + if (!combined_ct) { free(k_ct); OQS_KEM_free(kem); return pq_error("oom"); } + memcpy(combined_ct, e_xpk, 32); + memcpy(combined_ct + 32, k_ct, kem->length_ciphertext); + free(k_ct); + OQS_KEM_free(kem); + + el_val_t ct_hex = el_hex_encode(combined_ct, ct_len); + el_val_t ss_hex = el_hex_encode(combined, 32); + free(combined_ct); + memset(combined, 0, 32); + + const char* cts = EL_CSTR(ct_hex); + const char* sss = EL_CSTR(ss_hex); + char* buf = el_strbuf(strlen(cts) + strlen(sss) + 64); + sprintf(buf, "{\"ciphertext\":\"%s\",\"shared_secret\":\"%s\"}", cts, sss); + return el_wrap_str(buf); +} + +#endif /* EL_HAVE_OPENSSL */ +#endif /* EL_HAVE_LIBOQS */ + +/* ─── AEAD: AES-256-GCM ──────────────────────────────────────────────────── + * + * Symmetric authenticated encryption used to wrap envelopes once a shared + * secret has been derived from the KEM (Kyber-768 / hybrid). The El surface + * is intentionally narrow: + * + * aead_encrypt(key_hex, plaintext) + * → {"nonce":"<24 hex>","ciphertext":"<...hex including 16-byte tag>"} + * + * aead_decrypt(key_hex, nonce_hex, ciphertext_hex) + * → plaintext String, or "" on auth failure / malformed input + * + * Conventions: + * - key_hex must decode to exactly 32 bytes (AES-256). Callers that hold + * a longer KEM shared_secret should normalize via SHA3-256(ss) → 32 bytes + * before passing it in. (Kyber-768's shared_secret is already 32 bytes, + * but keeping this contract explicit lets the El side be agnostic.) + * - nonce is a fresh 12-byte random value drawn from the OS CSPRNG. Caller + * never picks the nonce — eliminates the GCM nonce-reuse footgun entirely. + * - tag is the standard 16 bytes, appended to ciphertext per RFC 5116. + * `ciphertext` field is therefore (plaintext_len + 16) bytes, hex-encoded. + * - No associated data (AAD). If we later need bound metadata, add a + * length-prefixed AAD argument and bump the envelope version tag. + * + * Failure mode: + * aead_encrypt returns http_error_json(...) on input/system failure. + * aead_decrypt returns the empty string on ANY failure (including auth-tag + * mismatch). Callers MUST check for "" before using the result. */ + +#if !__has_include() + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + (void)key_hex; (void)plaintext; + return http_error_json("aead_encrypt requires OpenSSL (libcrypto); rebuild with -lcrypto"); +} +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + (void)key_hex; (void)nonce_hex; (void)ciphertext_hex; + return el_wrap_str(el_strdup("")); +} + +#else /* OpenSSL available */ + +#include +#include + +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext) { + size_t key_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + if (!key) return http_error_json("invalid hex in key"); + if (key_len != 32) return http_error_json("aead key must be 32 bytes (64 hex chars) for AES-256-GCM"); + + const char* pt = EL_CSTR(plaintext); + size_t pt_len = el_input_len(pt); + if (!pt) pt = ""; + + unsigned char nonce[12]; + if (RAND_bytes(nonce, 12) != 1) return http_error_json("OS CSPRNG failed (RAND_bytes)"); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return http_error_json("EVP_CIPHER_CTX_new failed"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm init failed"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("set ivlen failed"); + } + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm key/iv init failed"); + } + + /* GCM ciphertext is the same length as plaintext; we append a 16-byte + * authentication tag for AEAD semantics. Allocate plaintext_len + 16. */ + unsigned char* ct = (unsigned char*)malloc(pt_len + 16); + if (!ct) { EVP_CIPHER_CTX_free(ctx); return http_error_json("oom"); } + int outlen = 0, total = 0; + if (EVP_EncryptUpdate(ctx, ct, &outlen, (const unsigned char*)pt, (int)pt_len) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm update failed"); + } + total += outlen; + if (EVP_EncryptFinal_ex(ctx, ct + total, &outlen) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm final failed"); + } + total += outlen; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, ct + total) != 1) { + free(ct); EVP_CIPHER_CTX_free(ctx); return http_error_json("aes-256-gcm get tag failed"); + } + EVP_CIPHER_CTX_free(ctx); + + el_val_t nonce_hex_v = el_hex_encode(nonce, 12); + el_val_t ct_hex_v = el_hex_encode(ct, (size_t)total + 16); + free(ct); + + const char* nh = EL_CSTR(nonce_hex_v); + const char* ch = EL_CSTR(ct_hex_v); + char* buf = el_strbuf(strlen(nh) + strlen(ch) + 48); + sprintf(buf, "{\"nonce\":\"%s\",\"ciphertext\":\"%s\"}", nh, ch); + return el_wrap_str(buf); +} + +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex) { + size_t key_len = 0, nonce_len = 0, ct_len = 0; + unsigned char* key = el_hex_decode(EL_CSTR(key_hex), &key_len); + unsigned char* nonce = el_hex_decode(EL_CSTR(nonce_hex), &nonce_len); + unsigned char* ct = el_hex_decode(EL_CSTR(ciphertext_hex), &ct_len); + if (!key || !nonce || !ct) return el_wrap_str(el_strdup("")); + if (key_len != 32 || nonce_len != 12) return el_wrap_str(el_strdup("")); + if (ct_len < 16) return el_wrap_str(el_strdup("")); + + size_t body_len = ct_len - 16; + const unsigned char* tag = ct + body_len; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return el_wrap_str(el_strdup("")); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + + unsigned char* pt = (unsigned char*)malloc(body_len + 1); + if (!pt) { EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); } + int outlen = 0, total = 0; + if (EVP_DecryptUpdate(ctx, pt, &outlen, ct, (int)body_len) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + total += outlen; + /* Set expected tag before final — GCM's final step is where auth happens. */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag) != 1) { + free(pt); EVP_CIPHER_CTX_free(ctx); return el_wrap_str(el_strdup("")); + } + int rc = EVP_DecryptFinal_ex(ctx, pt + total, &outlen); + EVP_CIPHER_CTX_free(ctx); + if (rc != 1) { + /* Auth failure or padding/length mismatch. Return empty so callers + * cannot accidentally treat tampered ciphertext as a valid message. */ + free(pt); + return el_wrap_str(el_strdup("")); + } + total += outlen; + pt[total] = '\0'; + + /* Copy into the el arena so the caller-visible string outlives this fn. */ + char* out = el_strbuf((size_t)total); + memcpy(out, pt, (size_t)total); + out[total] = '\0'; + free(pt); + return el_wrap_str(out); +} + +#endif /* __has_include() */ + +#ifdef HAVE_CURL +/* ──────────────────────────────────────────────────────────────────────────── + * OTLP/HTTP observability — logs, traces, metrics + * + * Design goals: + * - Zero blocking on the request path. Producers append to in-memory + * ring buffers; a single worker thread flushes to the OTLP endpoint. + * - Drop-on-failure semantics. If the endpoint is unreachable or slow, + * we drop telemetry rather than back-pressure into the request handler. + * - Best-effort serialization. Each record is pre-serialized as JSON when + * the El program calls the primitive; the worker just batches. + * - Configuration via env vars: + * OTLP_ENDPOINT e.g. https://alloy.neuralplatform.ai:4318 + * OTEL_SERVICE_NAME e.g. neuron-web (default: argv[0] basename) + * OTEL_SERVICE_VERSION (default: "0.0.0") + * OTEL_RESOURCE_ATTRS comma-sep k=v pairs (optional) + * + * Wire format: OTLP/HTTP JSON. Three endpoints: + * POST {endpoint}/v1/logs — log records + * POST {endpoint}/v1/traces — spans + * POST {endpoint}/v1/metrics — counter/gauge points + * + * El programs see four primitives: + * trace_span_start(name) -> SpanHandle (just a string id) + * trace_span_end(handle) (computes duration, queues) + * emit_log(level, msg, fields_json) (queues a log record) + * emit_metric(name, value, tags_json) (queues a counter increment) + * ──────────────────────────────────────────────────────────────────────────── + */ + +#define OTLP_BUF_CAP 4096 /* per-buffer ring size */ +#define OTLP_FLUSH_MS 2000 /* flush every 2s */ +#define OTLP_BATCH_MAX 200 /* up to 200 records per POST */ + +typedef struct { + char* data; /* malloc'd JSON fragment for this record */ +} OtlpRec; + +typedef struct { + OtlpRec ring[OTLP_BUF_CAP]; + size_t head; /* next write slot */ + size_t tail; /* next read slot */ + pthread_mutex_t mu; +} OtlpQueue; + +static OtlpQueue _otlp_logs = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_traces = { .mu = PTHREAD_MUTEX_INITIALIZER }; +static OtlpQueue _otlp_metrics = { .mu = PTHREAD_MUTEX_INITIALIZER }; + +static char* _otlp_endpoint = NULL; /* e.g. https://alloy.neuralplatform.ai:4318 */ +static char* _otlp_service_name = NULL; +static char* _otlp_service_version = NULL; +static int _otlp_initialized = 0; +static pthread_t _otlp_worker_thread; + +/* enqueue — returns 1 if accepted, 0 if dropped (full buffer or no endpoint) */ +static int otlp_enqueue(OtlpQueue* q, const char* json) { + if (!_otlp_endpoint || !json) return 0; + pthread_mutex_lock(&q->mu); + size_t next_head = (q->head + 1) % OTLP_BUF_CAP; + if (next_head == q->tail) { + /* buffer full — drop oldest */ + free(q->ring[q->tail].data); + q->ring[q->tail].data = NULL; + q->tail = (q->tail + 1) % OTLP_BUF_CAP; + } + q->ring[q->head].data = strdup(json); + q->head = next_head; + pthread_mutex_unlock(&q->mu); + return 1; +} + +/* drain — copies up to OTLP_BATCH_MAX items into a comma-joined string, + * caller must free the result. Returns NULL if queue is empty. */ +static char* otlp_drain(OtlpQueue* q) { + pthread_mutex_lock(&q->mu); + if (q->head == q->tail) { pthread_mutex_unlock(&q->mu); return NULL; } + /* compute total length */ + size_t total = 0, count = 0; + size_t i = q->tail; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) total += strlen(q->ring[i].data) + 1; /* +1 for comma */ + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + char* out = malloc(total + 4); + if (!out) { pthread_mutex_unlock(&q->mu); return NULL; } + out[0] = '\0'; + size_t off = 0; + i = q->tail; + count = 0; + while (i != q->head && count < OTLP_BATCH_MAX) { + if (q->ring[i].data) { + size_t l = strlen(q->ring[i].data); + if (off > 0) { out[off++] = ','; } + memcpy(out + off, q->ring[i].data, l); + off += l; + free(q->ring[i].data); + q->ring[i].data = NULL; + } + i = (i + 1) % OTLP_BUF_CAP; + count++; + } + out[off] = '\0'; + q->tail = i; + pthread_mutex_unlock(&q->mu); + return out; +} + +/* Build resource block once (service.name, service.version, host.name) */ +static char* otlp_resource_block(void) { + static char cached[1024]; + static int built = 0; + if (built) return cached; + char host[256] = "unknown"; + gethostname(host, sizeof(host) - 1); + snprintf(cached, sizeof(cached), + "{\"attributes\":[" + "{\"key\":\"service.name\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"service.version\",\"value\":{\"stringValue\":\"%s\"}}," + "{\"key\":\"host.name\",\"value\":{\"stringValue\":\"%s\"}}" + "]}", + _otlp_service_name ? _otlp_service_name : "el-app", + _otlp_service_version ? _otlp_service_version : "0.0.0", + host); + built = 1; + return cached; +} + +/* Best-effort POST. Drops on any error. */ +static void otlp_post(const char* path, const char* body) { + if (!_otlp_endpoint || !body || !*body) return; + char url[1024]; + snprintf(url, sizeof(url), "%s%s", _otlp_endpoint, path); + CURL* c = curl_easy_init(); + if (!c) return; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, NULL); /* discard response */ + curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); +} + +/* Flush worker — runs forever until process exits */ +static void* otlp_worker(void* arg) { + (void)arg; + while (1) { + struct timespec ts = { OTLP_FLUSH_MS / 1000, (OTLP_FLUSH_MS % 1000) * 1000000L }; + nanosleep(&ts, NULL); + + char* logs = otlp_drain(&_otlp_logs); + if (logs && *logs) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceLogs\":[{\"resource\":%s," + "\"scopeLogs\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"logRecords\":[%s]}]}]}", + otlp_resource_block(), logs); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/logs", body); + } + free(logs); + + char* traces = otlp_drain(&_otlp_traces); + if (traces && *traces) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceSpans\":[{\"resource\":%s," + "\"scopeSpans\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"spans\":[%s]}]}]}", + otlp_resource_block(), traces); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/traces", body); + } + free(traces); + + char* metrics = otlp_drain(&_otlp_metrics); + if (metrics && *metrics) { + char body[OTLP_BUF_CAP * 8]; + int n = snprintf(body, sizeof(body), + "{\"resourceMetrics\":[{\"resource\":%s," + "\"scopeMetrics\":[{\"scope\":{\"name\":\"el-runtime\"}," + "\"metrics\":[%s]}]}]}", + otlp_resource_block(), metrics); + if (n > 0 && n < (int)sizeof(body)) otlp_post("/v1/metrics", body); + } + free(metrics); + } + return NULL; +} + +/* Initialize OTLP — called lazily on first emit. Idempotent. */ +static void otlp_lazy_init(void) { + if (_otlp_initialized) return; + static pthread_mutex_t once_mu = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&once_mu); + if (_otlp_initialized) { pthread_mutex_unlock(&once_mu); return; } + + const char* ep = getenv("OTLP_ENDPOINT"); + if (!ep || !*ep) { + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); + return; + } + _otlp_endpoint = strdup(ep); + /* trim trailing slash */ + size_t l = strlen(_otlp_endpoint); + if (l > 0 && _otlp_endpoint[l - 1] == '/') _otlp_endpoint[l - 1] = '\0'; + + const char* svc = getenv("OTEL_SERVICE_NAME"); + _otlp_service_name = strdup(svc && *svc ? svc : "el-app"); + const char* ver = getenv("OTEL_SERVICE_VERSION"); + _otlp_service_version = strdup(ver && *ver ? ver : "0.0.0"); + + pthread_create(&_otlp_worker_thread, NULL, otlp_worker, NULL); + pthread_detach(_otlp_worker_thread); + _otlp_initialized = 1; + pthread_mutex_unlock(&once_mu); +} + +/* JSON-escape a string into out_buf. Returns chars written (excluding null). */ +static size_t otlp_json_escape(const char* in, char* out, size_t out_cap) { + size_t o = 0; + for (size_t i = 0; in[i] && o + 8 < out_cap; i++) { + unsigned char c = (unsigned char)in[i]; + if (c == '"') { out[o++] = '\\'; out[o++] = '"'; } + else if (c == '\\'){ out[o++] = '\\'; out[o++] = '\\'; } + else if (c == '\n'){ out[o++] = '\\'; out[o++] = 'n'; } + else if (c == '\r'){ out[o++] = '\\'; out[o++] = 'r'; } + else if (c == '\t'){ out[o++] = '\\'; out[o++] = 't'; } + else if (c < 0x20) { o += snprintf(out + o, out_cap - o, "\\u%04x", c); } + else { out[o++] = (char)c; } + } + out[o] = '\0'; + return o; +} + +/* ── Public El primitives ─────────────────────────────────────────────────── */ + +/* emit_log(level, msg, fields_json) — fields_json is a JSON object string or "" */ +el_val_t emit_log(el_val_t level_v, el_val_t msg_v, el_val_t fields_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* level = EL_CSTR(level_v); if (!level) level = "INFO"; + const char* msg = EL_CSTR(msg_v); if (!msg) msg = ""; + const char* fields = EL_CSTR(fields_v); if (!fields) fields = ""; + /* Map El level names to OTLP severity numbers */ + int sev_num = 9; /* INFO */ + if (strcmp(level, "TRACE") == 0) sev_num = 1; + else if (strcmp(level, "DEBUG") == 0) sev_num = 5; + else if (strcmp(level, "INFO") == 0) sev_num = 9; + else if (strcmp(level, "WARN") == 0 || strcmp(level, "WARNING") == 0) sev_num = 13; + else if (strcmp(level, "ERROR") == 0) sev_num = 17; + else if (strcmp(level, "FATAL") == 0) sev_num = 21; + char esc_msg[2048]; otlp_json_escape(msg, esc_msg, sizeof(esc_msg)); + /* unix nanos */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"timeUnixNano\":\"%lld\",\"severityNumber\":%d," + "\"severityText\":\"%s\"," + "\"body\":{\"stringValue\":\"%s\"}%s%s}", + now_nano, sev_num, level, esc_msg, + (fields && *fields) ? ",\"attributes\":" : "", + (fields && *fields) ? fields : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_logs, rec); + return EL_INT(1); +} + +/* emit_metric(name, value, tags_json) — Sum (counter) data point. tags_json + * is a JSON array of {key, value} pairs or empty string. */ +el_val_t emit_metric(el_val_t name_v, el_val_t value_v, el_val_t tags_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "unknown"; + int64_t val = (int64_t)value_v; + const char* tags = EL_CSTR(tags_v); if (!tags) tags = ""; + char esc_name[256]; otlp_json_escape(name, esc_name, sizeof(esc_name)); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"name\":\"%s\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true," + "\"dataPoints\":[{\"asInt\":\"%lld\"," + "\"timeUnixNano\":\"%lld\"" + "%s%s}]}}", + esc_name, (long long)val, now_nano, + (tags && *tags) ? ",\"attributes\":" : "", + (tags && *tags) ? tags : ""); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_metrics, rec); + return EL_INT(1); +} + +/* trace_span_start(name) — returns a span handle (string of "traceid:spanid:start_nano:name") */ +el_val_t trace_span_start(el_val_t name_v) { + otlp_lazy_init(); + const char* name = EL_CSTR(name_v); if (!name) name = "span"; + /* generate 16-byte trace id and 8-byte span id */ + static _Thread_local int seeded = 0; + if (!seeded) { srand((unsigned int)(uintptr_t)pthread_self() ^ (unsigned int)time(NULL)); seeded = 1; } + char tid[33], sid[17]; + for (int i = 0; i < 32; i++) tid[i] = "0123456789abcdef"[rand() & 0xF]; + tid[32] = '\0'; + for (int i = 0; i < 16; i++) sid[i] = "0123456789abcdef"[rand() & 0xF]; + sid[16] = '\0'; + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long now_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char* handle = malloc(strlen(name) + 80); + if (!handle) return EL_STR(""); + sprintf(handle, "%s:%s:%lld:%s", tid, sid, now_nano, name); + el_arena_track(handle); + return EL_STR(handle); +} + +/* trace_span_end(handle) — emits the span with computed duration */ +el_val_t trace_span_end(el_val_t handle_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* h = EL_CSTR(handle_v); if (!h) return EL_INT(0); + /* parse "tid:sid:start_nano:name" */ + char tid[64], sid[32], rest[1024]; + long long start_nano = 0; + if (sscanf(h, "%63[^:]:%31[^:]:%lld:%1023[^\n]", tid, sid, &start_nano, rest) != 4) return EL_INT(0); + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long end_nano = (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; + char esc_name[1024]; otlp_json_escape(rest, esc_name, sizeof(esc_name)); + char rec[4096]; + int n = snprintf(rec, sizeof(rec), + "{\"traceId\":\"%s\",\"spanId\":\"%s\"," + "\"name\":\"%s\"," + "\"kind\":1," + "\"startTimeUnixNano\":\"%lld\"," + "\"endTimeUnixNano\":\"%lld\"," + "\"status\":{\"code\":1}}", + tid, sid, esc_name, start_nano, end_nano); + if (n > 0 && n < (int)sizeof(rec)) otlp_enqueue(&_otlp_traces, rec); + return EL_INT(1); +} + +/* Convenience: emit a one-shot timed event (emit start+end immediately). + * For El programs that want point events with duration baked in. */ +el_val_t emit_event(el_val_t name_v, el_val_t duration_ms_v) { + otlp_lazy_init(); + if (!_otlp_endpoint) return EL_INT(0); + const char* name = EL_CSTR(name_v); if (!name) name = "event"; + int64_t dur_ms = (int64_t)duration_ms_v; + el_val_t h = trace_span_start(EL_STR((char*)name)); + /* fudge start to be (now - duration) */ + (void)dur_ms; + return trace_span_end(h); +} + +#endif /* HAVE_CURL — OTLP */ + +/* ── Threading seed primitives ─────────────────────────────────────────────── + * __thread_create(fn_name, arg) -> Int spawn El fn in a pthread, return tid + * __thread_join(tid) -> String join thread, return result string + * __mutex_new() -> Int allocate a mutex, return handle + * __mutex_lock(m) lock mutex m + * __mutex_unlock(m) unlock mutex m + * + * Every El fn compiles to a global C symbol. __thread_create uses dlsym to + * look up the function by name and run it in a pthread. This means any El fn + * with signature (String) -> String is directly threadable. + */ + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + ElFn1 fn; + el_val_t arg; + el_val_t result; +} ElThreadArg; + +#define EL_THREAD_MAX 256 + +typedef struct { + pthread_t tid; + ElThreadArg* arg; + int alive; +} ElThread; + +static ElThread _threads[EL_THREAD_MAX]; +static int _thread_count = 0; +static pthread_mutex_t _thread_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +static void* el_thread_runner(void* raw) { + ElThreadArg* a = (ElThreadArg*)raw; + a->result = a->fn(a->arg); + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v) { + const char* sym = EL_CSTR(fn_name_v); + if (!sym || !*sym) return EL_INT(-1); + void* p = dlsym(RTLD_DEFAULT, sym); + if (!p) { + fprintf(stderr, "[__thread_create] symbol not found: %s\n", sym); + return EL_INT(-1); + } + ElThreadArg* a = (ElThreadArg*)malloc(sizeof(ElThreadArg)); + if (!a) return EL_INT(-1); + a->fn = (ElFn1)p; + a->arg = arg_v; + a->result = EL_STR(""); + + pthread_mutex_lock(&_thread_alloc_mu); + if (_thread_count >= EL_THREAD_MAX) { + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + fprintf(stderr, "[__thread_create] thread table full\n"); + return EL_INT(-1); + } + int slot = _thread_count++; + _threads[slot].arg = a; + _threads[slot].alive = 1; + pthread_mutex_unlock(&_thread_alloc_mu); + + if (pthread_create(&_threads[slot].tid, NULL, el_thread_runner, a) != 0) { + pthread_mutex_lock(&_thread_alloc_mu); + _thread_count--; + pthread_mutex_unlock(&_thread_alloc_mu); + free(a); + return EL_INT(-1); + } + return EL_INT(slot); +} + +el_val_t __thread_join(el_val_t tid_v) { + int slot = (int)(int64_t)tid_v; + if (slot < 0 || slot >= EL_THREAD_MAX) return EL_STR(""); + pthread_join(_threads[slot].tid, NULL); + el_val_t result = _threads[slot].arg->result; + free(_threads[slot].arg); + _threads[slot].alive = 0; + return result; +} + +/* Mutex table */ + +#define EL_MUTEX_MAX 64 + +typedef struct { + pthread_mutex_t mu; + int allocated; +} ElMutexEntry; + +static ElMutexEntry _mutexes[EL_MUTEX_MAX]; +static int _mutex_count = 0; +static pthread_mutex_t _mutex_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_mutex_alloc_mu); + if (_mutex_count >= EL_MUTEX_MAX) { + pthread_mutex_unlock(&_mutex_alloc_mu); + fprintf(stderr, "[__mutex_new] mutex table full\n"); + return EL_INT(-1); + } + int slot = _mutex_count++; + pthread_mutex_init(&_mutexes[slot].mu, NULL); + _mutexes[slot].allocated = 1; + pthread_mutex_unlock(&_mutex_alloc_mu); + return EL_INT(slot); +} + +void __mutex_lock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_lock(&_mutexes[slot].mu); +} + +void __mutex_unlock(el_val_t m_v) { + int slot = (int)(int64_t)m_v; + if (slot < 0 || slot >= EL_MUTEX_MAX || !_mutexes[slot].allocated) return; + pthread_mutex_unlock(&_mutexes[slot].mu); +} + +/* ── Channels ─────────────────────────────────────────────────────────────── * + * Buffered MPMC channel backed by a mutex + condvar + circular buffer. + * channel_new(capacity) -> Int (handle) + * channel_send(ch, msg) — blocks if full (capacity > 0) or never (unbounded) + * channel_recv(ch) -> String — blocks until a message is available + * channel_try_recv(ch) -> String — non-blocking, returns "" if empty + * channel_close(ch) — signal no more sends; recv drains remaining + * + * Bounded channels (cap > 0): circular buffer, sender blocks when full. + * Unbounded channels (cap == 0): dynamic array, sender never blocks. + */ +#define EL_CHANNEL_MAX 64 +#define EL_CHANNEL_BUF 1024 + +typedef struct { + char** buf; + int cap; /* 0 = unbounded (grows dynamically) */ + int head, tail, count; + int dyn_cap; /* allocated slots for unbounded mode */ + int closed; + pthread_mutex_t mu; + pthread_cond_t not_empty; + pthread_cond_t not_full; +} ElChannel; + +static ElChannel _channels[EL_CHANNEL_MAX]; +static int _channel_count = 0; +static pthread_mutex_t _channel_alloc_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __channel_new(el_val_t capacity_v) { + int cap = (int)(int64_t)capacity_v; + if (cap < 0) cap = 0; + + pthread_mutex_lock(&_channel_alloc_mu); + if (_channel_count >= EL_CHANNEL_MAX) { + pthread_mutex_unlock(&_channel_alloc_mu); + fprintf(stderr, "[__channel_new] channel table full\n"); + return EL_INT(-1); + } + int slot = _channel_count++; + pthread_mutex_unlock(&_channel_alloc_mu); + + ElChannel* ch = &_channels[slot]; + memset(ch, 0, sizeof(*ch)); + ch->cap = cap; + ch->closed = 0; + ch->head = 0; + ch->tail = 0; + ch->count = 0; + + if (cap > 0) { + /* Bounded: fixed circular buffer. */ + ch->buf = (char**)malloc((size_t)cap * sizeof(char*)); + ch->dyn_cap = cap; + } else { + /* Unbounded: start with EL_CHANNEL_BUF slots, grow as needed. */ + ch->buf = (char**)malloc(EL_CHANNEL_BUF * sizeof(char*)); + ch->dyn_cap = EL_CHANNEL_BUF; + } + if (!ch->buf) { + fprintf(stderr, "[__channel_new] out of memory\n"); + return EL_INT(-1); + } + + pthread_mutex_init(&ch->mu, NULL); + pthread_cond_init(&ch->not_empty, NULL); + pthread_cond_init(&ch->not_full, NULL); + + return EL_INT(slot); +} + +void __channel_send(el_val_t ch_v, el_val_t msg_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + const char* msg = EL_CSTR(msg_v); + if (!msg) msg = ""; + char* copy = strdup(msg); /* channel owns the string */ + + pthread_mutex_lock(&ch->mu); + + if (ch->closed) { + /* Send on closed channel is a no-op (drop the message). */ + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + + if (ch->cap > 0) { + /* Bounded: block while full. */ + while (ch->count >= ch->cap && !ch->closed) { + pthread_cond_wait(&ch->not_full, &ch->mu); + } + if (ch->closed) { + pthread_mutex_unlock(&ch->mu); + free(copy); + return; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->cap; + ch->count++; + } else { + /* Unbounded: grow the buffer if needed. */ + if (ch->count >= ch->dyn_cap) { + int new_cap = ch->dyn_cap * 2; + char** grown = (char**)realloc(ch->buf, (size_t)new_cap * sizeof(char*)); + if (!grown) { + pthread_mutex_unlock(&ch->mu); + free(copy); + fprintf(stderr, "[__channel_send] out of memory growing channel\n"); + return; + } + /* The circular buffer may have wrapped. Linearise it first. + * In unbounded mode head is always 0 (we append at tail, drain + * from head), so a simple memmove isn't needed — but if the + * buffer did wrap (tail < head after growth), we need to fix up. + * Simplest safe path: if tail wrapped, move the head..old_cap + * segment to new_cap..new_cap+(old_cap-head). */ + if (ch->tail < ch->head) { + /* Wrapped: [head..old_cap) is the front, [0..tail) is the back. */ + int front = ch->dyn_cap - ch->head; + memmove(grown + ch->dyn_cap, grown + ch->head, (size_t)front * sizeof(char*)); + ch->head = ch->dyn_cap; + } + ch->buf = grown; + ch->dyn_cap = new_cap; + } + ch->buf[ch->tail] = copy; + ch->tail = (ch->tail + 1) % ch->dyn_cap; + ch->count++; + } + + pthread_cond_signal(&ch->not_empty); + pthread_mutex_unlock(&ch->mu); +} + +el_val_t __channel_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + /* Block until there is a message or the channel is closed and drained. */ + while (ch->count == 0 && !ch->closed) { + pthread_cond_wait(&ch->not_empty, &ch->mu); + } + + if (ch->count == 0) { + /* Closed and empty — signal EOF. */ + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + /* Hand the string to the arena so it is freed after the request. */ + el_arena_track(msg); + return EL_STR(msg); +} + +el_val_t __channel_try_recv(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return EL_STR(""); + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + + if (ch->count == 0) { + pthread_mutex_unlock(&ch->mu); + return EL_STR(""); + } + + int buf_cap = (ch->cap > 0) ? ch->cap : ch->dyn_cap; + char* msg = ch->buf[ch->head]; + ch->head = (ch->head + 1) % buf_cap; + ch->count--; + + pthread_cond_signal(&ch->not_full); + pthread_mutex_unlock(&ch->mu); + + el_arena_track(msg); + return EL_STR(msg); +} + +void __channel_close(el_val_t ch_v) { + int slot = (int)(int64_t)ch_v; + if (slot < 0 || slot >= EL_CHANNEL_MAX) return; + ElChannel* ch = &_channels[slot]; + + pthread_mutex_lock(&ch->mu); + ch->closed = 1; + /* Wake all blocked recvers and senders so they can observe the close. */ + pthread_cond_broadcast(&ch->not_empty); + pthread_cond_broadcast(&ch->not_full); + pthread_mutex_unlock(&ch->mu); +} + +/* ── DHARMA runtime additions ──────────────────────────────────────────────── + * + * Functions required by the dharma registry service. Added here so the + * released el_runtime.c includes them without requiring dharma to bundle + * its own stubs. + * + * Functions added: + * list_len — alias for el_list_len (used in handlers.el) + * list_get — alias for el_list_get (used in handlers.el) + * json_array_push — append a pre-encoded JSON element to a JSON array string + * now_millis — milliseconds since Unix epoch (alias for time_now) + * unix_timestamp_ms — same as now_millis (alias) + * time_now_ms — same as now_millis (alias) + * log_info — stderr structured log at INFO level + * log_warn — stderr structured log at WARN level + * config — reads a config value from the environment + * http_patch — HTTP PATCH with JSON Content-Type + * http_post_engram — HTTP POST with optional X-API-Key header + * http_get_engram — HTTP GET with optional X-API-Key header + * str_to_bytes — encode a string as a JSON array of byte values + * bytes_to_str — decode a JSON array of byte values back to a string + * hash_sha256 — SHA-256 hex digest of a string + */ + +/* list_len — return the number of elements in a list. */ +el_val_t list_len(el_val_t list) { + return el_list_len(list); +} + +/* list_get — return the element at index i in a list. */ +el_val_t list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\"" + * or "42") to the JSON array string arr. Returns a new JSON array string. + * Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" + * json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */ +el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) { + const char* arr = EL_CSTR(arr_v); + const char* elem = EL_CSTR(elem_v); + if (!arr || !*arr) arr = "[]"; + if (!elem || !*elem) elem = "null"; + + /* Trim whitespace, find the closing ']'. */ + const char* p = arr; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { + /* Not an array — return a single-element array. */ + size_t n = strlen(elem) + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + size_t arr_len = strlen(arr); + size_t elem_len = strlen(elem); + + /* Walk from the end to find the matching ']'. */ + const char* end = arr + arr_len - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; + if (*end != ']') { + /* Malformed — wrap elem in a new array. */ + size_t n = elem_len + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + + /* Content between '[' and ']'. */ + const char* inner_start = p + 1; + const char* inner_end = end; /* points AT ']' */ + /* Check if the array is empty (only whitespace between brackets). */ + const char* q = inner_start; + while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++; + int empty = (q == inner_end); + + /* Build: prefix + (comma if non-empty) + elem + "]" */ + size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */ + size_t sep_len = empty ? 0 : 1; /* "," if non-empty */ + size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */ + char* out = el_strbuf(out_len); + memcpy(out, arr, prefix_len); + if (!empty) out[prefix_len] = ','; + memcpy(out + prefix_len + sep_len, elem, elem_len); + out[prefix_len + sep_len + elem_len] = ']'; + out[prefix_len + sep_len + elem_len + 1] = '\0'; + return el_wrap_str(out); +} + +/* now_millis — milliseconds since Unix epoch. */ +el_val_t now_millis(void) { + return time_now(); +} + +/* unix_timestamp_ms — same as now_millis. */ +el_val_t unix_timestamp_ms(void) { + return time_now(); +} + +/* time_now_ms — same as now_millis. */ +el_val_t time_now_ms(void) { + return time_now(); +} + +/* log_info — write a structured [INFO] line to stderr. */ +void log_info(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[INFO] %s\n", msg ? msg : ""); +} + +/* log_warn — write a structured [WARN] line to stderr. */ +void log_warn(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[WARN] %s\n", msg ? msg : ""); +} + +/* config — read a configuration value from the environment. + * Returns "" if the variable is not set (same as __env_get). */ +el_val_t config(el_val_t key_v) { + const char* key = EL_CSTR(key_v); + if (!key || !*key) return EL_STR(""); + const char* val = getenv(key); + if (!val) return EL_STR(""); + return el_wrap_str(el_strdup(val)); +} + +#ifdef HAVE_CURL +/* http_patch — HTTP PATCH request with Content-Type: application/json. + * Returns the response body (same error convention as http_post_json). */ +el_val_t http_patch(el_val_t url_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_post_engram — HTTP POST with optional X-API-Key header. + * If key is "" no authentication header is sent. */ +el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_get_engram — HTTP GET with optional X-API-Key header. */ +el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + if (h) curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} +#endif /* HAVE_CURL */ + +/* str_to_bytes — encode a string as a JSON array of unsigned byte values. + * "hello" -> "[104,101,108,108,111]" + * Used by db.el to store binary content in Engram JSON nodes. */ +el_val_t str_to_bytes(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return el_wrap_str(el_strdup("[]")); + size_t n = strlen(s); + /* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */ + char* out = el_strbuf(n * 4 + 3); + size_t pos = 0; + out[pos++] = '['; + for (size_t i = 0; i < n; i++) { + unsigned char b = (unsigned char)s[i]; + if (i > 0) out[pos++] = ','; + /* Write decimal representation of b. */ + if (b >= 100) { + out[pos++] = (char)('0' + b / 100); + out[pos++] = (char)('0' + (b / 10) % 10); + out[pos++] = (char)('0' + b % 10); + } else if (b >= 10) { + out[pos++] = (char)('0' + b / 10); + out[pos++] = (char)('0' + b % 10); + } else { + out[pos++] = (char)('0' + b); + } + } + out[pos++] = ']'; + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* bytes_to_str — decode a JSON array of integer byte values back to a string. + * "[104,101,108,108,111]" -> "hello" + * Inverse of str_to_bytes. */ +el_val_t bytes_to_str(el_val_t arr_v) { + const char* s = EL_CSTR(arr_v); + if (!s) return el_wrap_str(el_strdup("")); + /* Skip whitespace, expect '['. */ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + + /* Count elements to size the output buffer. */ + int64_t n = (int64_t)json_array_len(arr_v); + if (n <= 0) return el_wrap_str(el_strdup("")); + + char* out = el_strbuf((size_t)n + 1); + size_t pos = 0; + + /* Walk the array, parse each integer, store as a byte. */ + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']' || *s == '\0') break; + /* Parse decimal integer. */ + char* end_ptr; + long v = strtol(s, &end_ptr, 10); + if (end_ptr == s) break; /* parse failure */ + s = end_ptr; + if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* hash_sha256 — return the SHA-256 hex digest of a string. + * Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */ +el_val_t hash_sha256(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)s, strlen(s), digest); + return el_hex_encode(digest, 32); +} + +/* ── __ prefixed aliases — public boundary for compiled El programs ────────── + * + * The El compiler's self-hosting back-end emits calls to __-prefixed function + * names (e.g. __println, __str_len). These wrappers forward to the existing + * el_runtime implementations so both naming conventions resolve at link time. + * + * Note: __thread_create and __thread_join are already defined above in the + * threading section; they are not repeated here. + * ──────────────────────────────────────────────────────────────────────────── */ + +/* I/O */ +el_val_t __println(el_val_t s) { return println(s); } +el_val_t __print(el_val_t s) { return print(s); } +el_val_t __readline(void) { return readline(); } + +/* String */ +el_val_t __int_to_str(el_val_t n) { return int_to_str(n); } +el_val_t __str_to_int(el_val_t s) { return str_to_int(s); } +el_val_t __float_to_str(el_val_t f) { return float_to_str(f); } +el_val_t __str_to_float(el_val_t s) { return str_to_float(s); } +el_val_t __str_len(el_val_t s) { return str_len(s); } +el_val_t __str_char_at(el_val_t s, el_val_t i) { return str_char_at(s, i); } + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strcmp(ca, cb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* ca = EL_CSTR(a); + const char* cb = EL_CSTR(b); + if (!ca) ca = ""; + if (!cb) cb = ""; + return (el_val_t)strncmp(ca, cb, (size_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { return str_concat(a, b); } +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { return str_slice(s, start, end); } + +el_val_t __str_alloc(el_val_t n) { + if (n <= 0) n = 0; + char* buf = el_strbuf((size_t)n + 1); + memset(buf, 0, (size_t)n + 1); + return el_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* buf = (char*)(uintptr_t)s; + if (buf) buf[(size_t)i] = (char)c; + return s; +} + +/* URL encoding */ +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* Environment */ +el_val_t __env_get(el_val_t key) { return env(key); } + +/* Subprocess */ +el_val_t __exec(el_val_t cmd) { return exec(cmd); } +el_val_t __exec_bg(el_val_t cmd) { return exec_bg(cmd); } + +/* Process */ +el_val_t __exit_program(el_val_t code) { return exit_program(code); } + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path) { return fs_exists(path); } +el_val_t __fs_mkdir(el_val_t path) { return fs_mkdir(path); } +el_val_t __fs_read(el_val_t path) { return fs_read(path); } +el_val_t __fs_write(el_val_t path, el_val_t content) { return fs_write(path, content); } +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return fs_write_bytes(path, bytes, n); } +el_val_t __fs_list_raw(el_val_t path) { return fs_list_json(path); } + +/* HTTP server (no curl dependency) */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return http_response(status, headers_json, body); } +el_val_t __http_serve(el_val_t port, el_val_t handler) { return http_serve(port, handler); } +el_val_t __http_serve_v2(el_val_t port, el_val_t handler) { return http_serve_v2(port, handler); } + +/* HTTP conn fd / SSE — __http_conn_fd lives in el_seed.c; stubs provided here + * so el_runtime.c compiles standalone. When both translation units are linked + * the el_seed.c definitions win via their non-static linkage (strong symbols). + * These stubs are marked weak so they are silently overridden. */ +__attribute__((weak)) el_val_t __http_conn_fd(void) { return (el_val_t)(-1); } +__attribute__((weak)) el_val_t __http_sse_open(el_val_t conn_id) { (void)conn_id; return 0; } +__attribute__((weak)) el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { (void)conn_id; (void)data; return 0; } +__attribute__((weak)) el_val_t __http_sse_close(el_val_t conn_id) { (void)conn_id; return 0; } + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index) { return json_array_get(json, index); } +el_val_t __json_array_get_string(el_val_t json, el_val_t index) { return json_array_get_string(json, index); } +el_val_t __json_array_len(el_val_t json) { return json_array_len(json); } +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json, el_val_t key) { return json_get_raw(json, key); } +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value){ return json_set(json, key, value); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s) { return hash_sha256(s); } + +/* State K/V */ +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_keys(void) { return state_keys(); } +el_val_t __state_set(el_val_t key, el_val_t val) { return state_set(key, val); } + +/* UUID */ +el_val_t __uuid_v4(void) { return uuid_v4(); } + +/* Args */ +el_val_t __args_json(void) { return args(); } + +/* HTTP client aliases — require curl; defined inside #ifdef HAVE_CURL below + * with a matching stub in the #ifndef HAVE_CURL block. */ +#ifdef HAVE_CURL +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms) { + /* timeout_ms is accepted for API compatibility but ignored here; + * el_runtime's http_do uses the EL_HTTP_TIMEOUT_MS env var instead. */ + (void)timeout_ms; + struct curl_slist* h = headers_from_map(headers_map); + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map — same as __http_do but headers_map arg is a JSON-string + * rather than an ElMap. Parse it first, then delegate. */ +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + (void)timeout_ms; + /* Build a curl_slist from a JSON object {"Header":"value",...}. */ + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + /* Walk the JSON pairs with a simple parser reusing json_get_string logic. */ + /* For correctness we just call the existing json_get iteration path. + * We duplicate the key-extraction loop from headers_from_map but driven + * by JSON rather than ElMap. Use json_get_raw to iterate is not easy + * without knowing keys, so accept the JSON string and build a tmp map. */ + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), h); + if (h) curl_slist_free_all(h); + return r; +} + +/* __http_do_map_to_file — same as __http_do_map but streams response body + * to a local file path rather than returning it as a string. */ +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path) { + const char* hj = EL_CSTR(headers_json); + struct curl_slist* h = NULL; + if (hj && *hj && *hj == '{') { + el_val_t map = json_parse(EL_STR(hj)); + h = headers_from_map(map); + } + el_val_t r = http_do_to_file(EL_CSTR(method), EL_CSTR(url), EL_CSTR(body), + h, EL_CSTR(output_path)); + if (h) curl_slist_free_all(h); + return r; +} +#endif /* HAVE_CURL */ + +#ifndef HAVE_CURL +/* ── HAVE_CURL=0 stubs — compile without -lcurl for the elc CLI binary. ───── * + * These return a JSON error string so El programs get a clear message if they + * call HTTP/LLM functions in a curl-less build. */ +static el_val_t _no_curl_err(void) { + return el_wrap_str(el_strdup("{\"error\":\"not built with HAVE_CURL\"}")); +} +el_val_t http_get(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_post(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_post_json(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { (void)url; (void)h; return _no_curl_err(); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { (void)url; (void)b; (void)h; return _no_curl_err(); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { (void)url; (void)h; (void)b; return _no_curl_err(); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { (void)url; (void)b; (void)a; return _no_curl_err(); } +el_val_t http_delete(el_val_t url) { (void)url; return _no_curl_err(); } +el_val_t http_patch(el_val_t url, el_val_t body) { (void)url; (void)body; return _no_curl_err(); } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t p) { (void)url; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t p) { (void)url; (void)b; (void)h; (void)p; return _no_curl_err(); } +el_val_t http_post_engram(el_val_t url, el_val_t k, el_val_t b) { (void)url; (void)k; (void)b; return _no_curl_err(); } +el_val_t http_get_engram(el_val_t url, el_val_t k) { (void)url; (void)k; return _no_curl_err(); } +el_val_t llm_call(el_val_t m, el_val_t p) { (void)m; (void)p; return _no_curl_err(); } +el_val_t llm_call_system(el_val_t m, el_val_t s, el_val_t u) { (void)m; (void)s; (void)u; return _no_curl_err(); } +el_val_t llm_call_agentic(el_val_t m, el_val_t s, el_val_t u, el_val_t t) { (void)m; (void)s; (void)u; (void)t; return _no_curl_err(); } +el_val_t llm_vision(el_val_t m, el_val_t s, el_val_t p, el_val_t i) { (void)m; (void)s; (void)p; (void)i; return _no_curl_err(); } +el_val_t llm_models(void) { return el_list_empty(); } +void llm_register_tool(el_val_t n, el_val_t f) { (void)n; (void)f; } +/* __ HTTP stubs (no-curl build) */ +el_val_t __http_do(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t t) { (void)m; (void)u; (void)b; (void)h; (void)t; return _no_curl_err(); } +el_val_t __http_do_map_to_file(el_val_t m, el_val_t u, el_val_t b, el_val_t h, el_val_t p) { (void)m; (void)u; (void)b; (void)h; (void)p; return _no_curl_err(); } +#endif /* !HAVE_CURL */ diff --git a/ui/examples/native-hello/build-docker/runtime/el_runtime.h b/ui/examples/native-hello/build-docker/runtime/el_runtime.h new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_runtime.h @@ -0,0 +1,883 @@ +/* + * el_runtime.h — El language C runtime header + * + * Declares all built-in functions available to compiled El programs. + * Include this in every generated .c file. + * + * Value model: + * All El values are represented as el_val_t (= int64_t). + * On 64-bit systems a pointer fits in int64_t. + * String values are cast: (el_val_t)(uintptr_t)"hello" + * Integer values are stored directly. + * This lets arithmetic work naturally while still passing strings around. + * + * Type conventions (El -> C): + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t + * Bool -> el_val_t (0 = false, nonzero = true) + * Any -> el_val_t + * Void -> void + * + * Macros for convenience: + * EL_STR(s) cast string literal to el_val_t + * EL_CSTR(v) cast el_val_t back to const char* + * EL_INT(v) identity — el_val_t is already int64_t + * EL_NULL null / zero value + * EL_FALSE boolean false (0) + * EL_TRUE boolean true (1) + * + * Link requirements: + * -lcurl — required for the HTTP client (http_get, http_post, llm_*). + * -lpthread — required for the HTTP server (one detached thread per + * connection, capped at 64 concurrent). + * -loqs — optional; required only when liboqs is installed and the + * pq_* / sha3_256_hex entry points are needed. Detected at + * compile time via __has_include(). + * -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in + * pq_hybrid_* and HKDF-SHA256 derivation. + * + * Canonical compile command: + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el-compiler/runtime/el_runtime.c + * + * With liboqs (post-quantum stack): + * cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \ + * -o .c el-compiler/runtime/el_runtime.c + */ + +#pragma once + +#include +#include + +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#define EL_INT(v) (v) +#define EL_NULL ((el_val_t)0) +#define EL_FALSE ((el_val_t)0) +#define EL_TRUE ((el_val_t)1) + +/* Float values share the el_val_t (int64) slot via a bit-cast. + * The codegen emits Float literals as `el_from_float()` so the + * underlying bits represent the IEEE 754 double. Float-aware builtins + * (math, format, json) round-trip via these helpers. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── I/O ──────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s); +el_val_t print(el_val_t s); +el_val_t readline(void); + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t a, el_val_t b); +el_val_t str_eq(el_val_t a, el_val_t b); +el_val_t str_starts_with(el_val_t s, el_val_t prefix); +el_val_t str_ends_with(el_val_t s, el_val_t suffix); +el_val_t str_len(el_val_t s); +el_val_t str_concat(el_val_t a, el_val_t b); +el_val_t int_to_str(el_val_t n); +el_val_t str_to_int(el_val_t s); +el_val_t native_str_to_int(el_val_t s); +el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end); +el_val_t str_contains(el_val_t s, el_val_t sub); +el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to); +el_val_t str_to_upper(el_val_t s); +el_val_t str_to_lower(el_val_t s); +el_val_t str_trim(el_val_t s); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n); +el_val_t el_max(el_val_t a, el_val_t b); +el_val_t el_min(el_val_t a, el_val_t b); + +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +void el_retain(el_val_t v); +void el_release(el_val_t v); + +/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */ +el_val_t el_arena_push(void); +el_val_t el_arena_pop(el_val_t mark); + +/* ── List ────────────────────────────────────────────────────────────────── */ + +el_val_t el_list_new(el_val_t count, ...); +el_val_t el_list_len(el_val_t list); +el_val_t el_list_get(el_val_t list, el_val_t index); +el_val_t el_list_append(el_val_t list, el_val_t elem); +el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +el_val_t el_map_new(el_val_t pair_count, ...); +el_val_t el_get_field(el_val_t map, el_val_t key); +el_val_t el_map_get(el_val_t map, el_val_t key); +el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value); + +/* ── HTTP ─────────────────────────────────────────────────────────────────── */ + +el_val_t http_get(el_val_t url); +el_val_t http_post(el_val_t url, el_val_t body); +el_val_t http_post_json(el_val_t url, el_val_t json_body); +el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map); +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map); +el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body); +el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header); +el_val_t http_delete(el_val_t url); +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_set_handler(el_val_t name); + +/* HTTP server v2 ───────────────────────────────────────────────────────────── + * Same dispatch model as http_serve, but the handler signature is widened: + * + * el_val_t handler(method, path, headers_map, body) + * + * `headers_map` is an ElMap from lowercased header name → header value (both + * Strings). Repeated headers are joined with ", " per RFC 7230. + * + * Response value: the handler may return either + * (a) a plain body string — same auto-content-type / 200-OK behaviour as + * http_serve (3-arg) — or + * (b) a response envelope built with `http_response(status, headers_json, + * body)`. The runtime detects the envelope discriminator + * `"el_http_response":1` at the start of the returned string and + * unpacks status / headers / body before sending. + * + * The 3-arg http_serve(port, handler) remains supported unchanged for + * existing handlers (e.g. products/web/server.el): it dispatches with + * (method, path, body), hardcodes 200 OK, and auto-detects content type. */ +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_set_handler_v2(el_val_t name); + +/* Build an HTTP response envelope. `headers_json` should be a JSON object + * literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The + * returned string carries the discriminator `{"el_http_response":1,...}` + * which the runtime's send-path detects and unpacks. Detection happens + * uniformly inside http_send_response, so a 3-arg handler may also return + * an envelope. The 3-arg variant remains documented as a fixed 200-OK + * auto-content-type contract for legacy handlers that return plain bodies. */ +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* SSE connection fd — set by http_worker_v2 before calling the El handler, + * cleared afterwards. Defined in el_seed.c; called from el_runtime.c. + * The getter is exposed as __http_conn_fd() to El programs. */ +void el_seed_set_http_conn_fd(int fd); + +/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default + * 60000ms). Read lazily on first use, so setting the env var any time before + * the first http_* call is sufficient. */ + +/* Streaming variants — write the response body straight to a file via + * libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string + * wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive + * embedded NUL bytes that would truncate a strlen()-based code path. + * + * Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same + * `headers_map` shape as http_post_with_headers (ElMap of String→String). + * + * Return value: 1 on success (file fully written), 0 on any failure + * (network, file open, partial write). On failure the output file is removed + * so callers cannot mistake a partially-written file for a valid one. */ +el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path); +el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path); + +/* ── URL encoding ────────────────────────────────────────────────────────── */ + +el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */ +el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ + +/* ── HTML allowlist sanitizer ──────────────────────────────────────────────── + * el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML + * cleaner. State-machine parser; tag/attribute names compared case- + * insensitively against the allowlist; `` / `<… src>` URL schemes + * validated (http, https, mailto, fragment-only, or relative); whole- + * subtree drop for script / style / iframe / object / embed / form; HTML- + * escapes free text outside dropped subtrees. + * + * The allowlist is JSON of the form + * {"p":[],"a":["href","title"],"strong":[],...} + * where each value is the array of attribute names allowed for that tag. */ +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t html_raw(el_val_t s); +el_val_t html_escape(el_val_t s); + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t path); +el_val_t fs_write(el_val_t path, el_val_t content); +el_val_t fs_list(el_val_t path); +el_val_t fs_list_json(el_val_t path); +el_val_t fs_exists(el_val_t path); +el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */ + +/* Length-explicit binary write. `length` is an Int (el_val_t holding the + * byte count). The caller knows the length from context — typically because + * `bytes` came from base64_decode (which produces a magic-tagged binary + * buffer with embedded NULs possible) and the caller already tracks the + * decoded length, OR because the bytes came from a fixed-size source + * (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely. + * + * Returns 1 on success, 0 on failure (invalid path, can't open, partial + * write, negative length). On partial-write failure, the file is removed + * so callers cannot read back a truncated artefact. */ +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_escape_string(el_val_t sv); +el_val_t json_build_object(el_val_t kvs); +el_val_t json_build_array(el_val_t items); + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t sleep_secs(el_val_t secs); +el_val_t sleep_ms(el_val_t ms); +el_val_t time_format(el_val_t ts, el_val_t fmt); +el_val_t time_to_parts(el_val_t ts); +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz); +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit); +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit); +el_val_t now_ns(void); + +/* ── Instant + Duration: first-class temporal types ────────────────────────── + * Both types share the el_val_t (int64) slot. Instants are nanoseconds + * since the Unix epoch; Durations are signed nanoseconds. Type discipline + * is enforced at codegen-time: BinOps on names registered as Instant or + * Duration route through the typed wrappers below; mismatches like + * Instant+Instant become #error at the C compiler. + * + * Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are + * recognised by the parser as DurationLit AST nodes and lowered to literal + * int64 nanoseconds at codegen time. The runtime never sees the units. */ + +el_val_t el_now_instant(void); +el_val_t now(void); +el_val_t unix_seconds(el_val_t n); +el_val_t unix_millis(el_val_t n); +el_val_t instant_from_iso8601(el_val_t s); + +el_val_t el_duration_from_nanos(el_val_t ns); +el_val_t duration_seconds(el_val_t n); +el_val_t duration_millis(el_val_t n); +el_val_t duration_nanos(el_val_t n); + +el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur); +el_val_t el_instant_diff(el_val_t a, el_val_t b); +el_val_t el_duration_add(el_val_t a, el_val_t b); +el_val_t el_duration_sub(el_val_t a, el_val_t b); +el_val_t el_duration_scale(el_val_t dur, el_val_t scalar); +el_val_t el_duration_div(el_val_t dur, el_val_t scalar); + +el_val_t el_instant_lt(el_val_t a, el_val_t b); +el_val_t el_instant_le(el_val_t a, el_val_t b); +el_val_t el_instant_gt(el_val_t a, el_val_t b); +el_val_t el_instant_ge(el_val_t a, el_val_t b); +el_val_t el_instant_eq(el_val_t a, el_val_t b); +el_val_t el_instant_ne(el_val_t a, el_val_t b); +el_val_t el_duration_lt(el_val_t a, el_val_t b); +el_val_t el_duration_le(el_val_t a, el_val_t b); +el_val_t el_duration_gt(el_val_t a, el_val_t b); +el_val_t el_duration_ge(el_val_t a, el_val_t b); +el_val_t el_duration_eq(el_val_t a, el_val_t b); +el_val_t el_duration_ne(el_val_t a, el_val_t b); + +el_val_t instant_to_unix_seconds(el_val_t i); +el_val_t instant_to_unix_millis(el_val_t i); +el_val_t instant_to_iso8601(el_val_t i); +el_val_t duration_to_seconds(el_val_t d); +el_val_t duration_to_millis(el_val_t d); +el_val_t duration_to_nanos(el_val_t d); + +el_val_t el_sleep_duration(el_val_t dur); +el_val_t unix_timestamp(void); + +el_val_t ttl_cache_set(el_val_t key, el_val_t value); +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age); +el_val_t ttl_cache_age(el_val_t key); + +/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ───────────── + * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA + * zones, Gregorian, DST) is the user-facing default; MarsCalendar, + * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth + * domains. + * + * A Calendar interprets an Instant under a particular cycle convention and + * produces a CalendarTime. CalendarTime carries the underlying Instant and + * a back-pointer to its Calendar; arithmetic and formatting consult the + * Calendar to convert ns since epoch into year/month/day/hour/minute/second + * (or sol/phase, or cycle/phase, depending on kind). + * + * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate / + * LocalDateTime are heap-allocated structs whose pointers are cast into + * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify + * the kind safely. LocalTime is small enough to live in the int64 slot + * directly (nanos since midnight, signed). */ + +/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar. + * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed + * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset() + * on first use of the owning EarthCalendar. */ +el_val_t zone(el_val_t id); +el_val_t zone_utc(void); +el_val_t zone_local(void); +el_val_t zone_offset(el_val_t hours, el_val_t minutes); + +/* Calendar constructors. Each returns an el_val_t pointer to a heap- + * allocated, magic-tagged Calendar struct. Calendars are interned by + * (kind, zone_id, period_ns, epoch_ns) so identical constructors return + * the same pointer — equality is reference equality. */ +el_val_t earth_calendar(el_val_t z); +el_val_t earth_calendar_default(void); +el_val_t mars_calendar(void); +el_val_t cycle_calendar(el_val_t period_dur); +el_val_t no_cycle_calendar(void); +el_val_t relative_calendar(el_val_t epoch_inst); + +/* CalendarTime constructors and methods. Returns a heap-allocated struct + * whose pointer fits in el_val_t. */ +el_val_t now_in(el_val_t cal); +el_val_t in_calendar(el_val_t inst, el_val_t cal); +el_val_t cal_format(el_val_t ct, el_val_t pattern); +el_val_t cal_to_instant(el_val_t ct); +el_val_t cal_cycle_phase(el_val_t ct); +el_val_t cal_in(el_val_t ct, el_val_t cal); + +/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types. + * LocalTime carries nanoseconds since midnight as a signed int64 directly + * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are + * heap-allocated structs with magic headers. */ +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d); +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns); +el_val_t local_datetime(el_val_t date, el_val_t time); +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal); + +el_val_t local_date_year(el_val_t ld); +el_val_t local_date_month(el_val_t ld); +el_val_t local_date_day(el_val_t ld); +el_val_t local_time_hour(el_val_t lt); +el_val_t local_time_minute(el_val_t lt); +el_val_t local_time_second(el_val_t lt); +el_val_t local_time_nanos(el_val_t lt); + +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur); +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur); +el_val_t el_local_date_lt(el_val_t a, el_val_t b); +el_val_t el_local_date_eq(el_val_t a, el_val_t b); + +/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct + * pointer in el_val_t; rhythms are immutable so callers may share them. */ +el_val_t rhythm_cycle_start(void); +el_val_t rhythm_cycle_phase(el_val_t phase); +el_val_t rhythm_duration(el_val_t d); +el_val_t rhythm_session_start(void); +el_val_t rhythm_event(el_val_t name); +el_val_t rhythm_and(el_val_t a, el_val_t b); +el_val_t rhythm_or(el_val_t a, el_val_t b); +el_val_t rhythm_weekday(el_val_t day); +el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute); +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal); +el_val_t rhythm_matches(el_val_t r, el_val_t ct); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_has(el_val_t key); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t fmt, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes) + * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex. + * is_* predicates: empty input returns false; multi-char requires ALL bytes + * to match. ASCII ranges only in Phase 1. */ + +/* Counting */ +el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */ +el_val_t str_count_chars(el_val_t s); /* codepoint count */ +el_val_t str_count_bytes(el_val_t s); /* alias of str_len */ +el_val_t str_count_lines(el_val_t s); +el_val_t str_count_words(el_val_t s); +el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */ +el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */ + +/* Find / position */ +el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */ +el_val_t str_last_index_of(el_val_t s, el_val_t sub); +el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */ + +/* Transform */ +el_val_t str_repeat(el_val_t s, el_val_t n); +el_val_t str_reverse(el_val_t s); /* by codepoint */ +el_val_t str_strip_prefix(el_val_t s, el_val_t prefix); +el_val_t str_strip_suffix(el_val_t s, el_val_t suffix); +el_val_t str_strip_chars(el_val_t s, el_val_t chars); +el_val_t str_lstrip(el_val_t s); +el_val_t str_rstrip(el_val_t s); + +/* Char classification (Bool) */ +el_val_t is_letter(el_val_t s); +el_val_t is_digit(el_val_t s); +el_val_t is_alphanumeric(el_val_t s); +el_val_t is_whitespace(el_val_t s); +el_val_t is_punctuation(el_val_t s); +el_val_t is_uppercase(el_val_t s); +el_val_t is_lowercase(el_val_t s); + +/* Split / join */ +el_val_t str_split_lines(el_val_t s); +el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */ +el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n); +el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */ + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); + +/* ── Numeric parsing ─────────────────────────────────────────────────────── */ + +el_val_t parse_int(el_val_t s, el_val_t default_val); + +/* ── Process ─────────────────────────────────────────────────────────────── */ + +el_val_t exit_program(el_val_t code); +el_val_t getpid_now(void); + +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + +/* ── CGI identity ───────────────────────────────────────────────────────────── + * Called at the start of main() in CGI programs (those with a `cgi {}` block). + * Records the program's DHARMA identity before any other code executes. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * + * Peers are addressed by `dharma_id` of the form + * "@" e.g. "ntn-genesis@http://localhost:7770" + * If the @ portion is omitted, transport defaults to + * "http://localhost:7770" (the local CGI daemon assumption). + * + * Wire protocol (all peers expose): + * POST /dharma/recv { channel, from, content } → response body + * POST /dharma/event { type, payload, source, timestamp } + * POST /api/activate { query } → list of nodes + * + * Hosting application's responsibility: an El program with a `cgi {}` block + * runs http_serve() with its own request handler; that handler should route + * "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so + * incoming events feed dharma_field() queues. The runtime itself does not + * intercept any /dharma path. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* Public C API: called by an El program's HTTP handler when a /dharma/event + * request arrives. Pushes onto the per-event-type queue and signals any + * pending dharma_field() blockers. All three arguments must be NUL-terminated + * C strings (or NULL — then treated as empty). */ +void el_runtime_dharma_event_arrive(const char* event_type, + const char* payload, + const char* source); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +/* Layered consciousness — see el_runtime.c for the layered architecture + * design notes (search "Layered consciousness architecture"). The five + * canonical layers (safety / core-identity / domain-knowledge / imprint / + * suit) are seeded automatically; engram_add_layer extends the registry + * with imprint or suit overlays at runtime. Nodes default to layer 1 + * (core-identity) when created via engram_node / engram_node_full. */ +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +/* Three-pass activation: background fan-out → working-memory promotion → + * Layer 0 override. See "Three-pass activation" in el_runtime.c. */ +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); + +/* JSON-string accessors — return pre-serialized JSON so HTTP handlers + * can pass results straight through without round-tripping ElList/ElMap + * through json_stringify. */ +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +/* engram_compile_layered_json — produce a prompt-ready text block split + * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) + * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if + * no nodes promoted to working memory. */ +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── LLM (Anthropic API client) ───────────────────────────────────────────── + * All functions call https://api.anthropic.com/v1/messages with the API key + * from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */ + +el_val_t llm_call(el_val_t model, el_val_t prompt); +el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt); +el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools); +el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64); +el_val_t llm_models(void); + +/* Register a tool handler by name. The handler is looked up via dlsym + * (mirroring http_set_handler), so any El `fn (input)` compiles to + * a global C symbol that this function can locate at runtime. + * Handler signature: `el_val_t handler(el_val_t input_json)` — receives + * the tool input as a JSON-string el_val_t and returns a JSON-string + * el_val_t result. Used by llm_call_agentic. */ +void llm_register_tool(el_val_t name, el_val_t handler_fn_name); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Crypto primitives ───────────────────────────────────────────────────── + * SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe). + * Self-contained — no OpenSSL/libcrypto dependency. The implementations are + * adapted from public-domain reference code (Brad Conte / RFC 4648). + * + * Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string + * value whose contents are raw binary; callers usually feed these into + * base64_encode. Note that el_val_t strings are NUL-terminated by convention, + * so the binary payload may contain embedded NULs — pass it directly into + * base64_encode (which uses an explicit length) rather than treating it as + * a printable C string. + * + * The "base64" variants emit/accept RFC 4648 standard alphabet with padding. + * The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding, + * as used in JWTs. */ + +el_val_t sha256_hex(el_val_t input); +el_val_t sha256_bytes(el_val_t input); +el_val_t hmac_sha256_hex(el_val_t key, el_val_t message); +el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message); +el_val_t base64_encode(el_val_t input); +el_val_t base64_decode(el_val_t input); +el_val_t base64url_encode(el_val_t input); +el_val_t base64url_decode(el_val_t input); + +/* Length-aware variants (internal — exposed for the rare caller that already + * has a known-length binary buffer and doesn't want to round-trip through + * a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed + * these implicitly. */ +el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len); +el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe); + +/* ── Post-quantum primitives (liboqs-backed) ──────────────────────────────── + * All inputs/outputs hex-encoded. Algorithm choices: + * Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced) + * KEM: CRYSTALS-Kyber-768 (NIST level 3) + * Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2) + * + * If liboqs is not linked (detected via __has_include() at compile + * time), the pq_* entry points return a JSON-shaped error string so callers + * fail loudly rather than silently fall back to classical schemes: + * {"error":"liboqs not linked, post-quantum primitives unavailable"} + * + * The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and + * CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss). + * Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack, + * Kyber holds. SHA3-256 also remains usable independent of liboqs (the + * Keccak permutation is PQ-OK as a primitive). */ + +el_val_t pq_keygen_signature(void); +el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message); +el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex); + +el_val_t pq_kem_keygen(void); +el_val_t pq_kem_encaps(el_val_t public_key_hex); +el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex); + +el_val_t pq_hybrid_keygen(void); +el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined); + +el_val_t sha3_256_hex(el_val_t input); + +/* ── AEAD: AES-256-GCM (libcrypto-backed) ─────────────────────────────────── + * Symmetric authenticated encryption used to wrap envelopes after a KEM + * handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the + * Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256. + * + * aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where + * ciphertext is the AES-256-GCM output with the 16-byte auth tag appended. + * Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which + * structurally rules out the GCM nonce-reuse footgun. + * + * aead_decrypt returns the plaintext String, or "" on any failure (including + * auth-tag mismatch). Callers MUST check for "" before trusting the result. */ +el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext); +el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + +/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */ +/* See bottom of el_runtime.c for the implementation. + * Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION. + * No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */ +/* ── Subprocess execution ────────────────────────────────────────────────── */ +el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */ +el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */ +el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */ +el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */ + +/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */ +el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */ +el_val_t stdout_restore(void); /* restore process stdout to terminal */ + +el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json); +el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json); +el_val_t trace_span_start(el_val_t name); +el_val_t trace_span_end(el_val_t span_handle); +el_val_t emit_event(el_val_t name, el_val_t duration_ms); + +el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v); +el_val_t __thread_join(el_val_t tid_v); + +/* ── __ prefixed aliases (self-hosting compiler ABI) ───────────────────────── + * The El self-hosting compiler emits calls to __-prefixed names. These are + * forwarding wrappers around the existing el_runtime functions above. */ + +/* I/O */ +el_val_t __println(el_val_t s); +el_val_t __print(el_val_t s); +el_val_t __readline(void); + +/* String */ +el_val_t __int_to_str(el_val_t n); +el_val_t __str_to_int(el_val_t s); +el_val_t __float_to_str(el_val_t f); +el_val_t __str_to_float(el_val_t s); +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); +el_val_t __str_cmp(el_val_t a, el_val_t b); +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); +el_val_t __str_concat_raw(el_val_t a, el_val_t b); +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); +el_val_t __str_alloc(el_val_t n); +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); + +/* URL encoding */ +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* Environment */ +el_val_t __env_get(el_val_t key); + +/* Subprocess */ +el_val_t __exec(el_val_t cmd); +el_val_t __exec_bg(el_val_t cmd); + +/* Process */ +el_val_t __exit_program(el_val_t code); + +/* Filesystem */ +el_val_t __fs_exists(el_val_t path); +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_read(el_val_t path); +el_val_t __fs_write(el_val_t path, el_val_t content); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); +el_val_t __fs_list_raw(el_val_t path); + +/* HTTP server */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); +el_val_t __http_serve(el_val_t port, el_val_t handler); +el_val_t __http_serve_v2(el_val_t port, el_val_t handler); + +/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */ +el_val_t __http_conn_fd(void); +el_val_t __http_sse_open(el_val_t conn_id); +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); +el_val_t __http_sse_close(el_val_t conn_id); + +/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_map, el_val_t timeout_ms); +el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); +el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t output_path); + +/* JSON */ +el_val_t __json_array_get(el_val_t json, el_val_t index); +el_val_t __json_array_get_string(el_val_t json, el_val_t index); +el_val_t __json_array_len(el_val_t json); +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json, el_val_t key); +el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value); +el_val_t __json_parse_map(el_val_t json_str); +el_val_t __json_stringify_val(el_val_t val); + +/* Hashing */ +el_val_t __sha256_hex(el_val_t s); + +/* State K/V */ +el_val_t __state_del(el_val_t key); +el_val_t __state_get(el_val_t key); +el_val_t __state_keys(void); +el_val_t __state_set(el_val_t key, el_val_t val); + +/* UUID */ +el_val_t __uuid_v4(void); + +/* Args */ +el_val_t __args_json(void); + +#ifdef __cplusplus +} +#endif diff --git a/ui/examples/native-hello/build-docker/runtime/el_seed.c b/ui/examples/native-hello/build-docker/runtime/el_seed.c new file mode 100644 index 0000000..fc9fbba --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_seed.c @@ -0,0 +1,1942 @@ +/* + * el_seed.c — El language seed runtime: minimal C OS boundary + * + * This file exposes all OS-boundary primitives that El programs need, under + * the __ prefix convention. It is self-contained: all allocators, arena + * management, and el_request_start / el_request_end are defined here. + * + * Threading: __thread_create / __thread_join use dlsym(RTLD_DEFAULT) to look + * up El function symbols at runtime. This is the foundation of El's parallelism. + * + * Link: cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ + * -o .c el_seed.c + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── Private allocator ───────────────────────────────────────────────────── */ +/* + * el_seed.c carries its own arena for per-request allocation tracking. + * The arena is reset at el_request_start / el_request_end, which are defined + * below and delegate to seed_request_start / seed_request_end. + */ + +#define SEED_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} SeedArena; + +static _Thread_local SeedArena _seed_arena = {NULL, 0, 0}; +static _Thread_local int _seed_arena_on = 0; + +static void seed_arena_track(char* p) { + if (!_seed_arena_on || !p) return; + if (_seed_arena.count >= _seed_arena.cap) { + size_t nc = _seed_arena.cap == 0 ? SEED_ARENA_INITIAL : _seed_arena.cap * 2; + char** g = realloc(_seed_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _seed_arena.ptrs = g; + _seed_arena.cap = nc; + } + _seed_arena.ptrs[_seed_arena.count++] = p; +} + +static void seed_request_start(void) { + _seed_arena.count = 0; + _seed_arena_on = 1; +} + +static void seed_request_end(void) { + _seed_arena_on = 0; + for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]); + _seed_arena.count = 0; +} + +/* el_request_start / el_request_end — formerly defined in el_runtime.c. + * Now self-contained in el_seed.c, delegating to the seed arena. */ +void el_request_start(void) { seed_request_start(); } +void el_request_end(void) { seed_request_end(); } + +/* Persistent alloc — bypasses arena (state, engram internals). */ +static char* seed_strdup_persist(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* seed_strdup(const char* s) { + char* p = strdup(s ? s : ""); + seed_arena_track(p); + return p; +} + +static char* seed_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_seed: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + seed_arena_track(p); + return p; +} + +static el_val_t seed_wrap_str(char* s) { return EL_STR(s); } + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)strlen(p); +} + +el_val_t __str_char_at(el_val_t s, el_val_t i) { + const char* p = EL_CSTR(s); + if (!p) return 0; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)p[idx]; +} + +el_val_t __str_alloc(el_val_t n) { + int64_t sz = (int64_t)n; + if (sz < 0) sz = 0; + char* buf = seed_strbuf((size_t)sz); + memset(buf, 0, (size_t)sz + 1); + return seed_wrap_str(buf); +} + +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) { + char* p = (char*)(uintptr_t)s; + if (!p) return s; + int64_t len = (int64_t)strlen(p); + int64_t idx = (int64_t)i; + if (idx < 0 || idx >= len) return s; + p[idx] = (char)(unsigned char)(int64_t)c; + return s; +} + +el_val_t __str_cmp(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strcmp(sa, sb); +} + +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + return (el_val_t)strncmp(sa, sb, (size_t)(int64_t)n); +} + +el_val_t __str_concat_raw(el_val_t a, el_val_t b) { + const char* sa = EL_CSTR(a); + const char* sb = EL_CSTR(b); + if (!sa) sa = ""; + if (!sb) sb = ""; + size_t la = strlen(sa), lb = strlen(sb); + char* out = seed_strbuf(la + lb); + memcpy(out, sa, la); + memcpy(out + la, sb, lb); + out[la + lb] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end) { + const char* p = EL_CSTR(s); + if (!p) return seed_wrap_str(seed_strdup("")); + int64_t len = (int64_t)strlen(p); + int64_t st = (int64_t)start; + int64_t en = (int64_t)end; + if (st < 0) st = 0; + if (en > len) en = len; + if (st >= en) return seed_wrap_str(seed_strdup("")); + int64_t sz = en - st; + char* out = seed_strbuf((size_t)sz); + memcpy(out, p + st, (size_t)sz); + out[sz] = '\0'; + return seed_wrap_str(out); +} + +el_val_t __int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)(int64_t)n); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_int(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return 0; + return (el_val_t)atoll(p); +} + +el_val_t __float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return seed_wrap_str(seed_strdup(buf)); +} + +el_val_t __str_to_float(el_val_t s) { + const char* p = EL_CSTR(s); + if (!p) return el_from_float(0.0); + return el_from_float(strtod(p, NULL)); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s) { + const char* p = EL_CSTR(s); + puts(p ? p : ""); +} + +void __print(el_val_t s) { + const char* p = EL_CSTR(s); + if (p) fputs(p, stdout); +} + +el_val_t __readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return seed_wrap_str(seed_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return seed_wrap_str(seed_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz < 0) { fclose(f); return seed_wrap_str(seed_strdup("")); } + char* buf = seed_strbuf((size_t)sz); + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + return seed_wrap_str(buf); +} + +el_val_t __fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t n = strlen(c); + size_t w = fwrite(c, 1, n, f); + fclose(f); + return w == n ? 1 : 0; +} + +el_val_t __fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + struct stat st; + return (el_val_t)(stat(p, &st) == 0 ? 1 : 0); +} + +el_val_t __fs_list_raw(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return seed_wrap_str(seed_strdup("")); + DIR* d = opendir(p); + if (!d) return seed_wrap_str(seed_strdup("")); + /* Build newline-separated list of filenames. */ + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { closedir(d); return seed_wrap_str(seed_strdup("")); } + buf[0] = '\0'; + struct dirent* e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + size_t nlen = strlen(e->d_name); + while (len + nlen + 2 >= cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); closedir(d); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + if (len > 0) buf[len++] = '\n'; + memcpy(buf + len, e->d_name, nlen); + len += nlen; + buf[len] = '\0'; + } + closedir(d); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +el_val_t __fs_mkdir(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return 0; + size_t n = strlen(p); + char* buf = malloc(n + 1); + if (!buf) return 0; + memcpy(buf, p, n + 1); + for (size_t i = 1; i <= n; i++) { + if (buf[i] == '/' || buf[i] == '\0') { + char saved = buf[i]; + buf[i] = '\0'; + if (buf[0] != '\0') { + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + struct stat st; + if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) { + free(buf); return 0; + } + } + } + buf[i] = saved; + } + } + free(buf); + return 1; +} + +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { + const char* p = EL_CSTR(path); + const char* b = EL_CSTR(bytes); + int64_t sz = (int64_t)n; + if (!p || !b || sz < 0) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + size_t written = (sz > 0) ? fwrite(b, 1, (size_t)sz, f) : 0; + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + if (!ok1 || !ok2 || written != (size_t)sz) { remove(p); return 0; } + return 1; +} + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +typedef struct { + char* data; + size_t len; + size_t cap; +} SeedHttpBuf; + +static void seed_httpbuf_init(SeedHttpBuf* b) { + b->cap = 1024; b->len = 0; + b->data = malloc(b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + b->data[0] = '\0'; +} + +static void seed_httpbuf_append(SeedHttpBuf* b, const void* src, size_t n) { + while (b->len + n + 1 > b->cap) b->cap *= 2; + b->data = realloc(b->data, b->cap); + if (!b->data) { fputs("el_seed: out of memory\n", stderr); exit(1); } + memcpy(b->data + b->len, src, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static size_t seed_http_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + size_t n = size * nmemb; + seed_httpbuf_append((SeedHttpBuf*)ud, ptr, n); + return n; +} + +/* Build a curl_slist from a JSON object string of header name:value pairs. */ +static struct curl_slist* seed_headers_from_json(const char* hj) { + struct curl_slist* h = NULL; + if (!hj || !*hj) return NULL; + /* Walk key:value pairs at depth 1. Simple parser — same logic as json_find_key + * in el_runtime.c but adapted for building curl headers. */ + const char* p = hj; + while (*p && *p != '{') p++; + if (*p == '{') p++; + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + if (*p != '"') break; + /* Parse key */ + p++; + const char* ks = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t klen = (size_t)(p - ks); + if (*p == '"') p++; + /* Skip : */ + while (*p == ' ' || *p == ':') p++; + /* Parse value */ + if (*p != '"') break; + p++; + const char* vs = p; + while (*p && *p != '"') { if (*p == '\\') p++; p++; } + size_t vlen = (size_t)(p - vs); + if (*p == '"') p++; + /* Build "Key: Value" header line */ + size_t line_len = klen + 2 + vlen + 1; + char* line = malloc(line_len); + if (line) { + memcpy(line, ks, klen); + memcpy(line + klen, ": ", 2); + memcpy(line + klen + 2, vs, vlen); + line[klen + 2 + vlen] = '\0'; + h = curl_slist_append(h, line); + free(line); + } + } + return h; +} + +static el_val_t seed_http_error_json(const char* msg) { + if (!msg) msg = "unknown error"; + size_t n = strlen(msg) * 6 + 20; + char* buf = seed_strbuf(n); + /* Simple escape: replace " with \" */ + char* d = buf; + *d++ = '{'; *d++ = '"'; *d++ = 'e'; *d++ = 'r'; *d++ = 'r'; + *d++ = 'o'; *d++ = 'r'; *d++ = '"'; *d++ = ':'; *d++ = '"'; + for (const char* s = msg; *s; s++) { + if (*s == '"' || *s == '\\') *d++ = '\\'; + *d++ = *s; + } + *d++ = '"'; *d++ = '}'; *d = '\0'; + return seed_wrap_str(buf); +} + +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + int64_t tms = (int64_t)timeout_ms; + if (tms <= 0) tms = 60000; + + if (!u || !*u) return seed_http_error_json("empty url"); + + CURL* c = curl_easy_init(); + if (!c) return seed_http_error_json("curl_easy_init failed"); + + SeedHttpBuf rb; seed_httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, (long)tms); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } else if (m && strcmp(m, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } else if (m && strcmp(m, "DELETE") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (m && strcmp(m, "PATCH") == 0) { + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (b) { + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(b)); + } + } + /* GET is the default */ + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + if (rc != CURLE_OK) { + free(rb.data); + const char* em = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return seed_http_error_json(em); + } + + seed_arena_track(rb.data); + return seed_wrap_str(rb.data); +} + +static size_t seed_http_file_write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { + return fwrite(ptr, size, nmemb, (FILE*)ud); +} + +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path) { + const char* m = EL_CSTR(method); + const char* u = EL_CSTR(url); + const char* b = EL_CSTR(body); + const char* hj = EL_CSTR(headers_json); + const char* op = EL_CSTR(out_path); + + if (!u || !*u || !op || !*op) return 0; + FILE* f = fopen(op, "wb"); + if (!f) return 0; + + CURL* c = curl_easy_init(); + if (!c) { fclose(f); remove(op); return 0; } + + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + curl_easy_setopt(c, CURLOPT_URL, u); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, seed_http_file_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, f); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 60000L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-seed/1.0"); + + struct curl_slist* hdrs = seed_headers_from_json(hj); + if (hdrs) curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + + if (m && strcmp(m, "POST") == 0) { + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, b ? b : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(b ? strlen(b) : 0)); + } + + CURLcode rc = curl_easy_perform(c); + if (hdrs) curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + + int ok1 = (fflush(f) == 0); + int ok2 = (fclose(f) == 0); + + if (rc != CURLE_OK || !ok1 || !ok2) { remove(op); return 0; } + return 1; +} + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ +/* Delegate to el_runtime.c's http_serve / http_serve_v2 via the existing + * http_set_handler mechanism. */ + +/* Forward declarations for el_runtime.c functions called from el_seed.c. + * A full #include "el_runtime.h" causes redefinition conflicts (el_seed.h + * already defines el_to_float, el_from_float, and several __ wrappers). + * Declare only the symbols actually used here. */ + +/* HTTP server */ +el_val_t http_serve(el_val_t port, el_val_t handler); +el_val_t http_serve_v2(el_val_t port, el_val_t handler); +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* JSON */ +el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_array_len(el_val_t json_str); +el_val_t json_array_get(el_val_t json_str, el_val_t index); +el_val_t json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); + +/* URL / HTML */ +el_val_t url_encode(el_val_t s); +el_val_t url_decode(el_val_t s); +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); + +/* State K/V */ +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); +el_val_t state_get_or(el_val_t key, el_val_t default_val); + +/* Engram graph */ +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json); +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags_json, el_val_t layer_id); +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t engram_remove_layer(el_val_t layer_id); +el_val_t engram_list_layers(void); +el_val_t engram_get_node(el_val_t id); +void engram_strengthen(el_val_t node_id); +void engram_forget(el_val_t node_id); +el_val_t engram_node_count(void); +el_val_t engram_search(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t engram_neighbors(el_val_t node_id); +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_edge_count(void); +el_val_t engram_activate(el_val_t query, el_val_t depth); +el_val_t engram_save(el_val_t path); +el_val_t engram_load(el_val_t path); +el_val_t engram_get_node_json(el_val_t id); +el_val_t engram_search_json(el_val_t query, el_val_t limit); +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t engram_activate_json(el_val_t query, el_val_t depth); +el_val_t engram_stats_json(void); +el_val_t engram_list_layers_json(void); +el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth); + +void __http_serve(el_val_t port, el_val_t handler_name) { + http_serve(port, handler_name); +} + +void __http_serve_v2(el_val_t port, el_val_t handler_name) { + http_serve_v2(port, handler_name); +} + +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) { + return http_response(status, headers_json, body); +} + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + +/* ── Threading ───────────────────────────────────────────────────────────── */ +/* + * Design: + * Static ElThread table (max EL_SEED_MAX_THREADS entries). + * __thread_create: pick a free slot, store fn_name + arg, launch pthread. + * Worker: dlsym(RTLD_DEFAULT, fn_name) → call as el_val_t fn(el_val_t). + * Store result string in slot.result. + * __thread_join: pthread_join, return stored result string. + * + * Thread handle is the slot index (0..EL_SEED_MAX_THREADS-1). + * Returns -1 on failure (no slots, dlsym failure, pthread_create failure). + * + * Each slot is guarded by its own mutex so join/create on different handles + * never contend. + */ + +#define EL_SEED_MAX_THREADS 64 + +typedef el_val_t (*ElFn1)(el_val_t); + +typedef struct { + int in_use; + char* fn_name; + char* arg; + char* result; + pthread_t tid; + int done; /* set to 1 by worker before exit */ +} ElThread; + +static ElThread _el_threads[EL_SEED_MAX_THREADS]; +static pthread_mutex_t _el_thread_mu = PTHREAD_MUTEX_INITIALIZER; + +typedef struct { + int slot; +} ElThreadArg; + +static void* el_thread_worker(void* raw) { + ElThreadArg* ta = (ElThreadArg*)raw; + int slot = ta->slot; + free(ta); + + ElThread* t = &_el_threads[slot]; + + /* Resolve the El function symbol in the running binary. */ + void* sym = dlsym(RTLD_DEFAULT, t->fn_name); + if (!sym) { + pthread_mutex_lock(&_el_thread_mu); + t->result = seed_strdup_persist(""); + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + return NULL; + } + + ElFn1 fn = (ElFn1)sym; + + /* Call the El function with the string argument. */ + el_val_t arg_val = EL_STR(t->arg); + el_val_t ret = fn(arg_val); + + /* Persist the result string. */ + const char* rs = EL_CSTR(ret); + char* stored = seed_strdup_persist(rs ? rs : ""); + + pthread_mutex_lock(&_el_thread_mu); + t->result = stored; + t->done = 1; + pthread_mutex_unlock(&_el_thread_mu); + + return NULL; +} + +el_val_t __thread_create(el_val_t fn_name, el_val_t arg) { + const char* fname = EL_CSTR(fn_name); + const char* astr = EL_CSTR(arg); + if (!fname || !*fname) return (el_val_t)(int64_t)-1; + if (!astr) astr = ""; + + pthread_mutex_lock(&_el_thread_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_THREADS; i++) { + if (!_el_threads[i].in_use) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + ElThread* t = &_el_threads[slot]; + t->in_use = 1; + t->done = 0; + t->fn_name = seed_strdup_persist(fname); + t->arg = seed_strdup_persist(astr); + t->result = NULL; + pthread_mutex_unlock(&_el_thread_mu); + + ElThreadArg* ta = malloc(sizeof(ElThreadArg)); + if (!ta) { + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + ta->slot = slot; + + if (pthread_create(&t->tid, NULL, el_thread_worker, ta) != 0) { + free(ta); + pthread_mutex_lock(&_el_thread_mu); + free(t->fn_name); free(t->arg); + t->in_use = 0; + pthread_mutex_unlock(&_el_thread_mu); + return (el_val_t)(int64_t)-1; + } + + return (el_val_t)(int64_t)slot; +} + +el_val_t __thread_join(el_val_t tid) { + int64_t slot = (int64_t)tid; + if (slot < 0 || slot >= EL_SEED_MAX_THREADS) return seed_wrap_str(seed_strdup("")); + + ElThread* t = &_el_threads[slot]; + + pthread_mutex_lock(&_el_thread_mu); + if (!t->in_use) { + pthread_mutex_unlock(&_el_thread_mu); + return seed_wrap_str(seed_strdup("")); + } + pthread_mutex_unlock(&_el_thread_mu); + + /* Wait for thread to finish. */ + pthread_join(t->tid, NULL); + + pthread_mutex_lock(&_el_thread_mu); + char* res = t->result ? t->result : ""; + char* copy = seed_strdup(res); /* arena-tracked for caller lifetime */ + free(t->fn_name); + free(t->arg); + if (t->result) { free(t->result); t->result = NULL; } + t->fn_name = NULL; t->arg = NULL; + t->in_use = 0; + t->done = 0; + pthread_mutex_unlock(&_el_thread_mu); + + return seed_wrap_str(copy); +} + +/* ── Mutex pool ──────────────────────────────────────────────────────────── */ + +#define EL_SEED_MAX_MUTEXES 256 + +static pthread_mutex_t _el_mutexes[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_init[EL_SEED_MAX_MUTEXES]; +static int _el_mutex_used[EL_SEED_MAX_MUTEXES]; +static pthread_mutex_t _el_mutex_pool_mu = PTHREAD_MUTEX_INITIALIZER; + +el_val_t __mutex_new(void) { + pthread_mutex_lock(&_el_mutex_pool_mu); + int slot = -1; + for (int i = 0; i < EL_SEED_MAX_MUTEXES; i++) { + if (!_el_mutex_used[i]) { slot = i; break; } + } + if (slot < 0) { + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)-1; + } + _el_mutex_used[slot] = 1; + if (!_el_mutex_init[slot]) { + pthread_mutex_init(&_el_mutexes[slot], NULL); + _el_mutex_init[slot] = 1; + } + pthread_mutex_unlock(&_el_mutex_pool_mu); + return (el_val_t)(int64_t)slot; +} + +void __mutex_lock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_lock(&_el_mutexes[slot]); +} + +void __mutex_unlock(el_val_t m) { + int64_t slot = (int64_t)m; + if (slot < 0 || slot >= EL_SEED_MAX_MUTEXES) return; + if (!_el_mutex_init[slot]) return; + pthread_mutex_unlock(&_el_mutexes[slot]); +} + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return seed_wrap_str(seed_strdup("")); + FILE* f = popen(c, "r"); + if (!f) return seed_wrap_str(seed_strdup("")); + size_t cap = 4096, len = 0; + char* buf = malloc(cap); + if (!buf) { pclose(f); return seed_wrap_str(seed_strdup("")); } + char tmp[4096]; + while (fgets(tmp, sizeof(tmp), f)) { + size_t n = strlen(tmp); + while (len + n + 1 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); pclose(f); return seed_wrap_str(seed_strdup("")); } + buf = g; + } + memcpy(buf + len, tmp, n); + len += n; + buf[len] = '\0'; + } + pclose(f); + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +void __exec_bg(el_val_t cmd) { + const char* c = EL_CSTR(cmd); + if (!c) return; + /* Fork-free: run in background via system() with & appended. */ + size_t n = strlen(c); + char* s = malloc(n + 4); + if (!s) return; + memcpy(s, c, n); + memcpy(s + n, " &", 3); + system(s); + free(s); +} + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return seed_wrap_str(seed_strdup("")); + const char* v = getenv(k); + return seed_wrap_str(seed_strdup(v ? v : "")); +} + +void __exit_program(el_val_t code) { + exit((int)(int64_t)code); +} + +/* ── args_json ────────────────────────────────────────────────────────────── */ + +static int _seed_argc = 0; +static char** _seed_argv = NULL; + +void el_seed_init_args(int argc, char** argv) { + _seed_argc = argc; + _seed_argv = argv; +} + +el_val_t __args_json(void) { + /* Return ["arg1","arg2",...] as a JSON string. Skip argv[0] (program name). */ + size_t cap = 256, len = 0; + char* buf = malloc(cap); + if (!buf) return seed_wrap_str(seed_strdup("[]")); + buf[len++] = '['; + int first = 1; + for (int i = 1; i < _seed_argc; i++) { + const char* a = _seed_argv[i]; + /* Estimate: each arg needs at most strlen*6+4 bytes (worst case escape) */ + size_t need = strlen(a) * 6 + 8; + while (len + need + 2 > cap) { + cap *= 2; + char* g = realloc(buf, cap); + if (!g) { free(buf); return seed_wrap_str(seed_strdup("[]")); } + buf = g; + } + if (!first) buf[len++] = ','; + first = 0; + buf[len++] = '"'; + for (const char* p = a; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"') { buf[len++] = '\\'; buf[len++] = '"'; } + else if (c == '\\') { buf[len++] = '\\'; buf[len++] = '\\'; } + else if (c == '\n') { buf[len++] = '\\'; buf[len++] = 'n'; } + else if (c == '\r') { buf[len++] = '\\'; buf[len++] = 'r'; } + else if (c == '\t') { buf[len++] = '\\'; buf[len++] = 't'; } + else buf[len++] = (char)c; + } + buf[len++] = '"'; + } + buf[len++] = ']'; + buf[len] = '\0'; + seed_arena_track(buf); + return seed_wrap_str(buf); +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + int64_t ns = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + return (el_val_t)ns; + } + struct timeval tv; + gettimeofday(&tv, NULL); + return (el_val_t)((int64_t)tv.tv_sec * 1000000000LL + (int64_t)tv.tv_usec * 1000LL); +} + +void __sleep_ms(el_val_t ms) { + int64_t m = (int64_t)ms; + if (m < 0) m = 0; + struct timespec ts; + ts.tv_sec = (time_t)(m / 1000LL); + ts.tv_nsec = (long)((m % 1000LL) * 1000000LL); + nanosleep(&ts, NULL); +} + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +static int _seed_uuid_seeded = 0; + +el_val_t __uuid_v4(void) { + if (!_seed_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_seed_uuid_seeded); + _seed_uuid_seeded = 1; + } + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + b[6] = (b[6] & 0x0f) | 0x40; /* version 4 */ + b[8] = (b[8] & 0x3f) | 0x80; /* RFC 4122 variant */ + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], + b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); + return seed_wrap_str(seed_strdup(buf)); +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t __log_f(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t __ln_f(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t __sin_f(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t __cos_f(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t __pi_f(void) { return el_from_float(3.14159265358979323846); } + +/* ── JSON — thin wrappers around el_runtime.c implementations ────────────── */ + +el_val_t __json_get(el_val_t json, el_val_t key) { return json_get(json, key); } +el_val_t __json_get_raw(el_val_t json_str, el_val_t key) { return json_get_raw(json_str, key); } +el_val_t __json_parse(el_val_t s) { return json_parse(s); } +el_val_t __json_stringify(el_val_t v) { return json_stringify(v); } +el_val_t __json_parse_map(el_val_t json_str) { return json_parse(json_str); } +el_val_t __json_stringify_val(el_val_t val) { return json_stringify(val); } +el_val_t __json_array_len(el_val_t json_str) { return json_array_len(json_str); } +el_val_t __json_array_get(el_val_t json_str, el_val_t index) { return json_array_get(json_str, index); } +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index) { return json_array_get_string(json_str, index); } +el_val_t __json_get_string(el_val_t json_str, el_val_t key) { return json_get_string(json_str, key); } +el_val_t __json_get_int(el_val_t json_str, el_val_t key) { return json_get_int(json_str, key); } +el_val_t __json_get_float(el_val_t json_str, el_val_t key) { return json_get_float(json_str, key); } +el_val_t __json_get_bool(el_val_t json_str, el_val_t key) { return json_get_bool(json_str, key); } +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value) { return json_set(json_str, key, value); } + +/* ── State K/V — thin wrappers ───────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value) { return state_set(key, value); } +el_val_t __state_get(el_val_t key) { return state_get(key); } +el_val_t __state_del(el_val_t key) { return state_del(key); } +el_val_t __state_keys(void) { return state_keys(); } + +/* ── HTML/URL — thin wrappers ────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json) { + return el_html_sanitize(input_html, allowlist_json); +} + +el_val_t __url_encode(el_val_t s) { return url_encode(s); } +el_val_t __url_decode(el_val_t s) { return url_decode(s); } + +/* ── Engram — thin wrappers ──────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + return engram_node(content, node_type, salience); +} + +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + return engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags); +} + +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id) { + return engram_node_layered(content, node_type, label, salience, certainty, confidence, + status, tags, layer_id); +} + +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + return engram_add_layer(name, priority, suppressible, transparent, injectable); +} + +el_val_t __engram_remove_layer(el_val_t layer_id) { return engram_remove_layer(layer_id); } +el_val_t __engram_list_layers(void) { return engram_list_layers(); } +el_val_t __engram_get_node(el_val_t id) { return engram_get_node(id); } +void __engram_strengthen(el_val_t node_id) { engram_strengthen(node_id); } +void __engram_forget(el_val_t node_id) { engram_forget(node_id); } +el_val_t __engram_node_count(void) { return engram_node_count(); } + +el_val_t __engram_search(el_val_t query, el_val_t limit) { return engram_search(query, limit); } +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset) { return engram_scan_nodes(limit, offset); } + +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + engram_connect(from_id, to_id, weight, relation); +} + +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id) { + return engram_edge_between(from_id, to_id); +} + +el_val_t __engram_neighbors(el_val_t node_id) { return engram_neighbors(node_id); } + +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_filtered(node_id, max_depth, direction); +} + +el_val_t __engram_edge_count(void) { return engram_edge_count(); } + +el_val_t __engram_activate(el_val_t query, el_val_t depth) { return engram_activate(query, depth); } +el_val_t __engram_save(el_val_t path) { return engram_save(path); } +el_val_t __engram_load(el_val_t path) { return engram_load(path); } + +el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); } + +el_val_t __engram_search_json(el_val_t query, el_val_t limit) { + return engram_search_json(query, limit); +} + +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + return engram_scan_nodes_json(limit, offset); +} + +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + return engram_scan_nodes_by_type_json(node_type, limit, offset); +} + +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { + return engram_neighbors_json(node_id, max_depth, direction); +} + +el_val_t __engram_activate_json(el_val_t query, el_val_t depth) { + return engram_activate_json(query, depth); +} + +el_val_t __engram_stats_json(void) { return engram_stats_json(); } +el_val_t __engram_list_layers_json(void) { return engram_list_layers_json(); } + +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { + return engram_compile_layered_json(intent, depth); +} + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +/* ── Manifest reader (shared: macOS + Linux) ─────────────────────────────── */ +/* + * Parse the app{} block from a manifest.el file. + * Expected format (subset — only app{} block is parsed): + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on parse failure. + * + * Compiled unconditionally so both EL_TARGET_MACOS and EL_TARGET_LINUX can use + * it — the __manifest_read builtin is identical on all native targets. + */ + +static void seed_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +/* Read a quoted string literal. Caller must free() the result. */ +static char* seed_manifest_read_string(const char** p) { + seed_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; /* skip escaped char */ + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +/* Read an unquoted integer literal. */ +static int64_t seed_manifest_read_int(const char** p) { + seed_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +/* Find "app {" block start. Returns pointer past the '{'. */ +static const char* seed_manifest_find_app_block(const char* src) { + while (*src) { + /* Skip line comments (// ...) */ + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +el_val_t __manifest_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p || !*p) return seed_wrap_str(seed_strdup("{}")); + + /* Read the file. */ + FILE* f = fopen(p, "r"); + if (!f) return seed_wrap_str(seed_strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + char* src = malloc((size_t)sz + 1); + if (!src) { fclose(f); return seed_wrap_str(seed_strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = seed_manifest_find_app_block(src); + if (!app_start) { free(src); return seed_wrap_str(seed_strdup("{}")); } + + /* Parse key-value pairs inside the app { } block. */ + char* title = NULL; + int64_t width = 1200; + int64_t height = 800; + int64_t min_w = 600; + int64_t min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + /* Skip whitespace and comments. */ + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + + /* Read key token. */ + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + + /* Skip whitespace between key and value. */ + while (*cur == ' ' || *cur == '\t') cur++; + + if (strncmp(key_start, "window_title", key_len) == 0 && key_len == 12) { + free(title); + title = seed_manifest_read_string(&cur); + } else if (strncmp(key_start, "window_width", key_len) == 0 && key_len == 12) { + width = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "window_height", key_len) == 0 && key_len == 13) { + height = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_width", key_len) == 0 && key_len == 9) { + min_w = seed_manifest_read_int(&cur); + } else if (strncmp(key_start, "min_height", key_len) == 0 && key_len == 10) { + min_h = seed_manifest_read_int(&cur); + } else { + /* Unknown key — skip to end of line. */ + while (*cur && *cur != '\n' && *cur != '}') cur++; + } + } + + free(src); + + /* Build JSON result. */ + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = seed_strbuf(buf_sz); + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return seed_wrap_str(buf); +} + +/* ── Native widget system — macOS AppKit ─────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_MACOS is defined. + * They call through to el_appkit.m (Objective-C) which implements the actual + * AppKit widget creation and management. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_appkit_* function, return the result. + * + * Compile requirements: + * - Compile el_appkit.m alongside this file (clang -ObjC -framework Cocoa) + * - Pass -DEL_TARGET_MACOS to enable this section + */ + +#ifdef EL_TARGET_MACOS + +/* Forward declarations — implemented in el_appkit.m */ +extern void el_appkit_init(void); +extern void el_appkit_run_loop(void); +extern int64_t el_appkit_window_create(const char* title, int w, int h, int mw, int mh); +extern void el_appkit_window_show(int64_t handle); +extern void el_appkit_window_set_title(int64_t handle, const char* title); +extern int64_t el_appkit_vstack_create(int spacing); +extern int64_t el_appkit_hstack_create(int spacing); +extern int64_t el_appkit_zstack_create(void); +extern int64_t el_appkit_scroll_create(void); +extern int64_t el_appkit_label_create(const char* text); +extern int64_t el_appkit_button_create(const char* label); +extern int64_t el_appkit_text_field_create(const char* placeholder); +extern int64_t el_appkit_text_area_create(const char* placeholder); +extern int64_t el_appkit_image_create(const char* path_or_name); +extern void el_appkit_widget_set_text(int64_t handle, const char* text); +extern const char* el_appkit_widget_get_text(int64_t handle); +extern void el_appkit_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_appkit_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_appkit_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_appkit_widget_set_width(int64_t h, int width); +extern void el_appkit_widget_set_height(int64_t h, int height); +extern void el_appkit_widget_set_flex(int64_t h, int flex); +extern void el_appkit_widget_set_corner_radius(int64_t h, int radius); +extern void el_appkit_widget_set_disabled(int64_t h, int disabled); +extern void el_appkit_widget_set_hidden(int64_t h, int hidden); +extern void el_appkit_widget_add_child(int64_t parent, int64_t child); +extern void el_appkit_widget_remove_child(int64_t parent, int64_t child); +extern void el_appkit_widget_destroy(int64_t handle); +extern void el_appkit_widget_on_click(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_change(int64_t h, const char* fn_name); +extern void el_appkit_widget_on_submit(int64_t h, const char* fn_name); + +/* ── Native widget builtins ──────────────────────────────────────────────── */ + +void __native_init(void) { el_appkit_init(); } +void __native_run_loop(void) { el_appkit_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_appkit_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_appkit_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_appkit_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_appkit_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_appkit_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_appkit_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_appkit_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_appkit_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_appkit_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_appkit_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_appkit_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_appkit_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_appkit_widget_get_text((int64_t)h); + if (!s) return seed_wrap_str(seed_strdup("")); + /* el_appkit returns strdup'd string — wrap it (arena won't track it, but + * caller must treat return as short-lived or copy it). */ + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_appkit_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_appkit_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_appkit_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_appkit_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_appkit_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_appkit_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_appkit_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_appkit_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_appkit_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_appkit_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_appkit_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_appkit_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_appkit_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system — Linux GTK4 ──────────────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_LINUX is defined. + * They call through to el_gtk4.c which implements the GTK4 widget layer. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_gtk4_* function, return the result. + * + * Compile requirements: + * - Compile el_gtk4.c alongside this file + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) el_gtk4.c -c ... + * - Pass -DEL_TARGET_LINUX to enable this section + * - Link with $(pkg-config --libs gtk4) -ldl + * + * __manifest_read is shared with the macOS path (implemented above in the + * seed_manifest_* helpers) and compiled for both targets unconditionally. + */ + +#ifdef EL_TARGET_LINUX + +/* Forward declarations — implemented in el_gtk4.c */ +extern void el_gtk4_init(void); +extern void el_gtk4_run_loop(void); +extern int64_t el_gtk4_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_gtk4_window_show(int64_t handle); +extern void el_gtk4_window_set_title(int64_t handle, const char* title); +extern int64_t el_gtk4_vstack_create(int spacing); +extern int64_t el_gtk4_hstack_create(int spacing); +extern int64_t el_gtk4_zstack_create(void); +extern int64_t el_gtk4_scroll_create(void); +extern int64_t el_gtk4_label_create(const char* text); +extern int64_t el_gtk4_button_create(const char* label); +extern int64_t el_gtk4_text_field_create(const char* placeholder); +extern int64_t el_gtk4_text_area_create(const char* placeholder); +extern int64_t el_gtk4_image_create(const char* path_or_name); +extern void el_gtk4_widget_set_text(int64_t handle, const char* text); +extern const char* el_gtk4_widget_get_text(int64_t handle); +extern void el_gtk4_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_gtk4_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_gtk4_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_gtk4_widget_set_width(int64_t h, int width); +extern void el_gtk4_widget_set_height(int64_t h, int height); +extern void el_gtk4_widget_set_flex(int64_t h, int flex); +extern void el_gtk4_widget_set_corner_radius(int64_t h, int radius); +extern void el_gtk4_widget_set_disabled(int64_t h, int disabled); +extern void el_gtk4_widget_set_hidden(int64_t h, int hidden); +extern void el_gtk4_widget_add_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_remove_child(int64_t parent, int64_t child); +extern void el_gtk4_widget_destroy(int64_t handle); +extern void el_gtk4_widget_on_click(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_change(int64_t h, const char* fn_name); +extern void el_gtk4_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_gtk4_init(); } +void __native_run_loop(void) { el_gtk4_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_gtk4_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_gtk4_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_gtk4_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_gtk4_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_gtk4_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_gtk4_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_gtk4_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_gtk4_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_gtk4_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_gtk4_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_gtk4_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_gtk4_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* el_gtk4 returns strdup'd string */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_gtk4_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_gtk4_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_gtk4_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_gtk4_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_gtk4_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_gtk4_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_gtk4_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_gtk4_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_gtk4_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_gtk4_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_gtk4_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_gtk4_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_gtk4_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system — SDL2 (embedded / Pi) ─────────────────────────── */ +/* + * These builtins are compiled in only when EL_TARGET_SDL2 is defined. + * They call through to el_sdl2.c which implements a retained-mode pixel + * widget layer on top of SDL2 + SDL_ttf + SDL_image. + * + * Each builtin is a thin wrapper: marshal el_val_t ↔ C types, call the + * el_sdl2_* function, return the result. + * + * Compile requirements: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) el_sdl2.c -c ... + * Pass -DEL_TARGET_SDL2 to enable this section. + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + * + * __manifest_read is shared with the macOS/Linux paths (implemented above). + */ + +#ifdef EL_TARGET_SDL2 + +/* Forward declarations — implemented in el_sdl2.c */ +extern void el_sdl2_init(void); +extern void el_sdl2_run_loop(void); +extern int64_t el_sdl2_window_create(const char* title, int w, int h, + int mw, int mh); +extern void el_sdl2_window_show(int64_t handle); +extern void el_sdl2_window_set_title(int64_t handle, const char* title); +extern int64_t el_sdl2_vstack_create(int spacing); +extern int64_t el_sdl2_hstack_create(int spacing); +extern int64_t el_sdl2_zstack_create(void); +extern int64_t el_sdl2_scroll_create(void); +extern int64_t el_sdl2_label_create(const char* text); +extern int64_t el_sdl2_button_create(const char* label); +extern int64_t el_sdl2_text_field_create(const char* placeholder); +extern int64_t el_sdl2_text_area_create(const char* placeholder); +extern int64_t el_sdl2_image_create(const char* path_or_name); +extern void el_sdl2_widget_set_text(int64_t handle, const char* text); +extern const char* el_sdl2_widget_get_text(int64_t handle); +extern void el_sdl2_widget_set_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_bg_color(int64_t h, float r, float g, float b, float a); +extern void el_sdl2_widget_set_font(int64_t h, const char* family, int size, int bold); +extern void el_sdl2_widget_set_padding(int64_t h, int top, int right, int bottom, int left); +extern void el_sdl2_widget_set_width(int64_t h, int width); +extern void el_sdl2_widget_set_height(int64_t h, int height); +extern void el_sdl2_widget_set_flex(int64_t h, int flex); +extern void el_sdl2_widget_set_corner_radius(int64_t h, int radius); +extern void el_sdl2_widget_set_disabled(int64_t h, int disabled); +extern void el_sdl2_widget_set_hidden(int64_t h, int hidden); +extern void el_sdl2_widget_add_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_remove_child(int64_t parent, int64_t child); +extern void el_sdl2_widget_destroy(int64_t handle); +extern void el_sdl2_widget_on_click(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_change(int64_t h, const char* fn_name); +extern void el_sdl2_widget_on_submit(int64_t h, const char* fn_name); + +void __native_init(void) { el_sdl2_init(); } +void __native_run_loop(void) { el_sdl2_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_sdl2_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_sdl2_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_sdl2_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_sdl2_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_sdl2_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_sdl2_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_sdl2_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_sdl2_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_sdl2_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_sdl2_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_sdl2_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_sdl2_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_sdl2_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_sdl2_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_sdl2_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_sdl2_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_sdl2_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_sdl2_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_sdl2_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_sdl2_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_sdl2_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_sdl2_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_sdl2_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_sdl2_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_sdl2_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_SDL2 */ + +/* ── EL_TARGET_LVGL — LVGL v9 embedded backend ─────────────────────────────── + * + * These builtins are compiled in only when EL_TARGET_LVGL is defined. + * Each wrapper translates el_val_t arguments (opaque int64_t) into the typed + * parameters that el_lvgl_* expects, then calls through. + * + * Build: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c el_seed.c -c ... + * # Link with lvgl.a. For bare-metal add -DEL_LVGL_NO_DLSYM. + * + * __manifest_read is shared — defined unconditionally above. + */ + +#ifdef EL_TARGET_LVGL + +extern void el_lvgl_init(void); +extern void el_lvgl_run_loop(void); +extern int64_t el_lvgl_window_create(const char *title, int w, int h, + int mw, int mh); +extern void el_lvgl_window_show(int64_t handle); +extern void el_lvgl_window_set_title(int64_t handle, const char *title); +extern int64_t el_lvgl_vstack_create(int spacing); +extern int64_t el_lvgl_hstack_create(int spacing); +extern int64_t el_lvgl_zstack_create(void); +extern int64_t el_lvgl_scroll_create(void); +extern int64_t el_lvgl_label_create(const char *text); +extern int64_t el_lvgl_button_create(const char *label); +extern int64_t el_lvgl_text_field_create(const char *placeholder); +extern int64_t el_lvgl_text_area_create(const char *placeholder); +extern int64_t el_lvgl_image_create(const char *path_or_name); +extern void el_lvgl_widget_set_text(int64_t handle, const char *text); +extern const char *el_lvgl_widget_get_text(int64_t handle); +extern void el_lvgl_widget_set_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_bg_color(int64_t h, float r, float g, + float b, float a); +extern void el_lvgl_widget_set_font(int64_t h, const char *family, + int size, int bold); +extern void el_lvgl_widget_set_padding(int64_t h, int top, int right, + int bottom, int left); +extern void el_lvgl_widget_set_width(int64_t h, int width); +extern void el_lvgl_widget_set_height(int64_t h, int height); +extern void el_lvgl_widget_set_flex(int64_t h, int flex); +extern void el_lvgl_widget_set_corner_radius(int64_t h, int radius); +extern void el_lvgl_widget_set_disabled(int64_t h, int disabled); +extern void el_lvgl_widget_set_hidden(int64_t h, int hidden); +extern void el_lvgl_widget_add_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_remove_child(int64_t parent, int64_t child); +extern void el_lvgl_widget_destroy(int64_t handle); +extern void el_lvgl_widget_on_click(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_change(int64_t h, const char *fn_name); +extern void el_lvgl_widget_on_submit(int64_t h, const char *fn_name); + +void __native_init(void) { el_lvgl_init(); } +void __native_run_loop(void) { el_lvgl_run_loop(); } + +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height) { + return (el_val_t)el_lvgl_window_create( + EL_CSTR(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_lvgl_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_lvgl_window_set_title((int64_t)h, EL_CSTR(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_lvgl_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_lvgl_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_lvgl_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_lvgl_label_create(EL_CSTR(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_lvgl_button_create(EL_CSTR(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_field_create(EL_CSTR(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_lvgl_text_area_create(EL_CSTR(placeholder)); } +el_val_t __image_create(el_val_t path_or_name) { return (el_val_t)el_lvgl_image_create(EL_CSTR(path_or_name)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_lvgl_widget_set_text((int64_t)h, EL_CSTR(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char *s = el_lvgl_widget_get_text((int64_t)h); + if (!s) return EL_STR(""); + return EL_STR(s); /* pointer into LVGL internal storage — valid until next LVGL op */ +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_lvgl_widget_set_bg_color((int64_t)h, + (float)el_to_float(r), (float)el_to_float(g), + (float)el_to_float(b), (float)el_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_lvgl_widget_set_font((int64_t)h, EL_CSTR(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_lvgl_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_lvgl_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_lvgl_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_lvgl_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_lvgl_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_lvgl_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_lvgl_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } +void __widget_add_child(el_val_t p, el_val_t c) { el_lvgl_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_lvgl_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_lvgl_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_click((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_change((int64_t)h, EL_CSTR(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_lvgl_widget_on_submit((int64_t)h, EL_CSTR(fn_name)); } + +#endif /* EL_TARGET_LVGL */ + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/ui/examples/native-hello/build-docker/runtime/el_seed.h b/ui/examples/native-hello/build-docker/runtime/el_seed.h new file mode 100644 index 0000000..08767df --- /dev/null +++ b/ui/examples/native-hello/build-docker/runtime/el_seed.h @@ -0,0 +1,324 @@ +/* + * el_seed.h — El language seed runtime header + * + * Declares all OS-boundary primitives available to compiled El programs. + * All functions use the __ prefix convention. Signatures use el_val_t (= int64_t) + * as the universal value type. + * + * el_seed.c is the complete C boundary for the El runtime. The heavy runtime + * (el_runtime.c) has been retired — everything lives in el_seed.c plus the + * native El runtime (runtime/ *.el files). + * + * Link requirements: + * -lcurl — HTTP client (__http_do, __http_do_to_file) + * -lpthread — threading (__thread_create, __thread_join, __mutex_new, ...) + * + * Canonical compile (via elb): + * elb builds and links el_seed.c automatically. + */ + +#pragma once + +#include +#include + +/* ── Value model ───────────────────────────────────────────────────────────── + * All El values are el_val_t (int64_t). On 64-bit systems a pointer fits. + * String -> el_val_t (holds const char* via uintptr_t cast) + * Int -> el_val_t (stored directly) + * Bool -> el_val_t (0 = false, nonzero = true) + * Void -> void + */ +typedef int64_t el_val_t; + +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#define EL_INT(v) (v) +#define EL_NULL ((el_val_t)0) + +/* Float values share the el_val_t slot via bit-cast. */ +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; u.i = (int64_t)v; return u.f; +} +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; u.f = f; return (el_val_t)u.i; +} + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── String primitives ───────────────────────────────────────────────────── */ + +el_val_t __str_len(el_val_t s); +el_val_t __str_char_at(el_val_t s, el_val_t i); /* returns Int (byte value) */ +el_val_t __str_alloc(el_val_t n); /* malloc(n+1), zero-init, return String */ +el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); /* s[i]=c, return s */ +el_val_t __str_cmp(el_val_t a, el_val_t b); /* strcmp result as Int */ +el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); /* strncmp */ +el_val_t __str_concat_raw(el_val_t a, el_val_t b); /* malloc+strcpy concat */ +el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); /* substring copy */ +el_val_t __int_to_str(el_val_t n); +el_val_t __str_to_int(el_val_t s); +el_val_t __float_to_str(el_val_t f); /* f is bit-cast double */ +el_val_t __str_to_float(el_val_t s); /* strtod, bit-cast result */ + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +void __println(el_val_t s); +void __print(el_val_t s); +el_val_t __readline(void); + +/* ── Filesystem ──────────────────────────────────────────────────────────── */ + +el_val_t __fs_read(el_val_t path); +el_val_t __fs_write(el_val_t path, el_val_t content); +el_val_t __fs_exists(el_val_t path); +el_val_t __fs_list_raw(el_val_t path); /* newline-separated filenames */ +el_val_t __fs_mkdir(el_val_t path); +el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n); + +/* ── HTTP client ─────────────────────────────────────────────────────────── */ + +/* Unified HTTP call. headers_json is a JSON object of header name->value pairs + * (e.g. {"Authorization":"Bearer ...","Content-Type":"application/json"}). + * Use "" or "{}" for no extra headers. timeout_ms <= 0 uses the default. */ +el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t timeout_ms); + +/* Stream response body directly to a file. Returns 1 on success, 0 on failure. */ +el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body, + el_val_t headers_json, el_val_t out_path); + +/* ── HTTP server ─────────────────────────────────────────────────────────── */ + +/* Blocking HTTP server. handler_name is the El function name to dispatch to. + * v1 handler: (method, path, body) -> String + * v2 handler: (method, path, headers_map, body) -> String or envelope */ +void __http_serve(el_val_t port, el_val_t handler_name); +void __http_serve_v2(el_val_t port, el_val_t handler_name); + +/* Build a structured HTTP response envelope. + * headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */ +el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); + +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ + +/* Returns the raw file descriptor for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. + * Returns -1 if called outside a handler context. */ +el_val_t __http_conn_fd(void); + +/* Sends SSE response headers on conn_id (the fd from __http_conn_fd), + * keeping the connection open for streaming. Returns 1 on success, 0 on + * write failure. Call once at the start of an SSE handler. */ +el_val_t __http_sse_open(el_val_t conn_id); + +/* Writes one SSE event frame: "data: \n\n". data must not contain + * newlines. Returns 1 on success, 0 if the client disconnected. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); + +/* Closes the SSE connection. The handler must return http_sse_sentinel() + * so the HTTP worker does not double-close the fd. */ +el_val_t __http_sse_close(el_val_t conn_id); + +/* ── Threading ───────────────────────────────────────────────────────────── */ + +/* Create a thread that calls the named El function with a String argument. + * fn_name is resolved via dlsym(RTLD_DEFAULT, fn_name). Returns a thread + * handle Int that can be passed to __thread_join. Returns -1 on failure. */ +el_val_t __thread_create(el_val_t fn_name, el_val_t arg); + +/* Wait for thread tid (returned by __thread_create) to finish. + * Returns the thread's return value as a String. */ +el_val_t __thread_join(el_val_t tid); + +/* Allocate a new mutex. Returns a handle Int (index into internal table). */ +el_val_t __mutex_new(void); + +void __mutex_lock(el_val_t m); +void __mutex_unlock(el_val_t m); + +/* ── Subprocess ──────────────────────────────────────────────────────────── */ + +el_val_t __exec(el_val_t cmd); /* popen, capture all stdout, return String */ +void __exec_bg(el_val_t cmd); /* fire and forget */ + +/* ── Environment and process ─────────────────────────────────────────────── */ + +el_val_t __env_get(el_val_t key); /* getenv, return "" if not set */ +void __exit_program(el_val_t code); +el_val_t __args_json(void); /* CLI args as JSON array string */ + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t __time_now_ns(void); /* clock_gettime REALTIME, nanoseconds */ +void __sleep_ms(el_val_t ms); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t __uuid_v4(void); + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t __sqrt_f(el_val_t f); +el_val_t __log_f(el_val_t f); +el_val_t __ln_f(el_val_t f); +el_val_t __sin_f(el_val_t f); +el_val_t __cos_f(el_val_t f); +el_val_t __pi_f(void); + +/* ── JSON ────────────────────────────────────────────────────────────────── */ + +el_val_t __json_get(el_val_t json, el_val_t key); +el_val_t __json_get_raw(el_val_t json_str, el_val_t key); +el_val_t __json_parse(el_val_t s); +el_val_t __json_stringify(el_val_t v); +el_val_t __json_parse_map(el_val_t json_str); /* alias for __json_parse */ +el_val_t __json_stringify_val(el_val_t val); /* alias for __json_stringify */ +el_val_t __json_array_len(el_val_t json_str); +el_val_t __json_array_get(el_val_t json_str, el_val_t index); +el_val_t __json_array_get_string(el_val_t json_str, el_val_t index); +el_val_t __json_get_string(el_val_t json_str, el_val_t key); +el_val_t __json_get_int(el_val_t json_str, el_val_t key); +el_val_t __json_get_float(el_val_t json_str, el_val_t key); +el_val_t __json_get_bool(el_val_t json_str, el_val_t key); +el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value); + +/* ── State K/V ───────────────────────────────────────────────────────────── */ + +el_val_t __state_set(el_val_t key, el_val_t value); +el_val_t __state_get(el_val_t key); +el_val_t __state_del(el_val_t key); +el_val_t __state_keys(void); + +/* ── HTML/URL ────────────────────────────────────────────────────────────── */ + +el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t __url_encode(el_val_t s); +el_val_t __url_decode(el_val_t s); + +/* ── Engram ──────────────────────────────────────────────────────────────── */ + +el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags); +el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t status, el_val_t tags, el_val_t layer_id); +el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable); +el_val_t __engram_remove_layer(el_val_t layer_id); +el_val_t __engram_list_layers(void); +el_val_t __engram_get_node(el_val_t id); +void __engram_strengthen(el_val_t node_id); +void __engram_forget(el_val_t node_id); +el_val_t __engram_node_count(void); +el_val_t __engram_search(el_val_t query, el_val_t limit); +el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset); +void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id); +el_val_t __engram_neighbors(el_val_t node_id); +el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t __engram_edge_count(void); +el_val_t __engram_activate(el_val_t query, el_val_t depth); +el_val_t __engram_save(el_val_t path); +el_val_t __engram_load(el_val_t path); +el_val_t __engram_get_node_json(el_val_t id); +el_val_t __engram_search_json(el_val_t query, el_val_t limit); +el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset); +el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); +el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); +el_val_t __engram_activate_json(el_val_t query, el_val_t depth); +el_val_t __engram_stats_json(void); +el_val_t __engram_list_layers_json(void); +el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth); + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +/* __sha256_hex — return the SHA-256 hex digest of a string. + * The returned string is 64 hex characters (lowercase). */ +el_val_t __sha256_hex(el_val_t s); + +/* ── args init (called from main) ────────────────────────────────────────── */ +/* Store argc/argv for __args_json. Call once at the start of main(). */ +void el_seed_init_args(int argc, char** argv); + +/* ── Native widget system (macOS AppKit) ─────────────────────────────────── */ +/* + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m + * (-framework Cocoa). Widget handles are opaque Int values (int64_t slot + * indices). The null/invalid handle value is -1. + * + * Lifecycle: + * 1. Call __native_init() once at program start. + * 2. Build widget tree with __window_create / __vstack_create / etc. + * 3. Attach children with __widget_add_child(parent, child). + * 4. Register callbacks with __widget_on_click / __widget_on_change. + * 5. Call __window_show(win) to make the window visible. + * 6. Call __native_run_loop() — never returns. AppKit owns the thread. + * + * Callback El function signature: + * fn my_handler(widget: Int, data: String) -> Void + */ +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height, + el_val_t min_width, el_val_t min_height); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_t handle); +void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a); +void __widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold); +void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +/* Parse the app{} block from manifest.el at path. Returns a JSON string: + * {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +#ifdef __cplusplus +} +#endif diff --git a/ui/examples/native-hello/build-gtk4.sh b/ui/examples/native-hello/build-gtk4.sh new file mode 100755 index 0000000..0cb2ad9 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# build-gtk4.sh — Compile and run native-hello using the GTK4 bridge on macOS. +# +# Usage: +# ./build-gtk4.sh # compile and run +# ./build-gtk4.sh compile # compile only +# ./build-gtk4.sh clean # remove build artefacts +# +# Requirements: +# - elc in PATH (or ../../../lang/dist/platform/elc) +# - clang (Xcode Command Line Tools) +# - GTK4 via Homebrew: brew install gtk4 +# - macOS 12+ +# +# This is the same pipeline as build.sh but targeting the GTK4 backend: +# - Links with $(pkg-config --libs gtk4) instead of -framework Cocoa +# - Compiles el_gtk4.c (plain C) instead of el_appkit.m (ObjC) +# - Defines EL_TARGET_LINUX (the GTK4 target flag) +# +# Multi-file compilation strategy: see build.sh comments — identical rationale. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" +EL_UI_ROOT="${SCRIPT_DIR}/../.." +EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" +EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" +BUILD_DIR="${SCRIPT_DIR}/build-gtk4" + +# Locate elc +if command -v elc &>/dev/null; then + ELC="elc" +elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then + ELC="${EL_LANG_ROOT}/dist/platform/elc" +else + echo "Error: elc not found. Add it to PATH or place it at:" + echo " ${EL_LANG_ROOT}/dist/platform/elc" + exit 1 +fi + +# GTK4 pkg-config flags +GTK4_CFLAGS=$(pkg-config --cflags gtk4) +GTK4_LIBS=$(pkg-config --libs gtk4) + +CLANG_FLAGS_COMMON=( + -std=c11 + -DEL_TARGET_LINUX + -I "${EL_RUNTIME}" +) + +clean() { + rm -rf "${BUILD_DIR}" + echo "Cleaned." +} + +compile() { + mkdir -p "${BUILD_DIR}" + + # ── 1. Vessel forward-declarations header ───────────────────────────────── + # elc --emit-header emits full function bodies, not just prototypes. + # Extract only the forward-declaration lines: stop at the first function + # definition (a line that ends with ') {' opening a body block). + # macOS awk does not support \s — use POSIX [[:space:]] instead. + echo "==> Generating el-native vessel header (declarations only)..." + "${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ + | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ + > "${BUILD_DIR}/el_native_vessel.h" + + # ── 2. Compile el-native vessel to C ────────────────────────────────────── + echo "==> Compiling el-native vessel to C..." + "${ELC}" "${EL_NATIVE_VESSEL}" \ + > "${BUILD_DIR}/el_native_vessel.c" + + # ── 3. Compile app entry to C ───────────────────────────────────────────── + echo "==> Compiling app entry to C..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${ELC}" "${SCRIPT_DIR}/src/main.el" \ + > "${BUILD_DIR}/native_hello.c" + + # ── 4. Compile el_gtk4.c (plain C, GTK4 backend) ───────────────────────── + echo "==> Compiling el_gtk4.c (GTK4 bridge)..." + # shellcheck disable=SC2086 + clang \ + -std=c11 \ + -DEL_TARGET_LINUX \ + -I "${EL_RUNTIME}" \ + ${GTK4_CFLAGS} \ + -c "${EL_RUNTIME}/el_gtk4.c" \ + -o "${BUILD_DIR}/el_gtk4.o" + + # ── 5a. Compile el_seed.c ───────────────────────────────────────────────── + # el_seed.c provides the OS-boundary __-prefixed primitives + thin wrappers. + # With -DEL_TARGET_LINUX, the EL_TARGET_LINUX section in el_seed.c is + # compiled in (GTK4 forwarders) and the EL_TARGET_MACOS section is excluded. + echo "==> Compiling el_seed.c (EL_TARGET_LINUX)..." + # shellcheck disable=SC2086 + clang "${CLANG_FLAGS_COMMON[@]}" \ + ${GTK4_CFLAGS} \ + -c "${EL_RUNTIME}/el_seed.c" \ + -o "${BUILD_DIR}/el_seed.o" + + # ── 5b. Compile el_runtime.c and patch out duplicate __-prefixed symbols ── + # Same strategy as build.sh: el_runtime.c has duplicates of the __-prefixed + # OS wrappers that el_seed.c defines. Hide el_runtime.o's copies with nmedit. + echo "==> Compiling el_runtime.c..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -c "${EL_RUNTIME}/el_runtime.c" \ + -o "${BUILD_DIR}/el_runtime.o" + + echo "==> Patching el_runtime.o (hiding __-prefixed symbols duplicated in el_seed.o)..." + nm "${BUILD_DIR}/el_seed.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.seed_T.txt" + nm "${BUILD_DIR}/el_runtime.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.rt_T.txt" + comm -23 "${BUILD_DIR}/.rt_T.txt" "${BUILD_DIR}/.seed_T.txt" \ + > "${BUILD_DIR}/.rt_keep.txt" + nmedit -s "${BUILD_DIR}/.rt_keep.txt" "${BUILD_DIR}/el_runtime.o" + + # ── 6. Compile el-native vessel object ──────────────────────────────────── + # Vessel code calls __-prefixed builtins. el_native_target.h provides the + # EL_TARGET_LINUX block of declarations (same function signatures as MACOS). + echo "==> Compiling el-native vessel object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -c "${BUILD_DIR}/el_native_vessel.c" \ + -o "${BUILD_DIR}/el_native_vessel.o" + + echo "==> Patching el_native_vessel.o (hiding vessel main)..." + nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ + | awk '/^[0-9a-f]+ T _/{print $3}' \ + | grep -v '^_main$' \ + > "${BUILD_DIR}/.vessel_keep.txt" + nmedit -s "${BUILD_DIR}/.vessel_keep.txt" "${BUILD_DIR}/el_native_vessel.o" + + # ── 7. Compile app object ───────────────────────────────────────────────── + # Inject el_native_target.h (float ops + EL_TARGET_LINUX native decls) and + # the vessel's forward-declaration header. + echo "==> Compiling app object..." + clang "${CLANG_FLAGS_COMMON[@]}" \ + -include "${EL_RUNTIME}/el_native_target.h" \ + -include "${BUILD_DIR}/el_native_vessel.h" \ + -c "${BUILD_DIR}/native_hello.c" \ + -o "${BUILD_DIR}/native_hello.o" + + # ── 8. Link ─────────────────────────────────────────────────────────────── + # el_seed.o = OS-boundary primitives (__-prefixed) + GTK4 forwarders + # el_runtime.o = high-level builtins (str_len, json_*, etc.), patched + # el_native_vessel.o = el-level widget API, patched + # el_gtk4.o = GTK4 C bridge + # GTK4_LIBS = $(pkg-config --libs gtk4) + # -ldl = dlsym for callback dispatch + # -lpthread = threading + echo "==> Linking native-hello-gtk4..." + # shellcheck disable=SC2086 + clang "${CLANG_FLAGS_COMMON[@]}" \ + "${BUILD_DIR}/native_hello.o" \ + "${BUILD_DIR}/el_native_vessel.o" \ + "${BUILD_DIR}/el_gtk4.o" \ + "${BUILD_DIR}/el_runtime.o" \ + "${BUILD_DIR}/el_seed.o" \ + ${GTK4_LIBS} \ + -ldl \ + -lpthread \ + -lcurl \ + -o "${BUILD_DIR}/native-hello-gtk4" + + echo "==> Build complete: ${BUILD_DIR}/native-hello-gtk4" +} + +run() { + compile + echo "==> Launching native-hello-gtk4..." + EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ + "${BUILD_DIR}/native-hello-gtk4" +} + +case "${1:-run}" in + clean) clean ;; + compile) compile ;; + run|*) run ;; +esac diff --git a/ui/examples/native-hello/build-gtk4/.rt_T.txt b/ui/examples/native-hello/build-gtk4/.rt_T.txt new file mode 100644 index 0000000..7657b58 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.rt_T.txt @@ -0,0 +1,397 @@ +___args_json +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___http_conn_fd +___http_do +___http_do_map +___http_do_map_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_raw +___json_parse_map +___json_set +___json_stringify_val +___mutex_lock +___mutex_new +___mutex_unlock +___print +___print_raw +___println +___read_n +___readline +___sha256_hex +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___thread_create +___thread_join +___url_decode +___url_encode +___uuid_v4 +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_request_end +_el_request_start +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build-gtk4/.rt_keep.txt b/ui/examples/native-hello/build-gtk4/.rt_keep.txt new file mode 100644 index 0000000..6aa9058 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.rt_keep.txt @@ -0,0 +1,340 @@ +___channel_close +___channel_new +___channel_recv +___channel_send +___channel_try_recv +___http_do_map +___http_do_map_to_file +___print_raw +___read_n +_aead_decrypt +_aead_encrypt +_append +_args +_base64_decode +_base64_encode +_base64url_decode +_base64url_encode +_bool_to_str +_bytes_to_str +_cal_cycle_phase +_cal_format +_cal_in +_cal_to_instant +_config +_cycle_calendar +_decimal_round +_duration_millis +_duration_nanos +_duration_seconds +_duration_to_millis +_duration_to_nanos +_duration_to_seconds +_earth_calendar +_earth_calendar_default +_el_abs +_el_arena_pop +_el_arena_push +_el_base64_encode_n +_el_cgi_init +_el_duration_add +_el_duration_div +_el_duration_eq +_el_duration_from_nanos +_el_duration_ge +_el_duration_gt +_el_duration_le +_el_duration_lt +_el_duration_ne +_el_duration_scale +_el_duration_sub +_el_get_field +_el_html_sanitize +_el_instant_add_dur +_el_instant_diff +_el_instant_eq +_el_instant_ge +_el_instant_gt +_el_instant_le +_el_instant_lt +_el_instant_ne +_el_instant_sub_dur +_el_list_append +_el_list_clone +_el_list_empty +_el_list_get +_el_list_len +_el_list_new +_el_local_date_add_dur +_el_local_date_eq +_el_local_date_lt +_el_local_time_add_dur +_el_map_get +_el_map_new +_el_map_set +_el_max +_el_mem_check +_el_min +_el_now_instant +_el_release +_el_retain +_el_runtime_init_args +_el_runtime_register_handler +_el_runtime_register_handler_v2 +_el_sha256_bytes_n +_el_sleep_duration +_el_str_concat +_engram_activate +_engram_activate_json +_engram_add_layer +_engram_apply_decay_json +_engram_compile_layered_json +_engram_connect +_engram_edge_between +_engram_edge_count +_engram_forget +_engram_get_node +_engram_get_node_json +_engram_list_layers +_engram_list_layers_json +_engram_load +_engram_neighbors +_engram_neighbors_filtered +_engram_neighbors_json +_engram_node +_engram_node_count +_engram_node_full +_engram_node_layered +_engram_query_range +_engram_remove_layer +_engram_save +_engram_scan_nodes +_engram_scan_nodes_by_type_json +_engram_scan_nodes_json +_engram_search +_engram_search_json +_engram_stats_json +_engram_strengthen +_env +_exec +_exec_bg +_exec_capture +_exec_command +_exit_program +_float_to_int +_float_to_str +_format_float +_fs_exists +_fs_list +_fs_list_json +_fs_mkdir +_fs_read +_fs_write +_fs_write_bytes +_get +_getpid_now +_hash_sha256 +_hmac_sha256_bytes +_hmac_sha256_hex +_html_escape +_html_raw +_http_delete +_http_get +_http_get_engram +_http_get_to_file +_http_get_with_headers +_http_patch +_http_post +_http_post_engram +_http_post_form_auth +_http_post_json +_http_post_json_with_headers +_http_post_to_file +_http_post_with_headers +_http_response +_http_serve +_http_serve_v2 +_http_set_handler +_http_set_handler_v2 +_in_calendar +_instant_from_iso8601 +_instant_to_iso8601 +_instant_to_unix_millis +_instant_to_unix_seconds +_int_to_float +_int_to_str +_is_alphanumeric +_is_digit +_is_letter +_is_lowercase +_is_punctuation +_is_uppercase +_is_whitespace +_json_array_get +_json_array_get_string +_json_array_len +_json_array_push +_json_build_array +_json_build_object +_json_escape_string +_json_get +_json_get_bool +_json_get_float +_json_get_int +_json_get_raw +_json_get_string +_json_parse +_json_set +_json_stringify +_len +_list_get +_list_join +_list_len +_list_push +_list_push_front +_list_range +_llm_call +_llm_call_agentic +_llm_call_system +_llm_models +_llm_register_tool +_llm_vision +_local_date +_local_date_day +_local_date_month +_local_date_year +_local_datetime +_local_time +_local_time_hour +_local_time_minute +_local_time_nanos +_local_time_second +_log_info +_log_warn +_map_get +_map_set +_mars_calendar +_math_cos +_math_ln +_math_log +_math_pi +_math_sin +_math_sqrt +_native_int_to_str +_native_list_append +_native_list_clone +_native_list_empty +_native_list_get +_native_list_len +_native_str_to_int +_native_string_chars +_no_cycle_calendar +_now +_now_in +_now_millis +_now_ns +_parse_int +_pq_hybrid_handshake +_pq_hybrid_keygen +_pq_kem_decaps +_pq_kem_encaps +_pq_kem_keygen +_pq_keygen_signature +_pq_sign +_pq_verify +_print +_println +_readline +_relative_calendar +_rhythm_and +_rhythm_cycle_phase +_rhythm_cycle_start +_rhythm_duration +_rhythm_event +_rhythm_matches +_rhythm_next_after +_rhythm_or +_rhythm_session_start +_rhythm_weekday +_rhythm_weekly_at +_sha256_bytes +_sha256_hex +_sha3_256_hex +_sleep_ms +_sleep_secs +_state_del +_state_get +_state_get_or +_state_has +_state_keys +_state_set +_stdout_restore +_stdout_to_file +_str_char_at +_str_char_code +_str_concat +_str_contains +_str_count +_str_count_bytes +_str_count_chars +_str_count_digits +_str_count_letters +_str_count_lines +_str_count_words +_str_ends_with +_str_eq +_str_find_chars +_str_format +_str_index_of +_str_index_of_all +_str_join +_str_last_index_of +_str_len +_str_lower +_str_lstrip +_str_pad_left +_str_pad_right +_str_repeat +_str_replace +_str_reverse +_str_rstrip +_str_slice +_str_split +_str_split_chars +_str_split_lines +_str_split_n +_str_starts_with +_str_strip_chars +_str_strip_prefix +_str_strip_suffix +_str_to_bytes +_str_to_float +_str_to_int +_str_to_lower +_str_to_upper +_str_trim +_str_upper +_time_add +_time_diff +_time_format +_time_from_parts +_time_now +_time_now_ms +_time_now_utc +_time_to_parts +_ttl_cache_age +_ttl_cache_get +_ttl_cache_set +_unix_millis +_unix_seconds +_unix_timestamp +_unix_timestamp_ms +_url_decode +_url_encode +_uuid_new +_uuid_v4 +_zone +_zone_local +_zone_offset +_zone_utc +_zoned diff --git a/ui/examples/native-hello/build-gtk4/.seed_T.txt b/ui/examples/native-hello/build-gtk4/.seed_T.txt new file mode 100644 index 0000000..260f1fe --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.seed_T.txt @@ -0,0 +1,137 @@ +___args_json +___button_create +___cos_f +___engram_activate +___engram_activate_json +___engram_add_layer +___engram_compile_layered_json +___engram_connect +___engram_edge_between +___engram_edge_count +___engram_forget +___engram_get_node +___engram_get_node_json +___engram_list_layers +___engram_list_layers_json +___engram_load +___engram_neighbors +___engram_neighbors_filtered +___engram_neighbors_json +___engram_node +___engram_node_count +___engram_node_full +___engram_node_layered +___engram_remove_layer +___engram_save +___engram_scan_nodes +___engram_scan_nodes_by_type_json +___engram_scan_nodes_json +___engram_search +___engram_search_json +___engram_stats_json +___engram_strengthen +___env_get +___exec +___exec_bg +___exit_program +___float_to_str +___fs_exists +___fs_list_raw +___fs_mkdir +___fs_read +___fs_write +___fs_write_bytes +___hstack_create +___html_sanitize +___http_conn_fd +___http_do +___http_do_to_file +___http_response +___http_serve +___http_serve_v2 +___http_sse_close +___http_sse_open +___http_sse_send +___image_create +___int_to_str +___json_array_get +___json_array_get_string +___json_array_len +___json_get +___json_get_bool +___json_get_float +___json_get_int +___json_get_raw +___json_get_string +___json_parse +___json_parse_map +___json_set +___json_stringify +___json_stringify_val +___label_create +___ln_f +___log_f +___manifest_read +___mutex_lock +___mutex_new +___mutex_unlock +___native_init +___native_run_loop +___pi_f +___print +___println +___readline +___scroll_create +___sha256_hex +___sin_f +___sleep_ms +___sqrt_f +___state_del +___state_get +___state_keys +___state_set +___str_alloc +___str_char_at +___str_cmp +___str_concat_raw +___str_len +___str_ncmp +___str_set_char +___str_slice_raw +___str_to_float +___str_to_int +___text_area_create +___text_field_create +___thread_create +___thread_join +___time_now_ns +___url_decode +___url_encode +___uuid_v4 +___vstack_create +___widget_add_child +___widget_destroy +___widget_get_text +___widget_on_change +___widget_on_click +___widget_on_submit +___widget_remove_child +___widget_set_bg_color +___widget_set_color +___widget_set_corner_radius +___widget_set_disabled +___widget_set_flex +___widget_set_font +___widget_set_height +___widget_set_hidden +___widget_set_padding +___widget_set_text +___widget_set_width +___window_create +___window_set_title +___window_show +___zstack_create +_el_request_end +_el_request_start +_el_seed_init_args +_el_seed_set_http_conn_fd diff --git a/ui/examples/native-hello/build-gtk4/.vessel_keep.txt b/ui/examples/native-hello/build-gtk4/.vessel_keep.txt new file mode 100644 index 0000000..a8a7964 --- /dev/null +++ b/ui/examples/native-hello/build-gtk4/.vessel_keep.txt @@ -0,0 +1,55 @@ +_button +_color_hex_a +_color_hex_b +_color_hex_g +_color_hex_r +_hex_channel +_hex_nibble +_hstack +_image +_label +_manifest_height +_manifest_min_height +_manifest_min_width +_manifest_read +_manifest_title +_manifest_width +_native_init +_native_run_loop +_scroll +_style_button_primary +_style_label_body +_style_label_heading +_style_label_muted +_style_surface +_text_area +_text_field +_vstack +_vstack_tight +_widget_add_child +_widget_destroy +_widget_get_text +_widget_on_change +_widget_on_click +_widget_on_submit +_widget_remove_child +_widget_set_bg_color +_widget_set_bg_color_hex +_widget_set_color +_widget_set_color_hex +_widget_set_corner_radius +_widget_set_disabled +_widget_set_flex +_widget_set_font +_widget_set_height +_widget_set_hidden +_widget_set_padding +_widget_set_padding_all +_widget_set_padding_xy +_widget_set_text +_widget_set_width +_window_create +_window_from_manifest +_window_set_title +_window_show +_zstack diff --git a/ui/examples/native-hello/build-gtk4/el_gtk4.o b/ui/examples/native-hello/build-gtk4/el_gtk4.o new file mode 100644 index 0000000000000000000000000000000000000000..34d1efcf173a8d21aa8c484458cb3957559902f2 GIT binary patch literal 33464 zcmeI54Rlr4b?2{mV1x}Zj&Y4^405n**+vddNsJS2{Tz`7*RNB?r7bgtJcJ~WijagP zY`M@GwIU6%X^T^)Wm?*Zm`;U zp1b(j@3<~l_G|PhrHG<>rQtdFl*hS%empmd^7+2j{yxvs?|gp4M>cLKhQr&)_xLyE zrz}UGj-s#YWp*^+IsSJ(zqPkFLCELVKDpt^A|c38UyvY*GDWtpa3Y+~H~04Sw0CYd zT3Ws!*9T1(7qW@Oyjt}OMDpv}+1=FKm*3s_e0yh0KA&4#VDfVL2_G7o=W+3ey?n24 zjiT@Oq>?P(2W>GwzWDE?Kz?`>N%YmNQS^skZBG=PTo^^S@V=fOJ*d$A)PXzrp@?tz zxiJ|1(e*tw$FDa=FW1t{wKdtXwkTSKJd5t&r#>2ZhN>K_uZhOU{2)Is*FZaZdf6!N zhko^RepKnGz}2WGdzEwt?{KC&*=Wzgnqm)pr*4P~0KvT~dd^8tO0y{nfZS9Bo`S`kBnK(N|6{9X&R< zbhM7NE?s8njFmAqF*|y{D|@!jbMj7K?};cH5q$_9D9t2#pr0+OrEJM1-Zc8SbZ7P| zI=DLAm>+fT^P;igjJ+@5{q)B?{WjhQ?fnkL`Pht$zaYALkvu5FwWrE@H+f5+7nesb zPoq=iJ$<{+clu7B=k(n^zv*Q@FZd;SIN2%V`ks%_#x-8|jnN-X&W*-oN7P}=>M&M2 zC%fy#Inf3m9vGW%cv_l8YplaXmIHa zb}?h^=lj|1p66^alb@+2Zm>se%Y~CCJJ*)j25zPG7OqFrxVn>gAXp4MP`8$wpmYl!R8!&iEht^TF{>FPxvbLG