feat(compiler): resolve the install root from the binary's location

The engine runtime and the bundled ludic.* packages belong to the toolchain,
not to the project, so the compiler has to know where the toolchain lives. It
derived that from the directory the binary sits in, which is `bin` — so an
in-repo build only found runtime/native/cocoa.ll when the caller set
LUDIC_HOME=., and an installed compiler had no way to find it at all.

ludic_home() derives it properly instead: $LUDIC_HOME when set, else the
binary's directory with a trailing `bin/` stripped, else a $PATH scan for
argv[0] (an install is invoked by bare name, which carries no directory). Both
layouts that exist then have the same shape — ~/.ludic/{bin,runtime,packages}
and a repo checkout — so the same rule serves both and LUDIC_HOME becomes an
override rather than a requirement.

`import "ludic.core/…"` also falls back to $LUDIC_HOME/packages, after the
project's own ludic_modules/, so a project that has not fetched its own copy
gets the packages that shipped with the toolchain instead of a symlink farm.

The `ludic` multi-call name is dropped from the compiler: that name now belongs
to the CLI, and --run is the flag it drives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 22:01:39 +03:00
parent 7e07573026
commit 005cc39394
4 changed files with 20115 additions and 19835 deletions

View file

@ -5,8 +5,8 @@
# no allocation, identical codegen to writing the hex by hand. Unknown names are # no allocation, identical codegen to writing the hex by hand. Unknown names are
# a compile error (color_lookup returns -1, which emit_expr reports). # a compile error (color_lookup returns -1, which emit_expr reports).
# #
# GENERATED by `x docs-palette` from the single source-of-truth palette table # GENERATED by `ludic dev docs-palette` from the single source-of-truth palette table
# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit. # in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.
# ============================================================================ # ============================================================================
function color_lookup(name: pointer) -> int { function color_lookup(name: pointer) -> int {

View file

@ -554,6 +554,51 @@ function join_path(dir: pointer, rel: pointer) -> pointer {
return dir + rel return dir + rel
} }
# The toolchain install root, with a trailing '/' ("" meaning the current
# directory). Everything the toolchain owns rather than the project — the engine
# runtime under runtime/native, the bundled ludic.* packages, the VERSION file,
# cocoa.ll — is resolved against it.
#
# $LUDIC_HOME wins when it is set. Otherwise it is derived from the compiler
# binary, whose directory is `bin` in both shapes that exist: an install
# (~/.ludic/bin/ludicc, root ~/.ludic) and a repo checkout (bin/ludicc, root the
# repo). So a directory named bin means the root is its parent; anything else is
# taken as the root itself, which is what a loose binary beside its runtime
# wants. Invoked through $PATH argv[0] carries no directory at all, so the PATH
# entries are searched for the binary first — an installed `ludicc mygame.ludic`
# has to find its own runtime with nothing in the environment.
function ludic_home() -> pointer {
let env = getenv("LUDIC_HOME")
if (env != null) { return ensure_slash(env) }
var d = dir_of(arg(0))
if (len(d) == 0) { d = path_lookup_dir(base_name(arg(0))) }
if (len(d) == 0) { return "" }
if (len(d) >= 4) and (d[len(d) - 4..len(d)] == "bin/") { return d[0..len(d) - 4] }
return d
}
# the first $PATH entry holding an openable file called `name`, with a trailing
# '/' — "" when $PATH is unset or nothing matches.
function path_lookup_dir(name: pointer) -> pointer {
let p = getenv("PATH")
if (p == null) { return "" }
let n = len(p)
var start = 0
var i = 0
while i <= n {
if (i == n) or (p[i] == ':') {
if i > start {
let dir = ensure_slash(p[start..i])
let f = file_open(dir + name, "rb")
if (f != null) { file_close(f); return dir }
}
start = i + 1
}
i += 1
}
return ""
}
var loaded_paths: []pointer var loaded_paths: []pointer
var cur_dir: pointer var cur_dir: pointer
var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the regex runtime var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the regex runtime
@ -816,7 +861,7 @@ function parse_test() -> Node {
# under the package module root — $LUDIC_MODULES, default "ludic_modules" — so a # under the package module root — $LUDIC_MODULES, default "ludic_modules" — so a
# fetched source package resolves by its import path, e.g. # fetched source package resolves by its import path, e.g.
# import "git.workshopsoft.io/user/pkg/foo.ludic" # import "git.workshopsoft.io/user/pkg/foo.ludic"
# materialised by `x get` at ludic_modules/git.workshopsoft.io/user/pkg/foo.ludic. # materialised by `ludic get` at ludic_modules/git.workshopsoft.io/user/pkg/foo.ludic.
# The fallback only fires when the local path does not exist, so every existing # The fallback only fires when the local path does not exist, so every existing
# import resolves exactly as before (the emitted IR is byte-identical). # import resolves exactly as before (the emitted IR is byte-identical).
# true when `rel` is an engine-runtime splice path ("runtime/..."), as opposed to # true when `rel` is an engine-runtime splice path ("runtime/..."), as opposed to
@ -868,8 +913,7 @@ function do_import(rel: pointer) -> void {
# place main.ludic finds cocoa.ll / audio.ll. This is tried before the package # place main.ludic finds cocoa.ll / audio.ll. This is tried before the package
# module root, so $LUDIC_MODULES / ludic_modules holds only third-party packages. # module root, so $LUDIC_MODULES / ludic_modules holds only third-party packages.
if (src == null) and (rel[0] != '/') and is_runtime_path(rel) { if (src == null) and (rel[0] != '/') and is_runtime_path(rel) {
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(arg(0)))) let hc = ludic_home() + rel
let hc = home + rel
let hs = read_file(hc) let hs = read_file(hc)
if (hs != null) { full = hc; src = hs } if (hs != null) { full = hc; src = hs }
} }
@ -878,6 +922,16 @@ function do_import(rel: pointer) -> void {
let s2 = read_file(cand) let s2 = read_file(cand)
if (s2 != null) { full = cand; src = s2 } if (s2 != null) { full = cand; src = s2 }
} }
# The ludic.* packages ship with the toolchain too, so an installed compiler
# resolves `import "ludic.core/components.ludic"` from $LUDIC_HOME/packages
# with no ludic_modules/ to set up. It is the last candidate on purpose: a
# project that fetched its own copy of a package (ludic_modules, above) keeps
# the version it pinned.
if (src == null) and (rel[0] != '/') {
let pc = ludic_home() + "packages/" + rel
let ps = read_file(pc)
if (ps != null) { full = pc; src = ps }
}
if already_loaded(full) { return } if already_loaded(full) { return }
push(loaded_paths, full) push(loaded_paths, full)
if (src == null) { perr(`cannot open import {full}`) } if (src == null) { perr(`cannot open import {full}`) }

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,12 @@
# ludicc app.ludic -o bin/app # native binary (windowed if a game) # ludicc app.ludic -o bin/app # native binary (windowed if a game)
# ludicc app.ludic -o bin/app --headless # force headless # ludicc app.ludic -o bin/app --headless # force headless
# ludicc app.ludic --emit-llvm -o app.ll # stop at the IR # ludicc app.ludic --emit-llvm -o app.ll # stop at the IR
# ludic app.ludic # compile to a temp binary and run it # ludicc app.ludic --run # compile to a temp binary and run it
# #
# The multi-call twist: when argv[0] is "ludic" it defaults to compile-and-run. # This is the compiler, not the command line a user of the language meets: that
# With no -o and no run, IR still goes to stdout — the contract `x reseed` and # is `ludic` (tools/ludic-cli), which drives this. With no -o and no --run, IR
# `x bootstrap` rely on, so the bootstrap is untouched. # still goes to stdout — the contract `ludic dev reseed` and `ludic dev
# bootstrap` rely on, so the bootstrap is untouched.
# basename: the part of a path after the last '/'. # basename: the part of a path after the last '/'.
function base_name(path: pointer) -> pointer { function base_name(path: pointer) -> pointer {
@ -59,11 +60,10 @@ function chomp(s: pointer) -> pointer {
} }
# print "ludic <version>" and exit. The version is read at runtime from the # print "ludic <version>" and exit. The version is read at runtime from the
# VERSION file next to LUDIC_HOME (the single source of truth a release bumps), # VERSION file at the install root (the single source of truth a release bumps),
# so a version change never requires reseeding the compiler. # so a version change never requires reseeding the compiler.
function show_version() -> void { function show_version() -> void {
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(arg(0)))) let v = read_file(join_path(ludic_home(), "VERSION"))
let v = read_file(join_path(home, "VERSION"))
if (v == null) { print("ludic (version unknown)") } if (v == null) { print("ludic (version unknown)") }
else { print(`ludic {chomp(v)}`) } else { print(`ludic {chomp(v)}`) }
exit(0) exit(0)
@ -79,9 +79,6 @@ entry {
var run = false # compile then execute the result var run = false # compile then execute the result
g_coverage = false # --coverage: instrument each statement with a per-line hit counter g_coverage = false # --coverage: instrument each statement with a per-line hit counter
# multi-call: invoked as `ludic` -> run mode by default
if (base_name(arg(0)) == "ludic") { run = true }
var ai = 1 var ai = 1
while ai < arg_count() { while ai < arg_count() {
let a = arg(ai) let a = arg(ai)
@ -127,7 +124,7 @@ entry {
# a game gets a window by default; a plain program stays headless. An explicit # a game gets a window by default; a plain program stays headless. An explicit
# flag always wins. The stdout-IR path (no target) also stays headless, which # flag always wins. The stdout-IR path (no target) also stays headless, which
# is what `x reseed` compiles the compiler itself with. # is what `ludic dev reseed` compiles the compiler itself with.
let has_target = run or (out != null) let has_target = run or (out != null)
if want == 1 { g_windowed = true } if want == 1 { g_windowed = true }
else if want == 2 { g_windowed = false } else if want == 2 { g_windowed = false }
@ -153,7 +150,7 @@ entry {
if not ir_flush(ll) { die("ludicc: cannot write IR\n") } if not ir_flush(ll) { die("ludicc: cannot write IR\n") }
let cc = getenv_or("LUDIC_CC", "clang") let cc = getenv_or("LUDIC_CC", "clang")
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(arg(0)))) let home = ludic_home()
# clang twice: assemble the IR, then link. A windowed build adds the macOS # clang twice: assemble the IR, then link. A windowed build adds the macOS
# platform layer and the Cocoa framework. # platform layer and the Cocoa framework.