Compare commits

..

15 Commits

Author SHA1 Message Date
Tim Lingo a36a62ca14 fix(el-runtime): promote http_handler typedefs to el_runtime.h (cross-module + Windows)
El SDK Release / build-and-release (pull_request) Failing after 13m0s
http_handler_fn / http_handler4_fn were defined only inside el_runtime.c, so soul
modules (routes/chat/...) that reference them via cross-module forward declarations
couldn't see the types — which broke the Windows link of every module. Moving the
public function-pointer types to the shared header is the correct home and unblocks
the build on all platforms (identical typedef, C11-safe redefinition in el_runtime.c).

With this, the soul links into a native Windows neuron.exe (mingw, static) that boots
and serves HTTP on :7770 — verified /health → 200 {"status":"alive",...} in a Win11 VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:14:17 -05:00
Tim Lingo 28ef43264a feat(el-runtime): native Windows port of el_runtime.c (winsock/dlsym/CreateProcess)
Compiles for Windows x64 via mingw-w64 and still compiles clean on POSIX
(darwin/linux) — all Windows code is behind #ifdef _WIN32, POSIX path unchanged.

- el_platform_win.h (new): winsock2 + auto WSAStartup, el_closesocket(),
  dlsym->GetProcAddress, popen/_popen, mkdir/_mkdir, setenv/_putenv_s,
  timegm/_mkgmtime, localtime_r/gmtime_r. Threading unchanged — mingw
  winpthreads supplies <pthread.h> + -lpthread.
- el_runtime.c: include block guarded; 10 socket-close sites -> el_closesocket();
  setsockopt arg4 cast; tm_zone guarded; exec_bg fork/exec -> CreateProcess.

Part of feat/windows-port. Core-el change, for Will's review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:58:11 -05:00
Tim Lingo c2afcbddf5 fix(engram): allow SessionSummary node_type in validation allowlist
El SDK CI - dev / build-and-test (pull_request) Successful in 3m47s
handle_api_consolidate writes a "SessionSummary" node, but engram_valid_node_type
omitted it — so once this validation ships, every consolidate() would be silently
REJECTED at the engram boundary. Add SessionSummary to the allowlist.

Found in Will's PR review of neuron #1 / el #52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 06:26:25 -05:00
Tim Lingo dfe4e83ed1 Fix engram_node_full wrapper field corruption + add node_type/tier validation
El SDK Release / build-and-release (pull_request) Failing after 9s
The wrapper signature was stale and didn't match the C primitive
__engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags).
Because el_val_t is an untyped machine word, the compiler coerced caller args to the
wrong declared param types and forwarded them BY POSITION — so tier received an int,
importance/confidence received strings, label received a float, etc. (~100 corrupt nodes).

- Correct the wrapper to match the C contract 1:1 (no coercion, no reorder).
- Add engram_valid_node_type / engram_valid_tier allowlists; engram_node and
  engram_node_full now reject invalid values with __println + return "" (fail loud,
  no silent malformed write).

See neuron repo: HANDOFF-engram-write-corruption.md for the full write-up + deploy runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:13:43 -05:00
will.anderson 2ed6b26dde Merge pull request 'promote: stage → main (all elb linker fixes + ci-base rebuild)' (#42) from stage into main
El SDK Release / build-and-release (push) Successful in 6m28s
promote: stage → main (all elb linker fixes + ci-base rebuild)
2026-05-07 14:25:37 +00:00
will.anderson d8e9fd12f4 Merge pull request 'promote: dev → stage (all elb linker fixes)' (#41) from dev into stage
El SDK Release / build-and-release (pull_request) Successful in 3m51s
El SDK CI - stage / build-and-test (push) Successful in 4m11s
promote: dev → stage (all elb linker fixes)
2026-05-07 14:20:53 +00:00
will.anderson 8fa9c4ba20 Merge pull request 'promote: dev → stage (elb linker fixes)' (#38) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 1m2s
El SDK CI - stage / build-and-test (push) Successful in 3m56s
promote: dev → stage (elb linker fixes)
2026-05-07 08:11:38 +00:00
will.anderson 9c7bde47dc Merge pull request 'promote: dev → stage (elb gcc fix)' (#35) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 40s
El SDK CI - stage / build-and-test (push) Successful in 3m45s
promote: dev → stage (elb gcc fix)
2026-05-07 08:01:22 +00:00
will.anderson c0553459e1 Merge pull request 'promote: dev → stage (CI rebuild fix + ci-base refresh)' (#32) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 35s
El SDK CI - stage / build-and-test (push) Successful in 3m47s
promote: dev → stage (CI rebuild fix + ci-base refresh)
2026-05-07 07:50:27 +00:00
will.anderson fd208583fe Merge pull request 'promote: dev → stage (elb build fix)' (#28) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m51s
El SDK Release / build-and-release (pull_request) Failing after 38s
promote: dev → stage (elb build fix)
2026-05-07 02:46:27 +00:00
will.anderson 3e29fc43ab Merge pull request 'promote: dev → stage (__http_do_map_to_file)' (#25) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m44s
El SDK Release / build-and-release (pull_request) Failing after 47s
2026-05-07 02:14:30 +00:00
will.anderson 979a5677d5 Merge pull request 'promote: dev → stage (__-prefixed runtime fix)' (#22) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m48s
El SDK Release / build-and-release (pull_request) Failing after 1m4s
2026-05-07 01:48:32 +00:00
will.anderson 17b1aa0736 Merge pull request 'promote: dev → stage (return type fix)' (#19) from dev into stage
El SDK CI - stage / build-and-test (push) Failing after 4m1s
El SDK Release / build-and-release (pull_request) Failing after 42s
2026-05-07 01:12:18 +00:00
will.anderson f0c731d2db Merge pull request 'promote: dev → stage (runtime fix)' (#16) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m43s
El SDK Release / build-and-release (pull_request) Failing after 45s
2026-05-07 00:43:52 +00:00
will.anderson e7e0f7d3e5 Merge pull request 'promote: dev → stage' (#12) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 4m3s
El SDK Release / build-and-release (pull_request) Failing after 37s
2026-05-07 00:23:46 +00:00
6 changed files with 282 additions and 316 deletions
@@ -0,0 +1,94 @@
#ifndef EL_PLATFORM_WIN_H
#define EL_PLATFORM_WIN_H
/*
* el_platform_win.h — Windows OS-boundary shim for el_runtime.c.
*
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
*
* What it maps:
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
* Sockets close with closesocket() (see el_closesocket), and the stack must be
* started once with WSAStartup — done automatically via a load-time constructor.
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
* exported by the main module. Windows equivalent: GetProcAddress on the process
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
* - popen : mapped to _popen/_pclose.
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <io.h>
#include <process.h>
/* Portable headers mingw-w64 provides (verified present). */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
/* ── socket close ─────────────────────────────────────────────────────────── */
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
build defines the same helper as close() so the call sites are identical across platforms. */
static inline int el_closesocket(int s) { return closesocket((SOCKET)s); }
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
static void el__win_net_init(void) {
static int inited = 0;
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
}
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT ((void*)0)
#endif
static inline void* el_win_dlsym(void* handle, const char* name) {
(void)handle;
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
}
#define dlsym(h, n) el_win_dlsym((h), (n))
/* ── popen / pclose ───────────────────────────────────────────────────────── */
#define popen _popen
#define pclose _pclose
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
#include <direct.h> /* _mkdir */
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
#define timegm _mkgmtime /* UTC tm → time_t */
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s. */
static inline int setenv(const char* name, const char* value, int overwrite) {
(void)overwrite;
return _putenv_s(name, value ? value : "");
}
static inline int unsetenv(const char* name) { return _putenv_s(name, ""); }
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
}
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
}
#endif /* EL_PLATFORM_WIN_H */
+45 -14
View File
@@ -21,6 +21,10 @@
#include "el_runtime.h"
#ifdef _WIN32
/* Windows OS-boundary shim (winsock/dlsym/popen). Threading stays on <pthread.h> (winpthreads). */
#include "el_platform_win.h"
#else
#include <stdarg.h>
#include <strings.h> /* strcasecmp */
#include <stdint.h>
@@ -42,6 +46,10 @@
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
/* On POSIX, sockets close with the same close() as files; el_platform_win.h supplies the Windows
variant. Defined here so the socket call sites are identical across platforms. */
static inline int el_closesocket(int s) { return close(s); }
#endif
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
@@ -1587,17 +1595,17 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
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));
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&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;
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
@@ -1614,11 +1622,11 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
_http_conn_active++;
pthread_mutex_unlock(&_http_conn_mu);
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
if (!arg) { close(cfd); continue; }
if (!arg) { el_closesocket(cfd); continue; }
arg->fd = cfd;
pthread_t tid;
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
close(cfd); free(arg);
el_closesocket(cfd); free(arg);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -1627,7 +1635,7 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
}
pthread_detach(tid);
}
close(sock);
el_closesocket(sock);
return 0;
}
@@ -1837,17 +1845,17 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
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));
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&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;
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
@@ -1864,11 +1872,11 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
_http_conn_active++;
pthread_mutex_unlock(&_http_conn_mu);
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
if (!arg) { close(cfd); continue; }
if (!arg) { el_closesocket(cfd); continue; }
arg->fd = cfd;
pthread_t tid;
if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) {
close(cfd); free(arg);
el_closesocket(cfd); free(arg);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -1877,7 +1885,7 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
}
pthread_detach(tid);
}
close(sock);
el_closesocket(sock);
return 0;
}
@@ -2051,6 +2059,23 @@ el_val_t exec(el_val_t cmdv) {
el_val_t exec_bg(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
#ifdef _WIN32
/* Windows: no fork/exec. Launch a detached `cmd /c <command>` with no console window via
CreateProcess (DETACHED_PROCESS | CREATE_NO_WINDOW). Returns the PID as a string, "" on fail.
Mirrors the POSIX branch: child runs independently, caller is not blocked. */
char cmdline[8192];
snprintf(cmdline, sizeof(cmdline), "cmd.exe /c %s", cmd);
STARTUPINFOA si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si);
PROCESS_INFORMATION pi; ZeroMemory(&pi, sizeof(pi));
BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
DETACHED_PROCESS | CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
if (!ok) return el_wrap_str(el_strdup(""));
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%lu", (unsigned long)pi.dwProcessId);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return el_wrap_str(el_strdup(pidbuf));
#else
pid_t pid = fork();
if (pid < 0) {
/* fork failed */
@@ -2073,6 +2098,7 @@ el_val_t exec_bg(el_val_t cmdv) {
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
return el_wrap_str(el_strdup(pidbuf));
#endif
}
el_val_t fs_list(el_val_t pathv) {
@@ -4337,7 +4363,12 @@ static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_le
localtime_r(&s, &tm);
*tm_out = tm;
if (abbr_buf && abbr_cap > 0) {
/* mingw's struct tm has no tm_zone (BSD/glibc extension); no abbrev available there. */
#ifdef _WIN32
const char* z_str = "";
#else
const char* z_str = tm.tm_zone ? tm.tm_zone : "";
#endif
size_t n = strlen(z_str);
if (n >= abbr_cap) n = abbr_cap - 1;
memcpy(abbr_buf, z_str, n);
+6
View File
@@ -52,6 +52,12 @@
typedef int64_t el_val_t;
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
* register handlers across translation units; previously defined only inside el_runtime.c, which
* made cross-module references (and the Windows build) fail. Home in the shared header. */
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
-15
View File
@@ -324,10 +324,6 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let each_c: String = cg_html_each(child, acc_var)
let parts = native_list_append(parts, each_c)
}
if str_eq(html_kind, "HtmlIf") {
let if_c: String = cg_html_if(child, acc_var)
let parts = native_list_append(parts, if_c)
}
let i = i + 1
}
str_join(parts, "")
@@ -417,17 +413,6 @@ fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
// Generate code for {#if cond} ... {/if} (with optional {#else}).
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
let cond_expr = node["cond"]
let then_children: [Map<String, Any>] = node["then"]
let else_children: [Map<String, Any>] = node["else"]
let cond_c: String = cg_expr(cond_expr)
let then_c: String = cg_html_parts(then_children, acc_var)
let else_c: String = cg_html_parts(else_children, acc_var)
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
}
// Top-level HTML template codegen returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
+94 -284
View File
@@ -293,48 +293,6 @@ fn is_void_element(name: String) -> Bool {
false
}
// Collect all tokens as raw text until </tag_name> is encountered.
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
// Returns { "text": "...", "pos": p_after_closing_tag }
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> Map<String, Any> {
let parts: [String] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// Check if this is </tag_name>
let close_name = tok_value(tokens, p + 2)
if str_eq(close_name, tag_name) {
// consume </tag_name>
let p = p + 3
let p = expect(tokens, p, "Gt")
let running = false
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
} else {
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
}
}
{ "text": str_join(parts, ""), "pos": p }
}
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
@@ -362,7 +320,7 @@ fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
}
}
}
{ "text": str_join(parts, ""), "pos": p }
{ "text": str_join(parts, " "), "pos": p }
}
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
@@ -477,125 +435,77 @@ fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<Strin
}
} else {
if str_eq(k, "LBrace") {
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
// Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
// LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
// {#else} lexes as LBrace Else RBrace.
// Interpolation: {expr} or {#each ...} or {/each}
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// {/each} or {/if} end of block, stop
// skip { /
let p = p + 2
// skip the close-tag name token (each, if, etc.)
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
if str_eq(k2, "If") {
// {#if condition} ... {/if}
// Skip { if (2 tokens; '#' was silently skipped by lexer)
let p = p + 2
// Parse condition expression (no block expr)
if str_eq(k2, "Hash") {
// {#each list as item}
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "each") {
let p = p + 3
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_cond = parse_expr(tokens, p)
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let cond_expr = r_cond["node"]
let p = r_cond["pos"]
el_release(r_cond)
let list_expr = r_list["node"]
let p = r_list["pos"]
// r_list result map fully consumed release to free peak heap.
el_release(r_list)
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse then-children until {#else} or {/if}
let r_then = parse_html_children(tokens, p, "__if_then__")
let then_children = r_then["children"]
let p = r_then["pos"]
el_release(r_then)
// check for {#else} lexed as LBrace Else RBrace
let else_children: [Map<String, Any>] = native_list_empty()
let ck = tok_kind(tokens, p)
if str_eq(ck, "LBrace") {
let ck2 = tok_kind(tokens, p + 1)
if str_eq(ck2, "Else") {
// consume { else }
let p = p + 2
let p = expect(tokens, p, "RBrace")
// parse else-children until {/if}
let r_else = parse_html_children(tokens, p, "__if_else__")
let else_children = r_else["children"]
let p = r_else["pos"]
el_release(r_else)
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
// r_body result map fully consumed release to free peak heap.
el_release(r_body)
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
let p = p + 1
}
} else {
if str_eq(k2, "Slash") {
// {/each} end of each block, stop
// skip {/each}
let p = p + 2
// skip "each"
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
// regular {expr}
let r = parse_expr(tokens, p + 1)
let interp_val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
let children = native_list_append(children, if_node)
} else {
if str_eq(k2, "Else") {
// {#else} sentinel lexed as LBrace Else RBrace
// Do NOT consume leave position for caller ({#if} handler checks for it)
let running = false
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
// Check for {#each list as item} lexed as LBrace Ident:"each" ...
let k2_v = tok_value(tokens, p + 1)
if str_eq(k2_v, "each") {
let p = p + 2
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let list_expr = r_list["node"]
let p = r_list["pos"]
// r_list result map fully consumed release to free peak heap.
el_release(r_list)
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
// r_body result map fully consumed release to free peak heap.
el_release(r_body)
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
// regular {expr} disable map-literal parsing so {fn(a,b)}
// does not trigger the LBracemap path inside parse_primary
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r = parse_expr(tokens, p + 1)
state_set("__no_block_expr", prev_no_block)
let interp_val = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
}
@@ -655,27 +565,6 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
if is_void_element(tag_name) {
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// raw-text mode for style/script collect content as plain text without parsing CSS/JS as El
if str_eq(tag_name, "style") {
let r_raw = parse_raw_text_content(tokens, p, "style")
let raw_text: String = r_raw["text"]
let p = r_raw["pos"]
el_release(r_raw)
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
let raw_children: [Map<String, Any>] = native_list_empty()
let raw_children = native_list_append(raw_children, raw_child)
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
}
if str_eq(tag_name, "script") {
let r_raw = parse_raw_text_content(tokens, p, "script")
let raw_text: String = r_raw["text"]
let p = r_raw["pos"]
el_release(r_raw)
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
let raw_children: [Map<String, Any>] = native_list_empty()
let raw_children = native_list_append(raw_children, raw_child)
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
}
// parse children
let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"]
@@ -829,123 +718,44 @@ fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
// as the start of the block they're expecting.
return make_result({ "expr": "Nil" }, pos)
}
// Distinguish map literal from interpolation chain.
// A map literal requires { key: value } the second token inside { must be Colon.
// An empty {} is a map literal. Everything else is an interpolation chain.
let first_k: String = tok_kind(tokens, pos + 1)
let second_k: String = tok_kind(tokens, pos + 2)
if str_eq(first_k, "RBrace") {
// Empty map literal {}
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
}
if str_eq(second_k, "Colon") {
// MAP LITERAL: { key: value, ... }
let p = pos + 1
let pairs: [Map<String, Any>] = native_list_empty()
let running = true
while running {
let k2 = tok_kind(tokens, p)
if k2 == "RBrace" {
let p = pos + 1
let pairs: [Map<String, Any>] = native_list_empty()
let running = true
while running {
let k2 = tok_kind(tokens, p)
if k2 == "RBrace" {
let running = false
} else {
if k2 == "Eof" {
let running = false
} else {
if k2 == "Eof" {
let running = false
// key: Str token
let key = tok_value(tokens, p)
let new_p: Int = p + 1
let new_p = expect(tokens, new_p, "Colon")
let r = parse_expr(tokens, new_p)
let val_node = r["node"]
let new_p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let pair = { "key": key, "value": val_node }
let pairs = native_list_append(pairs, pair)
let k3 = tok_kind(tokens, new_p)
if k3 == "Comma" {
let new_p = new_p + 1
}
// Non-progress guard: malformed map content can leave
// parse_expr returning the same pos. Force advance.
if new_p <= p {
let p = p + 1
} else {
// key: Str or Ident token
let key = tok_value(tokens, p)
let new_p: Int = p + 1
let new_p = expect(tokens, new_p, "Colon")
let r = parse_expr(tokens, new_p)
let val_node = r["node"]
let new_p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let pair = { "key": key, "value": val_node }
let pairs = native_list_append(pairs, pair)
let k3 = tok_kind(tokens, new_p)
if k3 == "Comma" {
let new_p = new_p + 1
}
// Non-progress guard: malformed map content can leave
// parse_expr returning the same pos. Force advance.
if new_p <= p {
let p = p + 1
} else {
let p = new_p
}
let p = new_p
}
}
}
let p = expect(tokens, p, "RBrace")
return make_result({ "expr": "Map", "pairs": pairs }, p)
}
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
// Build a BinOp(Plus, ...) concatenation chain.
let p = pos
let chain_node: Map<String, Any> = { "expr": "Nil" }
let chain_started = false
let chain_running = true
while chain_running {
let ck: String = tok_kind(tokens, p)
if str_eq(ck, "LBrace") {
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r = parse_expr(tokens, p + 1)
state_set("__no_block_expr", prev_no_block)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
let p = expect(tokens, p, "RBrace")
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
if str_eq(ck, "Lt") {
let ck2: String = tok_kind(tokens, p + 1)
if str_eq(ck2, "Not") {
let r = parse_html_template(tokens, p)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
if str_eq(ck2, "Ident") {
let tag_candidate: String = tok_value(tokens, p + 1)
if is_html_tag_name(tag_candidate) {
let r = parse_html_template(tokens, p)
let part = r["node"]
let p = r["pos"]
// r result map fully consumed release to free peak heap.
el_release(r)
if !chain_started {
let chain_node = part
let chain_started = true
} else {
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
}
} else {
let chain_running = false
}
} else {
let chain_running = false
}
}
} else {
let chain_running = false
}
}
}
return make_result(chain_node, p)
let p = expect(tokens, p, "RBrace")
return make_result({ "expr": "Map", "pairs": pairs }, p)
}
// if expression
+43 -3
View File
@@ -6,15 +6,55 @@
//
// Dependencies: runtime/string.el, runtime/json.el
// --- Validation (defense in depth) ---
// el_val_t is an untyped machine word, so a wrong TYPE can't be caught here but a
// wrong VALUE can (a tier in the node_type slot, an empty/garbage string, an int, a
// path, a model name, a cgi id). Reject loudly instead of silently writing junk.
fn engram_valid_node_type(t: String) -> Bool {
return str_eq(t, "Memory") || str_eq(t, "Knowledge") || str_eq(t, "Belief")
|| str_eq(t, "Project") || str_eq(t, "Tag") || str_eq(t, "BacklogItem")
|| str_eq(t, "Artifact") || str_eq(t, "Conversation") || str_eq(t, "ExecutionContext")
|| str_eq(t, "InternalStateEvent") || str_eq(t, "Self") || str_eq(t, "Entity")
|| str_eq(t, "Process") || str_eq(t, "ConfigEntry") || str_eq(t, "Concept") || str_eq(t, "Imprint")
|| str_eq(t, "SessionSummary")
}
fn engram_valid_tier(t: String) -> Bool {
return str_eq(t, "Semantic") || str_eq(t, "Episodic") || str_eq(t, "Working")
|| str_eq(t, "Procedural") || str_eq(t, "Canonical") || str_eq(t, "Note") || str_eq(t, "Lesson")
}
// --- Node creation ---
fn engram_node(content: String, node_type: String, salience: Float) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "'")
return ""
}
return __engram_node(content, node_type, salience)
}
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
source: String, lang: String, ts: Int, tags: String) -> String {
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
// Signature MUST match the C primitive __engram_node_full exactly (el_seed.h):
// (content, node_type, label, salience, importance, confidence, tier, tags)
// The previous wrapper declared a stale 8-arg schema with wrong names AND types
// (sal:Float at the label slot, ts:Int at the tier slot). Because el_val_t is an
// untyped machine word, the EL compiler coerced caller args to those wrong param
// types and then forwarded them BY POSITION into the C function so tier received
// an int, importance/confidence received strings, label received a float, etc.
// That is the field-corruption bug. Match the contract 1:1 no coercion, no reorder.
fn engram_node_full(content: String, node_type: String, label: String,
salience: Float, importance: Float, confidence: Float,
tier: String, tags: String) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "' (label=" + label + ")")
return ""
}
if !engram_valid_tier(tier) {
__println("[engram] REJECTED node write — invalid tier '" + tier + "' (node_type=" + node_type + ", label=" + label + ")")
return ""
}
return __engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
}
// --- Node retrieval ---