refactor(selfhost): reorganise into concern-based subdirectories
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s

Split the flat 38-file selfhost/ into concern-based subdirectories:

  frontend/        lex, parse, parse_game, ast
  support/         str, buf, io
  backend/         core IR + expression/statement lowering
  backend/game/    ECS/scene/event/world lowering
  backend/stdlib/  the namespaced Math.*/Text.*/Crypto.*/… intrinsics

and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:

  emit_game.ludic  -> + emit_world.ludic         (reflection world table,
                                                  tick helpers, @main synthesis)
  emit_expr.ludic  -> + emit_call.ludic          (namespaced builtins, call
                                                  lowering, expr dispatch)
  emit_text.ludic  -> + emit_text_prelude.ludic  (emitted string-builder runtime)

FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.

Closes #29

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 00:26:02 +03:00
parent f55216af50
commit 23726afa90
51 changed files with 780 additions and 771 deletions

View file

@ -153,7 +153,7 @@ compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed")
def parser_keywords():
kws = set()
for name in ("parse.ludic", "parse_game.ludic"):
text = open(os.path.join(ROOT, "selfhost", name), encoding="utf-8").read()
text = open(os.path.join(ROOT, "selfhost", "frontend", name), encoding="utf-8").read()
kws |= set(re.findall(r'is_id\("([a-z]+)"\)', text))
kws |= set(re.findall(r'streq\(t\.text,\s*"([a-z]+)"\)', text))
return kws
@ -161,7 +161,7 @@ def parser_keywords():
pkw = parser_keywords()
h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED")
if not pkw:
problems.append("could not extract any keywords from selfhost/parse*.ludic")
problems.append("could not extract any keywords from selfhost/frontend/parse*.ludic")
else:
for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)):
unparsed = (kws - pkw) - h_reserved

View file

@ -7,7 +7,7 @@ never written. So the set of things that MUST have a doc page is read straight
from the implementation, and every one is required to have a page (and every
namespace-method page is required to correspond to something real):
* namespace methods selfhost/emit_expr.ludic `emit_ns_call` + the
* namespace methods selfhost/backend/emit_call.ludic `emit_ns_call` + the
`is_<ns>_ns` predicates it delegates to (Math/Text/List)
* keywords tools/ludic-tools/ludic_syntax.h LUDIC_KW_* (minus
LUDIC_KW_RESERVED, which is not-yet-implemented)
@ -45,20 +45,23 @@ def fn_body(text, name):
def all_selfhost_source():
src = ""
d = os.path.join(ROOT, "selfhost")
for fn in sorted(os.listdir(d)):
if fn.endswith(".ludic"):
src += read("selfhost", fn) + "\n"
files = []
for dirpath, _dirs, names in os.walk(d):
for fn in names:
if fn.endswith(".ludic"):
files.append(os.path.join(dirpath, fn))
for path in sorted(files):
src += open(path, encoding="utf-8").read() + "\n"
return src
def compiler_ns_methods():
"""{Namespace: set(method)} the compiler actually dispatches on."""
expr = read("selfhost", "emit_expr.ludic")
body = fn_body(expr, "emit_ns_call")
if not body:
problems.append("emit_ns_call not found in selfhost/emit_expr.ludic")
return {}
allsrc = all_selfhost_source()
body = fn_body(allsrc, "emit_ns_call")
if not body:
problems.append("emit_ns_call not found in selfhost/ sources")
return {}
result = {}
# each `if (ns == "X")` opens a block that runs to the next such marker
chunks = re.split(r'if \(ns == "', body)

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Single source of truth for Ludic's named color palette.
# Emits: selfhost/emit_color.ludic (compiler lookup) and palette.json (docs).
# Emits: selfhost/backend/stdlib/emit_color.ludic (compiler lookup) and palette.json (docs).
import json, sys, os
# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries.
@ -293,6 +293,6 @@ if __name__ == "__main__":
names = guard_check()
root = os.path.dirname(os.path.abspath(__file__))
repo = "/Users/orkuncakilkaya/workspace/gpp"
emit_ludic(os.path.join(repo, "selfhost", "emit_color.ludic"))
emit_ludic(os.path.join(repo, "selfhost", "backend", "stdlib", "emit_color.ludic"))
emit_json(os.path.join(root, "palette.json"))
print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)")

View file

@ -1190,7 +1190,7 @@ program LudicLsp {
function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
# The namespaced standard-library surface: `Namespace.method`, mirroring the
# compiler's emit_ns_call (selfhost/emit_expr.ludic). Screen/Random/Input/Map
# compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map
# methods resolve to the underlying bare builtin's signature; Math/Text/List
# are pure fixed-point / string / slice ops with their own signatures. Returns
# the signature label for signature help, or null if it is not a known method.

View file

@ -12,49 +12,52 @@ function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") }
# the old scripts each carried their own copy.
function selfhost_frags() -> []pointer {
let f = new []pointer
push(f, "selfhost/str.ludic")
push(f, "selfhost/buf.ludic")
push(f, "selfhost/io.ludic")
push(f, "selfhost/ast.ludic")
push(f, "selfhost/lex.ludic")
push(f, "selfhost/parse.ludic")
push(f, "selfhost/parse_game.ludic")
push(f, "selfhost/emit_core.ludic")
push(f, "selfhost/emit_head.ludic")
push(f, "selfhost/emit_addr.ludic")
push(f, "selfhost/emit_intrin.ludic")
push(f, "selfhost/emit_intrin2.ludic")
push(f, "selfhost/emit_math.ludic")
push(f, "selfhost/emit_vector.ludic")
push(f, "selfhost/emit_text.ludic")
push(f, "selfhost/emit_hash.ludic")
push(f, "selfhost/emit_crypto.ludic")
push(f, "selfhost/emit_uuid.ludic")
push(f, "selfhost/emit_noise.ludic")
push(f, "selfhost/emit_log.ludic")
push(f, "selfhost/emit_os.ludic")
push(f, "selfhost/emit_unicode.ludic")
push(f, "selfhost/emit_fs.ludic")
push(f, "selfhost/emit_list.ludic")
push(f, "selfhost/emit_ease.ludic")
push(f, "selfhost/emit_collide.ludic")
push(f, "selfhost/emit_mem.ludic")
push(f, "selfhost/emit_time.ludic")
push(f, "selfhost/emit_datetime.ludic")
push(f, "selfhost/emit_colorfn.ludic")
push(f, "selfhost/emit_color.ludic")
push(f, "selfhost/emit_new.ludic")
push(f, "selfhost/emit_expr.ludic")
push(f, "selfhost/emit_stmt.ludic")
push(f, "selfhost/emit_ecs.ludic")
push(f, "selfhost/emit_query.ludic")
push(f, "selfhost/emit_spawn.ludic")
push(f, "selfhost/emit_game.ludic")
push(f, "selfhost/emit_machine.ludic")
push(f, "selfhost/emit_save.ludic")
push(f, "selfhost/emit_net.ludic")
push(f, "selfhost/emit_ui.ludic")
push(f, "selfhost/emit_decl.ludic")
push(f, "selfhost/support/str.ludic")
push(f, "selfhost/support/buf.ludic")
push(f, "selfhost/support/io.ludic")
push(f, "selfhost/frontend/ast.ludic")
push(f, "selfhost/frontend/lex.ludic")
push(f, "selfhost/frontend/parse.ludic")
push(f, "selfhost/frontend/parse_game.ludic")
push(f, "selfhost/backend/emit_core.ludic")
push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_addr.ludic")
push(f, "selfhost/backend/emit_intrin.ludic")
push(f, "selfhost/backend/emit_intrin2.ludic")
push(f, "selfhost/backend/stdlib/emit_math.ludic")
push(f, "selfhost/backend/stdlib/emit_vector.ludic")
push(f, "selfhost/backend/stdlib/emit_text.ludic")
push(f, "selfhost/backend/stdlib/emit_text_prelude.ludic")
push(f, "selfhost/backend/stdlib/emit_hash.ludic")
push(f, "selfhost/backend/stdlib/emit_crypto.ludic")
push(f, "selfhost/backend/stdlib/emit_uuid.ludic")
push(f, "selfhost/backend/stdlib/emit_noise.ludic")
push(f, "selfhost/backend/stdlib/emit_log.ludic")
push(f, "selfhost/backend/stdlib/emit_os.ludic")
push(f, "selfhost/backend/stdlib/emit_unicode.ludic")
push(f, "selfhost/backend/stdlib/emit_fs.ludic")
push(f, "selfhost/backend/stdlib/emit_list.ludic")
push(f, "selfhost/backend/stdlib/emit_ease.ludic")
push(f, "selfhost/backend/game/emit_collide.ludic")
push(f, "selfhost/backend/emit_mem.ludic")
push(f, "selfhost/backend/stdlib/emit_time.ludic")
push(f, "selfhost/backend/stdlib/emit_datetime.ludic")
push(f, "selfhost/backend/stdlib/emit_colorfn.ludic")
push(f, "selfhost/backend/stdlib/emit_color.ludic")
push(f, "selfhost/backend/emit_new.ludic")
push(f, "selfhost/backend/emit_expr.ludic")
push(f, "selfhost/backend/emit_call.ludic")
push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic")
push(f, "selfhost/backend/game/emit_spawn.ludic")
push(f, "selfhost/backend/game/emit_game.ludic")
push(f, "selfhost/backend/game/emit_world.ludic")
push(f, "selfhost/backend/game/emit_machine.ludic")
push(f, "selfhost/backend/game/emit_save.ludic")
push(f, "selfhost/backend/stdlib/emit_net.ludic")
push(f, "selfhost/backend/game/emit_ui.ludic")
push(f, "selfhost/backend/emit_decl.ludic")
push(f, "selfhost/main.ludic")
return f
}