From c442e01dbc14b051d0778e8085852165691d7105 Mon Sep 17 00:00:00 2001 From: "Josh Schairbaum (via Claude agent)" Date: Mon, 15 Jun 2026 01:34:04 -0500 Subject: [PATCH] fix(kernel): apply three upstream rxi/fe patches - fe_write() now detects and prints "..." for circular structures instead of looping forever / stack-overflowing (upstream PR #22). Relevant for the REPL where setcar/setcdr can produce circular lists. - Comment parser now terminates on \r as well as \n, fixing parse corruption with Windows-style CRLF cart source files (upstream PR #25). - fe_savegc() in fe_open() now precedes the t symbol allocation, closing a theoretical GC-during-init window (upstream PR #25). All 19 tests pass. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 17 +++++++++++++++++ kernel/fe.c | 30 +++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b28259..a030a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ is updated to key off the `:error` car, so the suite stays green. ### Fixed +- **`fe_write()` no longer crashes or loops infinitely on circular structures** + (upstream rxi/fe PR #22). A user at the REPL can construct a circular list + with `setcar`/`setcdr`; the old code followed the cycle forever, causing a + stack overflow or hang. The fix borrows `GCMARKBIT` during traversal to detect + cycles and prints `...` in their place, then immediately clears the marks. +- **Comments terminated by `\r` now parse correctly** (upstream rxi/fe PR #25 + partial). Source files with Windows-style `\r\n` line endings had their `\r` + swallowed into the next token after a `;` comment, corrupting the parse. The + comment-skip loop now stops on either `\n` or `\r`. +- **GC save in `fe_open()` now covers the `t` symbol** (upstream rxi/fe PR #25 + partial). `fe_savegc` was called after `fe_symbol(ctx, "t")`, so a GC cycle + triggered by that allocation could theoretically collect the freshly created + `t` object before it was stored in `ctx->t`. The save now precedes the + allocation. - **String host primitives no longer truncate at ~4 KB** (GWP-528). `string-length`, `string-ref`, `substring`, `string-append`/`str`, and `repr` copied through a fixed 4 KB C buffer, so any string past ~4095 bytes was @@ -94,6 +108,9 @@ First standalone release, split out from the KN-86 emulator. - `GCSTACKSIZE` is compile-time configurable (default 256). The desktop build raises it to 8192 so recursive code has headroom; hosts that vendor the kernel can keep 256. +- `fe_write()` is safe on circular structures (upstream PR #22). +- Comment parser terminates on `\r` as well as `\n` (upstream PR #25). +- `fe_savegc` in `fe_open()` precedes the `t` symbol allocation (upstream PR #25). ### Notes - `kec build` isn't a compiler — Fe is a tree-walking interpreter. It inlines diff --git a/kernel/fe.c b/kernel/fe.c index 2a4a336..16eea5b 100644 --- a/kernel/fe.c +++ b/kernel/fe.c @@ -354,7 +354,7 @@ static void writestr(fe_Context *ctx, fe_WriteFn fn, void *udata, const char *s) while (*s) { fn(ctx, udata, *s++); } } -void fe_write(fe_Context *ctx, fe_Object *obj, fe_WriteFn fn, void *udata, int qt) { +static void write_(fe_Context *ctx, fe_Object *obj, fe_WriteFn fn, void *udata, int qt) { char buf[32]; switch (type(obj)) { @@ -368,22 +368,26 @@ void fe_write(fe_Context *ctx, fe_Object *obj, fe_WriteFn fn, void *udata, int q break; case FE_TPAIR: + if (tag(obj) & GCMARKBIT) { writestr(ctx, fn, udata, "..."); break; } fn(ctx, udata, '('); for (;;) { - fe_write(ctx, car(obj), fn, udata, 1); + fe_Object *tmp = car(obj); + tag(obj) |= GCMARKBIT; + write_(ctx, tmp, fn, udata, 1); obj = cdr(obj); if (type(obj) != FE_TPAIR) { break; } fn(ctx, udata, ' '); + if (tag(obj) & GCMARKBIT) { writestr(ctx, fn, udata, "..."); break; } } - if (!isnil(obj)) { + if (!isnil(obj) && !(tag(obj) & GCMARKBIT)) { writestr(ctx, fn, udata, " . "); - fe_write(ctx, obj, fn, udata, 1); + write_(ctx, obj, fn, udata, 1); } fn(ctx, udata, ')'); break; case FE_TSYMBOL: - fe_write(ctx, car(cdr(obj)), fn, udata, 0); + write_(ctx, car(cdr(obj)), fn, udata, 0); break; case FE_TSTRING: @@ -406,6 +410,18 @@ void fe_write(fe_Context *ctx, fe_Object *obj, fe_WriteFn fn, void *udata, int q } } +static void unmarkpairs(fe_Object *obj) { + for (; !isnil(obj) && (tag(obj) & GCMARKBIT); obj = cdr(obj)) { + tag(obj) &= ~GCMARKBIT; + unmarkpairs(car(obj)); + } +} + +void fe_write(fe_Context *ctx, fe_Object *obj, fe_WriteFn fn, void *udata, int qt) { + write_(ctx, obj, fn, udata, qt); + unmarkpairs(obj); +} + static void writefp(fe_Context *ctx, void *udata, char chr) { unused(ctx); @@ -485,7 +501,7 @@ static fe_Object* read_(fe_Context *ctx, fe_ReadFn fn, void *udata) { return NULL; case ';': - while (chr && chr != '\n') { chr = fn(ctx, udata); } + while (chr && chr != '\n' && chr != '\r') { chr = fn(ctx, udata); } return read_(ctx, fn, udata); case ')': @@ -868,11 +884,11 @@ fe_Context* fe_open(void *ptr, int size) { } /* init objects */ + save = fe_savegc(ctx); ctx->t = fe_symbol(ctx, "t"); fe_set(ctx, ctx->t, ctx->t); /* register built in primitives */ - save = fe_savegc(ctx); for (i = 0; i < P_MAX; i++) { fe_Object *v = object(ctx); settype(v, FE_TPRIM);