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:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1264,6 +1264,70 @@ static int http_parse_envelope(const char* s, int* out_status,
|
|||||||
return 1;
|
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,
|
/* Send a fully-built HTTP response. If `body` starts with the envelope tag,
|
||||||
* unpack status/headers/body. Otherwise emit the historical 200-OK with
|
* unpack status/headers/body. Otherwise emit the historical 200-OK with
|
||||||
* auto-detected Content-Type. */
|
* auto-detected Content-Type. */
|
||||||
@@ -1283,6 +1347,19 @@ static void http_send_response(int fd, const char* body) {
|
|||||||
&env_headers_map, &env_body,
|
&env_headers_map, &env_body,
|
||||||
&env_parsed_root);
|
&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;
|
const char* eff_body = is_envelope ? env_body : body;
|
||||||
/* Use the real byte count from fs_read if available (handles binary files
|
/* 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
|
* 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 ────────────────────────────────────────────────────────────────── */
|
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
|
/* True iff the segment is non-empty and every byte is an ASCII digit. We treat
|
||||||
const char* json = EL_CSTR(jsonv);
|
* such segments as numeric array indices when walking a dot-path; mixed names
|
||||||
const char* key = EL_CSTR(keyv);
|
* like "0a" remain object-key lookups, so a key named "0" still wins over an
|
||||||
if (!json || !key) return el_wrap_str(el_strdup(""));
|
* index when the surrounding container is an object. */
|
||||||
size_t klen = strlen(key);
|
static int json_path_seg_is_index(const char* seg, size_t n) {
|
||||||
/* Use a stack buffer for the pattern to avoid arena double-free.
|
if (n == 0) return 0;
|
||||||
* Keys in El maps are typically short; 512 bytes is a safe upper bound. */
|
for (size_t i = 0; i < n; i++) {
|
||||||
char stack_pat[512];
|
if (seg[i] < '0' || seg[i] > '9') return 0;
|
||||||
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);
|
return 1;
|
||||||
const char* p = strstr(json, pattern);
|
}
|
||||||
if (pattern != stack_pat) free(pattern);
|
|
||||||
if (!p) return el_wrap_str(el_strdup(""));
|
/* Skip JSON whitespace. */
|
||||||
p += strlen(key) + 3; /* skip "key": */
|
static const char* json_skip_ws(const char* p) {
|
||||||
while (*p == ' ' || *p == '\t' || *p == '\n') 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 == '"') {
|
if (*p == '"') {
|
||||||
p++;
|
p++;
|
||||||
/* Unescape the JSON string value into a clean buffer. */
|
|
||||||
size_t cap = strlen(p) + 1;
|
size_t cap = strlen(p) + 1;
|
||||||
char* out = el_strbuf(cap);
|
char* out = el_strbuf(cap);
|
||||||
char* w = out;
|
char* w = out;
|
||||||
@@ -1940,15 +2068,70 @@ el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
|
|||||||
*w = '\0';
|
*w = '\0';
|
||||||
return el_wrap_str(out);
|
return el_wrap_str(out);
|
||||||
}
|
}
|
||||||
const char* start = p;
|
/* Object/array/number/bool/null — return the raw slice up to the value's
|
||||||
while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++;
|
* end. json_skip_value tracks brace/bracket/string state so nested objects
|
||||||
size_t len = (size_t)(p - start);
|
* round-trip cleanly. */
|
||||||
char* out = el_strbuf(len);
|
const char* end = json_skip_value(p);
|
||||||
memcpy(out, start, len);
|
if (!end) end = p;
|
||||||
out[len] = '\0';
|
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);
|
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 ──────────────────────────────────────────────── */
|
/* ── Float bit-cast helpers ──────────────────────────────────────────────── */
|
||||||
/* `el_to_float` and `el_from_float` are exposed in el_runtime.h as static
|
/* `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
|
* inlines so generated programs (which #include the header) can call them
|
||||||
|
|||||||
@@ -594,6 +594,16 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
|
|||||||
} else {
|
} else {
|
||||||
let out = out + "(void)(" + val_c + "); "
|
let out = out + "(void)(" + val_c + "); "
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if str_eq(sk, "Assign") {
|
||||||
|
// Real reassignment in an expression-position arm —
|
||||||
|
// emit the store; the arm's "value" stays whatever
|
||||||
|
// result_var was last set to, which is the El
|
||||||
|
// semantics (assignment is a statement, not a value).
|
||||||
|
let aname: String = s["name"]
|
||||||
|
let aval = s["value"]
|
||||||
|
let aval_c: String = cg_expr(aval)
|
||||||
|
let out = out + aname + " = " + aval_c + "; "
|
||||||
} else {
|
} else {
|
||||||
// Non-trivial stmt kinds (While/For) shouldn't appear in
|
// Non-trivial stmt kinds (While/For) shouldn't appear in
|
||||||
// expression-position arm bodies; emit nothing rather
|
// expression-position arm bodies; emit nothing rather
|
||||||
@@ -601,6 +611,7 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let i = i + 1
|
let i = i + 1
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -686,6 +697,20 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
|||||||
return declared
|
return declared
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
||||||
|
// (no `el_val_t` prefix) — by construction the parser only produces
|
||||||
|
// Assign for an existing identifier. If the name happens NOT to be in
|
||||||
|
// `declared` for the current C scope (it was let-bound by an enclosing
|
||||||
|
// block) the emit still resolves at C level because the variable lives
|
||||||
|
// in the surrounding scope.
|
||||||
|
if kind == "Assign" {
|
||||||
|
let name: String = stmt["name"]
|
||||||
|
let val = stmt["value"]
|
||||||
|
let val_c: String = cg_expr(val)
|
||||||
|
emit_line(indent + name + " = " + val_c + ";")
|
||||||
|
return declared
|
||||||
|
}
|
||||||
|
|
||||||
if kind == "Expr" {
|
if kind == "Expr" {
|
||||||
let val = stmt["value"]
|
let val = stmt["value"]
|
||||||
let val_kind: String = val["expr"]
|
let val_kind: String = val["expr"]
|
||||||
|
|||||||
@@ -946,6 +946,24 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|||||||
}, p)
|
}, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bare reassignment: `name = expr`. Handled BEFORE the expression
|
||||||
|
// fallback so we don't drop the assign on the floor and emit three
|
||||||
|
// orphan expressions (the original silent-miscompile bug). El's `let`
|
||||||
|
// already permits redeclaration, so this only applies when the parser
|
||||||
|
// sees an Ident followed directly by `=`. `==` is a separate kind
|
||||||
|
// (EqEq) so there's no ambiguity.
|
||||||
|
if k == "Ident" {
|
||||||
|
let k2 = tok_kind(tokens, pos + 1)
|
||||||
|
if k2 == "Eq" {
|
||||||
|
let name = tok_value(tokens, pos)
|
||||||
|
let p = pos + 2
|
||||||
|
let r = parse_expr(tokens, p)
|
||||||
|
let val = r["node"]
|
||||||
|
let p = r["pos"]
|
||||||
|
return make_result({ "stmt": "Assign", "name": name, "value": val }, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bare expression or if/match statement
|
// bare expression or if/match statement
|
||||||
let r = parse_expr(tokens, pos)
|
let r = parse_expr(tokens, pos)
|
||||||
let val = r["node"]
|
let val = r["node"]
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// http-status-envelope.el — acceptance test for the __status__ HTTP envelope.
|
||||||
|
//
|
||||||
|
// Before fix: a handler returning {"__status__":401,"error":"unauthorized"}
|
||||||
|
// went out as an HTTP 200 with the JSON body verbatim, so Cloud Run logs were
|
||||||
|
// full of 200s for what should have been 4xx/5xx.
|
||||||
|
//
|
||||||
|
// After fix: when the response body's FIRST key is __status__, the runtime
|
||||||
|
// reads the integer value as the HTTP status code and strips the marker from
|
||||||
|
// the body before sending it to the client.
|
||||||
|
//
|
||||||
|
// Verify with curl:
|
||||||
|
// curl -i http://localhost:8081/auth -> HTTP/1.1 401 Unauthorized
|
||||||
|
// curl -i http://localhost:8081/health -> HTTP/1.1 200 OK
|
||||||
|
// curl -i http://localhost:8081/oops -> HTTP/1.1 503 Service Unavailable
|
||||||
|
|
||||||
|
fn handle(method: String, path: String, body: String) -> String {
|
||||||
|
if path == "/auth" {
|
||||||
|
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
||||||
|
}
|
||||||
|
if path == "/oops" {
|
||||||
|
return "{\"__status__\":503,\"error\":\"degraded\"}"
|
||||||
|
}
|
||||||
|
if path == "/health" {
|
||||||
|
return "{\"ok\":true}"
|
||||||
|
}
|
||||||
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Int {
|
||||||
|
http_set_handler("handle")
|
||||||
|
http_serve(8081, "handle")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// json-array-traversal.el — acceptance test for json_get dot-path with array
|
||||||
|
// indices.
|
||||||
|
//
|
||||||
|
// Before fix: json_get("...", "0.field") would substring-search for a literal
|
||||||
|
// key named `"0.field"` and find nothing, returning "".
|
||||||
|
//
|
||||||
|
// After fix: dot-path segments that are all digits are treated as array
|
||||||
|
// indices and the walker descends into the array.
|
||||||
|
|
||||||
|
fn test_array_traversal() -> String {
|
||||||
|
let s: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]"
|
||||||
|
let a: String = json_get(s, "0.name")
|
||||||
|
let b: String = json_get(s, "1.name")
|
||||||
|
return a + "," + b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Int {
|
||||||
|
let r: String = test_array_traversal()
|
||||||
|
print(r)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// reassign-in-if.el — acceptance test for bare reassignment inside if-body.
|
||||||
|
//
|
||||||
|
// Before fix: parser dropped `x = "override"` on the floor and codegen emitted
|
||||||
|
// three orphan expressions (`x; EL_NULL; EL_STR("override");`). Effective store
|
||||||
|
// was lost, so the function returned "default".
|
||||||
|
//
|
||||||
|
// After fix: parse_stmt recognises `Ident "=" Expr` as an Assign statement and
|
||||||
|
// codegen emits a real C assignment, so the function returns "override".
|
||||||
|
|
||||||
|
fn test_reassign() -> String {
|
||||||
|
let x: String = "default"
|
||||||
|
if true {
|
||||||
|
x = "override"
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Int {
|
||||||
|
let r: String = test_reassign()
|
||||||
|
print(r)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user