Phase 3a: spawn/record initializers use ':' (Rule A)

Comp = { f = v }  ->  Comp { f: v }. record() requires ':' and parse_spawn()
drops the '=' before the record; '=' is now assignment/binding only. Records
appear only in games, so the compiler seed is unaffected.

Migration: tools/ludic-tools/migrate_records.c (spawn-context aware). Verified
every golden byte-identical, old '=' form rejected, reseeded, test.sh 14/14.
Docs updated (LANGUAGE.md, BOOTSTRAP.md R2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 15:58:00 +03:00
parent eac8f8f335
commit 39bd430ce8
12 changed files with 2499 additions and 2425 deletions

View file

@ -0,0 +1,59 @@
/* migrate_records.c — one-time migration for Rule A (named fields use ':').
* Records appear ONLY inside `spawn` blocks, so we track spawn context and, for
* every single '=' inside one:
* - '=' followed by '{' is a component init `Comp = { … }` -> delete it (`Comp { … }`)
* - otherwise is a field binding `field = value` -> rewrite to ':'
* A '=' at a real boundary is a pure re-spelling; the driver's IR-identity check
* (migrated program compiles to the same IR) is the proof.
* usage: migrate_records <file> -> migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: migrate_records <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "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);
char* rep = calloc(L.n, 1); /* '=' -> ':' */
char* del = calloc(L.n, 1); /* drop the '=' (component init) */
int sp_stack[1024]; int bp = 0; int spawn_active = 0; int pendingSpawn = 0;
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;
if (k == LT_KW && ltok_is(&L, i, "spawn")) pendingSpawn = 1;
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
char c0 = L.src[L.v[i].start];
if (op1 && c0 == '{'){
if (bp < 1024){ sp_stack[bp] = (pendingSpawn || spawn_active > 0) ? 1 : 0; if (sp_stack[bp]) spawn_active++; bp++; }
pendingSpawn = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (sp_stack[bp] && spawn_active > 0) spawn_active--; }
} else if (op1 && c0 == '='){
if (spawn_active > 0){
int nx = ltok_next_sig(&L, i);
if (nx >= 0 && L.v[nx].kind == LT_OP && ltok_len(&L.v[nx]) == 1 && L.src[L.v[nx].start] == '{') del[i] = 1;
else rep[i] = 1;
}
}
}
/* map flags onto byte positions so the rebuild can peek one char ahead */
char* repb = calloc(n + 1, 1); char* delb = calloc(n + 1, 1);
for (int i = 0; i < L.n; i++){ if (rep[i]) repb[L.v[i].start] = 1; if (del[i]) delb[L.v[i].start] = 1; }
for (int b = 0; b < n; ){
if (repb[b]){ putchar(':'); b++; continue; } /* '=' -> ':' */
if (delb[b]){ b++; if (b < n && src[b] == ' ') b++; continue; } /* drop '=' and one space */
if (src[b] == ' ' && b + 1 < n && repb[b + 1]){ b++; continue; } /* drop the space before ':' -> `field: v` */
putchar(src[b]); b++;
}
free(repb); free(delb);
lud_lex_free(&L); free(rep); free(del); free(src);
return 0;
}