Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
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>
This commit is contained in:
commit
985f9ad8f2
418 changed files with 39065 additions and 0 deletions
150
tools/check-vocabulary.py
Executable file
150
tools/check-vocabulary.py
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
#!/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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue