lumbda/c/reader.c
russell@unturf.com f7352b51b0 rename: uncommonlisp -> lumbda throughout the repo
Historical internal name "uncommonlisp" retired in favor of the
public name "lumbda" ahead of lumbda.com going live. Scope of
this commit:

Source files renamed:
  uncommonlisp.py                     -> lumbda.py
  asm/uncommonlisp.s                  -> asm/lumbda.s
  c/uncommonlisp.h                    -> c/lumbda.h
  whitepaper/uncommonlisp-whitepaper  -> whitepaper/lumbda-whitepaper (.rst + .pdf)

Binaries renamed (tracked ones; c/ was always gitignored):
  asm/uncommonlisp, asm/uncommonlisp-gc, asm/uncommonlisp.o,
  asm/uncommonlisp-gc.o                -> asm/lumbda(-gc)(.o)
  c/.gitignore                          -> ignores lumbda

Internal string updates (sed pass ordered longest-first):
  asm/uncommonlisp -> asm/lumbda
  c/uncommonlisp   -> c/lumbda
  uncommonlisp.py  -> lumbda.py
  UNCOMMONLISP_BIN -> LUMBDA_BIN (asm/test.sh env var)
  "uncommonlisp> " -> "lumbda> " (asm REPL prompt baked into binary)
  UNCOMMONLISP     -> LUMBDA (macros, comments)
  uncommonlisp     -> lumbda (prose)

Binary portal magic updated:
  "ULPORTAL" -> "LUMBDAB1"   # "Lumbda Binary v1"
Old portal files are not backward-compatible — this is a deliberate
break since it's the rename moment. S-expression portals already
carry their own ";; lumbda-portal v1" header and remain cleanly
versioned.

WHITEPAPER.pdf / WHITEPAPER.rst symlinks repointed to the renamed
files. Makefile's whitepaper target targets lumbda-whitepaper.pdf.

Not changed (intentional, separate phases):
  - Filesystem directory /home/fox/git/uncommonlisp itself
    (fox renames locally and the gitlab repo URL in a follow-up)
  - tests.py hardcoded cwd=/home/fox/git/uncommonlisp
    (matches the current on-disk location; will flip when the
    directory rename ships)
  - Git history (immutable; old commits still say uncommonlisp,
    which is correct — that's what they were)

Verified:
  137 asm no-GC + 137 asm GC + 571 Python + 83 C + 189 shared
  functional tests all pass under the new names.
  bench-gc-http (2000 req): all 4 cells behave as expected
  (cells 1/2 flat, 3 leaks, 4 bounded at 1 chunk).
  Python REPL, C REPL, asm REPL all start cleanly.
2026-04-19 10:20:11 -04:00

393 lines
13 KiB
C

/*
* reader.c — Tokenizer + parser for Scheme source
*/
#include "lumbda.h"
/* ═══════════════════════════════════════════════════════════════════════════
* Tokenizer
* ═══════════════════════════════════════════════════════════════════════════ */
static void tl_init(TokenList *tl) {
tl->cap = 256;
tl->count = 0;
tl->tokens = (char **)ul_malloc(sizeof(char *) * tl->cap);
tl->lines = (int *)ul_malloc(sizeof(int) * tl->cap);
}
static void tl_push(TokenList *tl, const char *tok, int len, int line) {
if (tl->count >= tl->cap) {
tl->cap *= 2;
tl->tokens = (char **)ul_realloc(tl->tokens, sizeof(char *) * tl->cap);
tl->lines = (int *)ul_realloc(tl->lines, sizeof(int) * tl->cap);
}
char *copy = (char *)ul_malloc(len + 1);
memcpy(copy, tok, len);
copy[len] = '\0';
tl->tokens[tl->count] = copy;
tl->lines[tl->count] = line;
tl->count++;
}
static int is_delimiter(int c) {
return c == '\0' || c == '(' || c == ')' || c == '"' || c == '\'' ||
c == '`' || c == ',' || c == ';' || isspace(c);
}
void tokenize(const char *src, TokenList *out, bool track_lines) {
tl_init(out);
const char *p = src;
int line = 1;
while (*p) {
/* Skip whitespace */
while (*p && isspace(*p)) {
if (*p == '\n') line++;
p++;
}
if (!*p) break;
/* Line comment */
if (*p == ';') {
while (*p && *p != '\n') p++;
continue;
}
/* Block comment #| ... |# */
if (*p == '#' && *(p+1) == '|') {
p += 2;
int depth = 1;
while (*p && depth > 0) {
if (*p == '#' && *(p+1) == '|') { depth++; p += 2; }
else if (*p == '|' && *(p+1) == '#') { depth--; p += 2; }
else { if (*p == '\n') line++; p++; }
}
continue;
}
/* Datum comment #; */
if (*p == '#' && *(p+1) == ';') {
p += 2;
/* Skip whitespace before datum */
while (*p && isspace(*p)) { if (*p == '\n') line++; p++; }
/* We need to skip exactly one datum — use a recursive approach */
/* For now, count balanced parens */
if (*p == '(') {
int depth = 0;
do {
if (*p == '(') depth++;
else if (*p == ')') depth--;
if (*p == '"') { p++; while (*p && (*p != '"' || *(p-1) == '\\')) p++; if (*p) p++; continue; }
if (*p == '\n') line++;
p++;
} while (*p && depth > 0);
} else if (*p == '"') {
p++; while (*p && (*p != '"' || *(p-1) == '\\')) p++; if (*p) p++;
} else {
while (*p && !is_delimiter(*p)) p++;
}
continue;
}
int tok_line = line;
/* String literal */
if (*p == '"') {
const char *start = p;
p++; /* skip opening " */
while (*p && *p != '"') {
if (*p == '\\' && *(p+1)) { p += 2; continue; }
if (*p == '\n') line++;
p++;
}
if (*p == '"') p++; /* skip closing " */
tl_push(out, start, (int)(p - start), tok_line);
continue;
}
/* Single-char tokens */
if (*p == '(' || *p == ')') {
tl_push(out, p, 1, tok_line);
p++;
continue;
}
if (*p == '\'') { tl_push(out, p, 1, tok_line); p++; continue; }
if (*p == '`') { tl_push(out, p, 1, tok_line); p++; continue; }
/* ,@ (unquote-splicing) */
if (*p == ',' && *(p+1) == '@') {
tl_push(out, p, 2, tok_line);
p += 2;
continue;
}
if (*p == ',') { tl_push(out, p, 1, tok_line); p++; continue; }
/* #( vector literal */
if (*p == '#' && *(p+1) == '(') {
tl_push(out, p, 2, tok_line);
p += 2;
continue;
}
/* Boolean #t #f */
if (*p == '#' && (*(p+1) == 't' || *(p+1) == 'T' || *(p+1) == 'f' || *(p+1) == 'F')) {
if (is_delimiter(*(p+2))) {
tl_push(out, p, 2, tok_line);
p += 2;
continue;
}
/* #true / #false */
if ((*(p+1) == 't' || *(p+1) == 'T') && strncmp(p, "#true", 5) == 0 && is_delimiter(*(p+5))) {
tl_push(out, "#t", 2, tok_line); p += 5; continue;
}
if ((*(p+1) == 'f' || *(p+1) == 'F') && strncmp(p, "#false", 6) == 0 && is_delimiter(*(p+6))) {
tl_push(out, "#f", 2, tok_line); p += 6; continue;
}
}
/* Character #\ */
if (*p == '#' && *(p+1) == '\\') {
const char *start = p;
p += 2;
/* Named chars */
const char *names[] = {"space", "newline", "tab", "return", "null", "escape", NULL};
bool found = false;
for (int i = 0; names[i]; i++) {
size_t nlen = strlen(names[i]);
if (strncasecmp(p, names[i], nlen) == 0 && is_delimiter(*(p + nlen))) {
p += nlen;
tl_push(out, start, (int)(p - start), tok_line);
found = true;
break;
}
}
if (!found) {
/* Single char */
if (*p) p++;
tl_push(out, start, (int)(p - start), tok_line);
}
continue;
}
/* Atom (number, symbol, etc.) */
const char *start = p;
while (*p && !is_delimiter(*p)) p++;
if (p > start) {
tl_push(out, start, (int)(p - start), tok_line);
}
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Parser
* ═══════════════════════════════════════════════════════════════════════════ */
static char *unescape_string(const char *s, size_t len) {
/* s includes quotes: "..." */
char *buf = (char *)ul_malloc(len + 1);
int j = 0;
for (size_t i = 1; i < len - 1; i++) {
if (s[i] == '\\' && i + 1 < len - 1) {
i++;
switch (s[i]) {
case 'n': buf[j++] = '\n'; break;
case 't': buf[j++] = '\t'; break;
case 'r': buf[j++] = '\r'; break;
case '"': buf[j++] = '"'; break;
case '\\': buf[j++] = '\\'; break;
default: buf[j++] = '\\'; buf[j++] = s[i]; break;
}
} else {
buf[j++] = s[i];
}
}
buf[j] = '\0';
return buf;
}
Value parse_atom(const char *tok) {
/* Booleans */
if (strcmp(tok, "#t") == 0 || strcmp(tok, "#T") == 0) return VAL_TRUE;
if (strcmp(tok, "#f") == 0 || strcmp(tok, "#F") == 0) return VAL_FALSE;
/* Character */
if (tok[0] == '#' && tok[1] == '\\') {
const char *name = tok + 2;
if (strcasecmp(name, "space") == 0) return VAL_CHAR(' ');
if (strcasecmp(name, "newline") == 0) return VAL_CHAR('\n');
if (strcasecmp(name, "tab") == 0) return VAL_CHAR('\t');
if (strcasecmp(name, "return") == 0) return VAL_CHAR('\r');
if (strcasecmp(name, "null") == 0) return VAL_CHAR('\0');
if (strcasecmp(name, "escape") == 0) return VAL_CHAR('\x1b');
return VAL_CHAR(name[0]);
}
/* String */
if (tok[0] == '"') {
size_t len = strlen(tok);
char *unescaped = unescape_string(tok, len);
Value v = make_string(unescaped, strlen(unescaped), false);
ul_free(unescaped);
return v;
}
/* Integer */
{
char *end;
errno = 0;
long long val = strtoll(tok, &end, 10);
if (*end == '\0' && errno == 0) {
/* Check if fits in 48-bit int */
if (val >= -(1LL << 47) && val < (1LL << 47))
return VAL_INT(val);
else
return make_double((double)val);
}
}
/* Float */
{
char *end;
errno = 0;
double val = strtod(tok, &end);
if (*end == '\0' && errno == 0) {
return make_double(val);
}
}
/* Special float literals */
if (strcmp(tok, "+inf.0") == 0) return make_double(INFINITY);
if (strcmp(tok, "-inf.0") == 0) return make_double(-INFINITY);
if (strcmp(tok, "+nan.0") == 0 || strcmp(tok, "-nan.0") == 0) return make_double(NAN);
/* Rational n/d */
{
const char *slash = strchr(tok, '/');
if (slash && slash != tok && *(slash+1) != '\0') {
/* Check if it's a valid rational: digits/digits */
bool valid = true;
const char *p = tok;
if (*p == '-') p++;
while (p < slash) { if (!isdigit(*p)) { valid = false; break; } p++; }
p = slash + 1;
if (*p == '-') p++;
while (*p) { if (!isdigit(*p)) { valid = false; break; } p++; }
if (valid) {
int64_t num = strtoll(tok, NULL, 10);
int64_t den = strtoll(slash + 1, NULL, 10);
if (den != 0) return rational_normalize(num, den);
}
}
}
/* Symbol */
return intern(tok);
}
Value parse_one(TokenList *tl, int *pos) {
if (*pos >= tl->count) lisp_error("unexpected EOF");
char *tok = tl->tokens[*pos];
int line = tl->lines[*pos];
(*pos)++;
/* Quote abbreviations */
if (strcmp(tok, "'") == 0) {
Value v = parse_one(tl, pos);
Pair *p = make_pair(SYM_QUOTE, cons(v, VAL_NIL));
p->line = line;
return VAL_PTR(p);
}
if (strcmp(tok, "`") == 0) {
Value v = parse_one(tl, pos);
Pair *p = make_pair(SYM_QUASIQUOTE, cons(v, VAL_NIL));
p->line = line;
return VAL_PTR(p);
}
if (strcmp(tok, ",") == 0) {
Value v = parse_one(tl, pos);
Pair *p = make_pair(SYM_UNQUOTE, cons(v, VAL_NIL));
p->line = line;
return VAL_PTR(p);
}
if (strcmp(tok, ",@") == 0) {
Value v = parse_one(tl, pos);
Pair *p = make_pair(SYM_UNQUOTE_SPLICING, cons(v, VAL_NIL));
p->line = line;
return VAL_PTR(p);
}
/* List */
if (strcmp(tok, "(") == 0) {
/* Collect items */
Value items[4096];
int count = 0;
Value tail = VAL_NIL;
bool has_dot = false;
while (1) {
if (*pos >= tl->count) lisp_error("unclosed (");
char *t = tl->tokens[*pos];
if (strcmp(t, ")") == 0) { (*pos)++; break; }
if (strcmp(t, ".") == 0) {
(*pos)++;
tail = parse_one(tl, pos);
has_dot = true;
if (*pos >= tl->count || strcmp(tl->tokens[*pos], ")") != 0)
lisp_error(". without )");
(*pos)++;
break;
}
if (count >= 4096) lisp_error("list too long");
items[count++] = parse_one(tl, pos);
}
Value r = has_dot ? tail : VAL_NIL;
for (int i = count - 1; i >= 0; i--) {
r = cons(items[i], r);
}
if (IS_PAIR(r)) AS_PAIR(r)->line = line;
return r;
}
/* Vector literal #( */
if (strcmp(tok, "#(") == 0) {
Value items[4096];
int count = 0;
while (1) {
if (*pos >= tl->count) lisp_error("unclosed #(");
if (strcmp(tl->tokens[*pos], ")") == 0) { (*pos)++; break; }
if (count >= 4096) lisp_error("vector too long");
items[count++] = parse_one(tl, pos);
}
return make_vector_from(items, count);
}
if (strcmp(tok, ")") == 0) lisp_error("unexpected )");
return parse_atom(tok);
}
Value *read_all(const char *src, int *count, bool track_lines) {
TokenList tl;
tokenize(src, &tl, track_lines);
int cap = 64;
Value *exprs = (Value *)ul_malloc(sizeof(Value) * cap);
*count = 0;
int pos = 0;
while (pos < tl.count) {
if (*count >= cap) {
cap *= 2;
exprs = (Value *)ul_realloc(exprs, sizeof(Value) * cap);
}
exprs[*count] = parse_one(&tl, &pos);
(*count)++;
}
/* Free token list */
for (int i = 0; i < tl.count; i++) ul_free(tl.tokens[i]);
ul_free(tl.tokens);
ul_free(tl.lines);
return exprs;
}