fix: three foundation/el root-cause bugs (no more bandaids)
1. Parser+codegen: bare reassignment `x = expr` inside an if-body
was compiling to three orphan expressions with no store. Now
emits a real assignment.
2. Runtime json_get: dot-path segments that are all digits now
correctly traverse array indices. `json_get(s, "0.field")` works.
3. Runtime HTTP writer: response bodies starting with
`{"__status__":<int>,...}` now set the HTTP status header to
that value and strip the marker from the served body. Existing
404/401/503 paths in product code now produce real status codes
instead of HTTP 200 with the status hidden in the body.
Self-host fixed point holds: gen2 == gen3 byte-identical.
Snapshot tagged at dist/platform/elc.20260502-1231-self-host.
Backlog: bl-c121edda
This commit is contained in:
@@ -1264,6 +1264,70 @@ static int http_parse_envelope(const char* s, int* out_status,
|
||||
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__":<int>,...}` 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. */
|
||||
@@ -1283,6 +1347,19 @@ static void http_send_response(int fd, const char* body) {
|
||||
&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
|
||||
@@ -1893,30 +1970,81 @@ el_val_t url_decode(el_val_t sv) {
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
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(""));
|
||||
size_t klen = strlen(key);
|
||||
/* Use a stack buffer for the pattern to avoid arena double-free.
|
||||
* Keys in El maps are typically short; 512 bytes is a safe upper bound. */
|
||||
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(""));
|
||||
/* 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;
|
||||
}
|
||||
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": */
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
|
||||
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++;
|
||||
/* Unescape the JSON string value into a clean buffer. */
|
||||
size_t cap = strlen(p) + 1;
|
||||
char* out = el_strbuf(cap);
|
||||
char* w = out;
|
||||
@@ -1940,15 +2068,70 @@ el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
|
||||
*w = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
const char* start = p;
|
||||
while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++;
|
||||
size_t len = (size_t)(p - start);
|
||||
char* out = el_strbuf(len);
|
||||
memcpy(out, start, len);
|
||||
out[len] = '\0';
|
||||
/* 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
|
||||
|
||||
Reference in New Issue
Block a user