ludic/tools/ludic-tools/migrate_separators.c
Orkuncakilkaya eac8f8f335 Phase 2: enforce statement separators (Rule B)
The parser now requires a newline or ';' between statements (block() in
selfhost/parse.ludic); two statements may no longer sit adjacent with only
spaces. Also fixed if-without-else swallowing its trailing separator.

Migration: tools/ludic-tools/migrate_separators.c inserts ';' at statement
boundaries corpus-wide (examples, runtime, 25 self-host fragments, ~1100
boundaries). Verified semantically identical — the migrated compiler compiles
itself to IR byte-identical to the pre-migration seed, and every golden game
renders identically. Reseeded to the strict compiler; C-free fixpoint holds;
test.sh 14/14.

Docs: Rule B documented in LANGUAGE.md; BOOTSTRAP.md R1 + stale fences updated;
check-docs green across all docs. Fixed a multi-line string literal in
emit_expr.ludic (byte-identical \n escape) that the C toolchain lexer mis-lexes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:41:29 +03:00

119 lines
5.4 KiB
C

/* sepfix.c — one-time migration for Rule B: insert `;` at statement boundaries
* that are currently spelled with only whitespace. Reuses the toolchain lexer.
*
* A boundary is inserted between two significant, same-line tokens prev,cur when
* - we are at paren/bracket depth 0 (not inside a call's args or an index), AND
* - we are NOT inside a `ui` block (widget props are `k=v`, space-separated,
* and the parser does not skip newlines between them), AND
* - prev can END an operand/statement, AND cur can START a statement.
* Inserting `;` (which the lexer maps to a newline token) at a REAL boundary is
* a semantic no-op under today's permissive parser — the IR-identity check in
* the driver is the proof. Wrong insertions change the IR and are rejected.
*
* usage: sepfix <in.ludic> -> writes migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
static const char* STMT_START[] = {
"let","return","if","when","while","for","spawn","despawn",
"match","machine","become","enter","break","continue", 0
};
static int ender(const LLex* L, int i){
const LTok* t = &L->v[i];
switch (t->kind){
case LT_ID: case LT_INT: case LT_FLOAT: case LT_STR: case LT_CHAR:
case LT_BOOL: case LT_TYPE: case LT_PHASE: return 1;
case LT_OP: {
if (ltok_len(t) != 1) return 0;
char c = L->src[t->start];
if (c == ']'){
/* `]` closing an empty `[]` is a slice-TYPE marker (`[]Node`),
* not an operand end — the type name that follows continues it. */
int p = ltok_prev_sig(L, i);
if (p >= 0 && L->v[p].kind == LT_OP && ltok_len(&L->v[p]) == 1 && L->src[L->v[p].start] == '[') return 0;
return 1;
}
return (c == ')' || c == '}'); }
case LT_KW: { char b[32]; ltok_text(L, i, b, sizeof b);
return !strcmp(b, "break") || !strcmp(b, "continue"); }
default: return 0;
}
}
static int starter(const LLex* L, int i){
const LTok* t = &L->v[i];
if (t->kind == LT_ID || t->kind == LT_INT || t->kind == LT_FLOAT ||
t->kind == LT_STR || t->kind == LT_CHAR || t->kind == LT_BOOL) return 1;
if (t->kind == LT_KW){ char b[32]; ltok_text(L, i, b, sizeof b); return lud_in(STMT_START, b); }
return 0; /* an operator never starts a statement here */
}
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: sepfix <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "sepfix: cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
/* insert[k] = 1 means: emit a ';' immediately after token k's bytes */
char* insert = calloc(L.n, 1);
int paren = 0; /* unclosed ( or [ */
/* ui suppression: brace depth stack, marking which open braces are ui/widget */
int ui_stack[512]; int bp = 0; int ui_active = 0;
int pendingUiOpen = 0; /* saw `ui` or a widget word; the next { is a ui brace */
int prev = -1; /* previous significant token index */
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind;
if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
char c0 = L.src[L.v[i].start];
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
/* track ui context by the `ui` KEYWORD only — it brackets the whole
* widget tree via its root brace, so every nested widget brace is
* already inside ui_active. (Do NOT key off widget words like col/row/
* image/label: those are also ordinary variable names, and matching them
* would wrongly suppress real statement boundaries.) */
if (k == LT_KW && ltok_is(&L, i, "ui")) pendingUiOpen = 1;
if (op1 && c0 == '{'){
if (bp < 512){ ui_stack[bp] = pendingUiOpen; bp++; if (pendingUiOpen) ui_active++; }
pendingUiOpen = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (ui_stack[bp]) { if (ui_active) ui_active--; } }
} else if (op1 && (c0 == '(' || c0 == '[')){ paren++; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
else if (op1 && (c0 == ')' || c0 == ']')){ if (paren > 0) paren--; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
/* candidate boundary between prev and this token */
if (prev >= 0 && paren == 0 && !ui_active &&
L.v[prev].line == L.v[i].line &&
ender(&L, prev) && starter(&L, i)){
insert[prev] = 1;
}
prev = i;
}
if (getenv("SEPFIX_DEBUG")) fprintf(stderr, "END STATE %s: paren=%d ui_active=%d bp=%d\n", argv[1], paren, ui_active, bp);
/* rebuild: copy bytes, dropping in ';' right after any token flagged */
for (int i = 0, t = 0; i < n; ){
/* find if a token ends exactly at i and is flagged */
putchar(src[i]);
i++;
/* emit ';' after the last byte of a flagged token */
for (; t < L.n; t++){
if (L.v[t].end == i && insert[t]){ putchar(';'); break; }
if (L.v[t].end > i) break;
}
}
lud_lex_free(&L); free(insert); free(src);
return 0;
}