feat(lang): L3 module scope - module, export, friend module

A barrel's 'module NAME' makes its directory a module; a declaration other modules
use says 'export'. Private use from another module is an error naming the module
and where to mark it; 'friend module' sees everything (a test harness); a file in
no module is public and a package keeps its own module. LUDIC_VIS_REPORT=1 lists
every violation instead of stopping, so a codebase can be given its exports first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 00:52:06 +03:00
parent 0c73287e35
commit 0677aee93f
20 changed files with 55913 additions and 54132 deletions

View file

@ -1409,6 +1409,7 @@ function emit_call(e: Node) -> Val {
if (fn2 == null) { perr(`unknown function {name}`) }
cname = rtname
}
vis_check(fn2, name)
reorder_named(e, param_labels(fn2))
# evaluate args first (their IR is emitted before the call instruction), coercing
# each to the parameter's declared type so an int passed for a `long` widens.
@ -1471,6 +1472,7 @@ function emit_expr(e: Node) -> Val {
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
let g = find_global(e.s)
if (g != null) {
vis_check(g, e.s)
# a const reference IS its initializer expression, carrying that
# expression's real type — so `const X: fixed = 10.0` yields a `fixed`, not
# the raw Q16.16 bits mislabelled `int`. Every existing const is an int

View file

@ -62,6 +62,7 @@ function fn_ty_ret(t: pointer) -> pointer {
function emit_fnref(e: Node) -> Val {
let d = find_fn(e.s)
if d == null { perr(`fn {e.s}: no function called {e.s}`) }
vis_check(d, e.s)
return val(`@fn_{e.s}`, fn_sig_of(d))
}
# a call through a value of a function type

View file

@ -26,6 +26,7 @@ function rec_field(rec: Node, fname: pointer) -> Node {
function emit_new_struct(name: pointer, rec: Node) -> Val {
let s = layout_node(name) # a struct or a property — same shape
if (s == null) { perr("unknown record type in new") }
vis_check(s, name)
let lty = layout_ty(name)
let sz = emit_sizeof(lty)
let obj = emit_bind(`call ptr @malloc(i64 {sz})`)

View file

@ -41,6 +41,7 @@ function emit_assign(st: Node) -> void {
else {
let g = find_global(t.s)
if (g == null) { perr(`assign to unknown {t.s}`) }
vis_check(g, t.s)
addr = `@g_{t.s}`; ty = g.ty
}
} else {
@ -291,6 +292,7 @@ function emit_match(st: Node) -> void {
function emit_emit(st: Node) -> Val {
let ev = find_event(st.s)
if (ev == null) { perr(`emit: unknown event {st.s}`) }
vis_check(ev, st.s)
# evaluate each payload field in declared order (default for a missing arg)
let fcodes = new []pointer
let ftys = new []pointer

View file

@ -0,0 +1,37 @@
# emit_vis.ludic — L3: module scope. A file belongs to the module its own `module NAME` line or
# its importer's names (frontend/parse.ludic, module_of); a file in no module is public. A
# reference from one module to another module's function, global, record or event needs that
# declaration `export`ed, unless the referring module is a `friend module` (a test harness).
# LUDIC_VIS_REPORT=1 prints every violation as `vis: <file>: <module>.<name>` and goes on, so a
# tool can add the exports a codebase needs before the rule is switched on for it.
var g_vis_report: int = -1
function vis_reporting() -> bool {
if g_vis_report < 0 {
g_vis_report = 0
if getenv("LUDIC_VIS_REPORT") != null { g_vis_report = 1 }
}
return g_vis_report == 1
}
function vis_allowed(d: Node, from: pointer) -> bool {
if d.vis == 1 { return true }
if d.file == null { return true }
let to = module_of(d.file)
if to == "" { return true }
if from == to { return true }
return module_is_friend(from)
}
# `what` is the name as written at the reference
function vis_check(d: Node, what: pointer) -> void {
if d == null { return }
var here = g_err_file
if here == null { return }
let from = module_of(here)
if vis_allowed(d, from) { return }
let to = module_of(d.file)
if vis_reporting() {
let m = `vis: {d.file}:{itoa(d.line)}: {to}.{what} used from {here}\n`
file_write(file_stderr(), m, len(m))
return
}
perr(`{what} is private to module {to}; mark it 'export' where it is declared ({d.file})`)
}

View file

@ -658,6 +658,8 @@ function emit_event_fns() -> void {
g_term = false
let next = lbl("evnext")
g_ret_label = next
g_err_file = g_onlisten[i].file
vis_check(ev, en)
emit_block(g_onlisten[i].a)
g_ret_label = "ret"
if not g_term { emit(` br label %{next}\n`) }