Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling, and docs. Phase 1 of the syntax-redesign cohesion pass has landed: edge-system fix, signature-query, when-alias, and the documentation truth-pass. Suite green (14/14), C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
6.2 KiB
Python
Executable file
150 lines
6.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Guard against the failure mode this whole layout exists to prevent.
|
|
|
|
The Ludic vocabulary — keywords, types, phases, builtins, intrinsics — is
|
|
written down in five places that cannot include each other:
|
|
|
|
compiler/**/*.c BUILTINS[] the language itself
|
|
compiler/**/*.c INTRINSICS[] the language itself
|
|
tools/ludic-tools/ ludic_syntax.h the toolchain's source of truth
|
|
tools/editors/shared/ the TextMate grammar (JSON, no includes)
|
|
tools/editors/jetbrains/ LudicVocabulary (Kotlin, no includes)
|
|
|
|
Adding a builtin to the compiler and forgetting the rest is silent: the editor
|
|
just stops colouring it, and nobody notices for months. So it is checked.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
problems = []
|
|
|
|
|
|
def read(*parts):
|
|
with open(os.path.join(ROOT, *parts)) as fh:
|
|
return fh.read()
|
|
|
|
|
|
def c_string_list(text, name):
|
|
"""Names in `static const char* NAME[] = { "a", "b", 0 };`"""
|
|
m = re.search(r"\b" + name + r"\s*\[\s*\]\s*=\s*\{(.*?)\}\s*;", text, re.S)
|
|
if not m:
|
|
problems.append(f"{name}: not found")
|
|
return set()
|
|
return set(re.findall(r'"([^"]+)"', m.group(1)))
|
|
|
|
|
|
def c_table_names(text, name):
|
|
"""First string of each row in a `{ "name", ... }` table."""
|
|
m = re.search(r"\b" + name + r"\s*\[\s*\]\s*=\s*\{(.*?)\n\s*\}\s*;", text, re.S)
|
|
if not m:
|
|
problems.append(f"{name}: not found")
|
|
return set()
|
|
return set(re.findall(r'\{\s*"([^"]+)"', m.group(1)))
|
|
|
|
|
|
def kotlin_set(text, name):
|
|
"""Names in `val NAME = setOf(...)`, found by matching the parentheses —
|
|
a non-greedy regex silently runs past a one-line setOf into the next one."""
|
|
m = re.search(r"\bval\s+" + name + r"\s*=\s*setOf\(", text)
|
|
if not m:
|
|
problems.append(f"Kotlin {name}: not found")
|
|
return set()
|
|
i = m.end()
|
|
depth = 1
|
|
while i < len(text) and depth:
|
|
if text[i] == "(":
|
|
depth += 1
|
|
elif text[i] == ")":
|
|
depth -= 1
|
|
i += 1
|
|
body = re.sub(r"//[^\n]*", "", text[m.end():i - 1])
|
|
return set(re.findall(r'"([^"]+)"', body))
|
|
|
|
|
|
def grammar_alternation(grammar, path, marker):
|
|
"""Names in a `\\b(a|b|c)\\b` match inside the TextMate grammar."""
|
|
node = grammar
|
|
for key in path:
|
|
node = node[key]
|
|
for pat in node:
|
|
rx = pat.get("match", "")
|
|
if marker in rx:
|
|
inner = re.search(r"\\b\((.*?)\)\\b", rx)
|
|
if inner:
|
|
return set(inner.group(1).split("|"))
|
|
problems.append(f"grammar: no rule containing {marker!r}")
|
|
return set()
|
|
|
|
|
|
def compare(label, reference, other, other_label, ignore=frozenset()):
|
|
missing = (reference - other) - ignore
|
|
extra = (other - reference) - ignore
|
|
if missing:
|
|
problems.append(f"{other_label} is missing {label}: {', '.join(sorted(missing))}")
|
|
if extra:
|
|
problems.append(f"{other_label} has unknown {label}: {', '.join(sorted(extra))}")
|
|
|
|
|
|
syntax_h = read("tools", "ludic-tools", "ludic_syntax.h")
|
|
def read_table_owner(table):
|
|
"""Find whichever compiler translation unit declares a table.
|
|
|
|
The compiler is split by concern and files move; hardcoding a path here
|
|
turns a routine refactor into a spurious vocabulary failure.
|
|
"""
|
|
import glob
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "compiler", "**", "*.c"), recursive=True)):
|
|
text = open(path, encoding="utf-8").read()
|
|
if re.search(r"\b%s\s*\[\s*\]" % re.escape(table), text):
|
|
return text
|
|
return None # the compiler is now written in Ludic (selfhost/); no C table
|
|
|
|
ludicc_c = read_table_owner("BUILTINS")
|
|
native_c = read_table_owner("INTRINSICS")
|
|
kotlin = read("tools", "editors", "jetbrains", "src", "main", "kotlin", "io", "ludic", "ide", "LudicTokens.kt")
|
|
grammar = json.loads(read("tools", "editors", "shared", "ludic.tmLanguage.json"))
|
|
|
|
# --- the toolchain header is the reference ---------------------------------
|
|
h_decl = c_string_list(syntax_h, "LUDIC_KW_DECL")
|
|
h_clause = c_string_list(syntax_h, "LUDIC_KW_CLAUSE")
|
|
h_stmt = c_string_list(syntax_h, "LUDIC_KW_STMT")
|
|
h_types = c_string_list(syntax_h, "LUDIC_TYPES")
|
|
h_phases = c_string_list(syntax_h, "LUDIC_PHASES")
|
|
h_widgets = c_string_list(syntax_h, "LUDIC_WIDGETS")
|
|
h_builtins = c_table_names(syntax_h, "LUDIC_BUILTINS")
|
|
h_intrinsics = c_table_names(syntax_h, "LUDIC_INTRINSICS")
|
|
|
|
# --- against the compiler's builtin/intrinsic tables, when a C compiler is
|
|
# present. The compiler is now written in Ludic (selfhost/), so this
|
|
# cross-check is skipped; the editor tools remain checked against each other.
|
|
if ludicc_c is not None:
|
|
compare("builtins", c_table_names(ludicc_c, "BUILTINS"), h_builtins, "ludic_syntax.h")
|
|
if native_c is not None:
|
|
compare("intrinsics", c_table_names(native_c, "INTRINSICS"), h_intrinsics, "ludic_syntax.h")
|
|
|
|
# --- against the JetBrains lexer -------------------------------------------
|
|
compare("declaration keywords", h_decl, kotlin_set(kotlin, "DECL"), "LudicTokens.kt")
|
|
compare("clause keywords", h_clause, kotlin_set(kotlin, "CLAUSE"), "LudicTokens.kt")
|
|
compare("statement keywords", h_stmt, kotlin_set(kotlin, "STMT"), "LudicTokens.kt")
|
|
compare("primitive types", h_types, kotlin_set(kotlin, "PRIMITIVES"), "LudicTokens.kt")
|
|
compare("phases", h_phases, kotlin_set(kotlin, "PHASES"), "LudicTokens.kt")
|
|
compare("widgets", h_widgets, kotlin_set(kotlin, "WIDGETS"), "LudicTokens.kt")
|
|
compare("builtins", h_builtins | h_intrinsics, kotlin_set(kotlin, "BUILTINS"), "LudicTokens.kt")
|
|
|
|
# --- against the TextMate grammar ------------------------------------------
|
|
gpath = ["repository", "builtin", "patterns"]
|
|
compare("builtins", h_builtins, grammar_alternation(grammar, gpath, "rng_chance"), "ludic.tmLanguage.json")
|
|
compare("intrinsics", h_intrinsics, grammar_alternation(grammar, gpath, "mem_alloc"), "ludic.tmLanguage.json")
|
|
kpath = ["repository", "keyword", "patterns"]
|
|
compare("phases", h_phases, grammar_alternation(grammar, kpath, "FixedUpdate"), "ludic.tmLanguage.json")
|
|
compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed"), "ludic.tmLanguage.json")
|
|
|
|
if problems:
|
|
print("vocabulary drift:", file=sys.stderr)
|
|
for p in problems:
|
|
print(" -", p, file=sys.stderr)
|
|
sys.exit(1)
|
|
sys.exit(0)
|