feat(lang): packages count under uses; a port of defaults may go unbound
A module that says uses must name every package module it reaches; a package with no module line is named for its directory (ludic_render3d) for this rule, so a mechanic reaching into the renderer is caught. Only the engine's runtime needs no naming. 'module x uses' with nothing after it reaches no other module. A port whose every member has a default answers with its defaults when unbound, and 'new' of a port says to bind it. ludic-dev test runs 'ludic test packages'. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8c1d54a96
commit
f4062010e9
15 changed files with 47465 additions and 46516 deletions
13
LANGUAGE.md
13
LANGUAGE.md
|
|
@ -131,8 +131,13 @@ fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): a
|
|||
|
||||
- A module with no `uses` clause keeps the rule before it - anything exported - so the rule can be
|
||||
switched on one module at a time. Two module lines for one module add their lists together.
|
||||
- A file in no module (the program's root, the runtime) is unaffected either way, and a package's
|
||||
module (found through `ludic_modules` or the toolchain, such as `ludic_ui`) is always usable.
|
||||
- A package's module counts like any other: a mechanic that says `uses ludic_base` and calls
|
||||
into `ludic_inventory`, `ludic_ui` or the renderer is refused. A package with no `module` line
|
||||
of its own (`ludic.render3d`) is named for its directory - `ludic_render3d` - for this rule; its
|
||||
names stay public to the export rule. Only the engine's own runtime - `Value`, `Json`,
|
||||
`Random` and the rest, which is in no module and no package - needs no naming.
|
||||
- A file in no module (the program's root) is unaffected either way.
|
||||
- `module ludic_base uses` with nothing after it is a module that reaches no other module at all.
|
||||
- A friend of a module (`friend module lab`, or `friend module lab of fishing`) is not held to its
|
||||
`uses` for that module.
|
||||
- The declared graph may not go round: `module a uses b` beside `module b uses a` is refused
|
||||
|
|
@ -176,7 +181,9 @@ app.ludic:9: error: port Clock has no member later
|
|||
```
|
||||
|
||||
and a member of the wrong type is a type error like any other (`field now of Clock_port wants a
|
||||
fn()->int and this is a fn(int)->float`). A port nobody uses may stay unbound.
|
||||
fn()->int and this is a fn(int)->float`). A port nobody uses may stay unbound, and so may a port
|
||||
whose every member has a default: unbound, it answers with its defaults - which is how a
|
||||
package's fallbacks ("unbound: this machine runs the world") are written.
|
||||
|
||||
### Types are checked before anything is emitted
|
||||
|
||||
|
|
|
|||
11
changes/uses-packages.md
Normal file
11
changes/uses-packages.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Packages count under `uses`, and the mechanic packages hold themselves to it.** A module that
|
||||
says `uses` must name every package module it reaches (`ludic_inventory`, `ludic_ui`); a package
|
||||
with no `module` line is named for its directory (`ludic_render3d`), so a mechanic reaching into
|
||||
the renderer is caught. Only the engine's own runtime needs no naming. `module x uses` with nothing
|
||||
after it reaches no other module. A port whose every member has a default may go unbound and
|
||||
answers with its defaults. ludic.base, ludic.clock, ludic.effects, ludic.inventory, ludic.wallet,
|
||||
ludic.weather and ludic.tracks say `uses ludic_base` (ludic.base: nothing), and their records of
|
||||
function values are ports - `clock_bind(w)`, `inv_bind(r)`, `weather_bind(w)` and `tracks_bind(w)`
|
||||
are gone; a game writes `bind ClockWorld { ... }` (and so on) instead.
|
||||
11
examples/rejected/port_new.ludic
Normal file
11
examples/rejected/port_new.ludic
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# L3: a port is not a record a program makes - it is filled once, by `bind`
|
||||
program PortNew {
|
||||
port Clock { now: fn() -> int }
|
||||
function hours() -> int {
|
||||
return 3
|
||||
}
|
||||
entry {
|
||||
let c = new Clock
|
||||
print(0)
|
||||
}
|
||||
}
|
||||
7
examples/rejected/uses_package.ludic
Normal file
7
examples/rejected/uses_package.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# L3: a package's module counts under `uses` like any other - ludic_inventory is not named
|
||||
import "uses_pkg_mech"
|
||||
program UsesPackage {
|
||||
entry {
|
||||
print(stock())
|
||||
}
|
||||
}
|
||||
8
examples/rejected/uses_pkg_mech/index.ludic
Normal file
8
examples/rejected/uses_pkg_mech/index.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# uses_pkg_mech/index.ludic - a mechanic that says it uses ludic_base, then reaches into another
|
||||
# mechanic package
|
||||
module mech uses ludic_base
|
||||
import "ludic.base"
|
||||
import "ludic.inventory"
|
||||
export function stock() -> int {
|
||||
return inv_count(0)
|
||||
}
|
||||
7
examples/rejected/uses_r3d_mech/index.ludic
Normal file
7
examples/rejected/uses_r3d_mech/index.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# uses_r3d_mech/index.ludic - a mechanic that reaches into the renderer, which has no module line
|
||||
module mech uses ludic_base
|
||||
numbers float
|
||||
import "ludic.render3d/fmath.ludic"
|
||||
export function circle() -> float {
|
||||
return PI * 2.0
|
||||
}
|
||||
8
examples/rejected/uses_render3d.ludic
Normal file
8
examples/rejected/uses_render3d.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# L3: a package with no module line is still named under `uses` - ludic_render3d is not
|
||||
import "uses_r3d_mech"
|
||||
program UsesRender3d {
|
||||
numbers float
|
||||
entry {
|
||||
print(circle())
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,8 @@ function rec_field(rec: Node, fname: pointer) -> Node {
|
|||
# default. `rec` is the E_REC of overrides (or null for the bare `new T`).
|
||||
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") }
|
||||
if (s == null) and port_find(name) >= 0 { perr(`{name} is a port: it is filled once with 'bind {name} {{ ... }}' where the program is put together, not made with new`) }
|
||||
if (s == null) { perr(`unknown record type {name} in new`) }
|
||||
vis_check(s, name)
|
||||
let lty = layout_ty(name)
|
||||
let sz = emit_sizeof(lty)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# 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, and - when the referring module says `uses` - that module named there
|
||||
# (frontend/modules.ludic), unless the referring module is a friend of it (a test harness).
|
||||
# declaration `export`ed, and - when the referring module says `uses` - that module (or, for a
|
||||
# package with no module line, the package) named there (frontend/modules.ludic), unless the
|
||||
# referring module is a friend of it (a test harness). The engine's runtime needs no naming.
|
||||
# LUDIC_VIS_REPORT=1 prints every violation as `vis: <file>: <module>.<name>` (a private name) or
|
||||
# `uses: <file>: ...` (a module not in the uses list) and goes on, so a tool can add the exports
|
||||
# and the uses a codebase needs before the rule is switched on for it.
|
||||
|
|
@ -37,16 +38,17 @@ function vis_check(d: Node, what0: pointer) -> void {
|
|||
var here = g_err_file
|
||||
if here == null { return }
|
||||
let from = module_of(here)
|
||||
let uto = module_for_uses(d.file)
|
||||
if not (uto == "") and not (from == uto) and not module_friend_of(from, uto) and not module_may_use(from, uto) {
|
||||
if vis_reporting() {
|
||||
vis_say(`uses: {d.file}:{itoa(d.line)}: {uto}.{what} used from {here} (module {from})\n`)
|
||||
} else {
|
||||
perr(`{from} uses {uto}.{what} ({d.file}:{itoa(d.line)}): add 'uses {uto}' to {from}'s module line, or take it through a port`)
|
||||
}
|
||||
}
|
||||
let to = module_of(d.file)
|
||||
if (to == "") or (from == to) { return }
|
||||
if module_friend_of(from, to) { return }
|
||||
if not module_may_use(from, to) {
|
||||
if vis_reporting() {
|
||||
vis_say(`uses: {d.file}:{itoa(d.line)}: {to}.{what} used from {here} (module {from})\n`)
|
||||
} else {
|
||||
perr(`{from} uses {to}.{what} ({d.file}:{itoa(d.line)}): add 'uses {to}' to {from}'s module line, or take it through a port`)
|
||||
}
|
||||
}
|
||||
if d.vis == 1 { return }
|
||||
if vis_reporting() {
|
||||
vis_say(`vis: {d.file}:{itoa(d.line)}: {to}.{what} used from {here}\n`)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
# fishing may reach at all: a reference from fishing into a module it does not name is refused
|
||||
# (backend/emit_vis.ludic), exported or not, unless fishing is a friend of that module. A module
|
||||
# that says no `uses` keeps the old rule (anything exported), so the rule comes in one module at a
|
||||
# time. A package's module (found through ludic_modules or the toolchain) is always usable, and
|
||||
# the declared graph may not have a cycle.
|
||||
# time. A package's module counts like any other (ludic_base, ludic_ui, ludic_render3d must be
|
||||
# named); only the engine's own runtime, which is in no module, needs no naming. The declared
|
||||
# graph may not have a cycle.
|
||||
#
|
||||
# `friend module lab` sees every module's private names; `friend module lab of fishing, data`
|
||||
# sees only those modules'.
|
||||
|
|
@ -12,8 +13,6 @@ var g_mu_list: []pointer = new []pointer # ",a,b," - what it may use (union
|
|||
var g_mu_file: []pointer = new []pointer # where it said so first
|
||||
var g_mu_line: []int = new []int
|
||||
var g_fr_scope: []pointer = new []pointer # per g_mod_friends entry: "" for all, else ",a,b,"
|
||||
var g_pkg_files: []pointer = new []pointer # files that belong to a package, not the project
|
||||
var g_pkg_mods: []pointer = new []pointer # modules declared in a package's files
|
||||
|
||||
function mod_list_has(list: pointer, name: pointer) -> bool {
|
||||
return has_sub(list, `,{name},`)
|
||||
|
|
@ -41,11 +40,12 @@ function mod_parse_line() -> void {
|
|||
pi += 1
|
||||
let name = eat_id()
|
||||
module_set(g_parse_file, name)
|
||||
if is_pkg_file(g_parse_file) and not mod_is_pkg(name) { push(g_pkg_mods, name) }
|
||||
if not is_id("uses") { return }
|
||||
let ln = toks[pi].line
|
||||
pi += 1
|
||||
let list = mod_parse_names()
|
||||
# `uses` and nothing after it: a module that reaches no other module at all
|
||||
var list = ","
|
||||
if toks[pi].kind == TK_ID { list = mod_parse_names() }
|
||||
let k = mod_find_uses(name)
|
||||
if k >= 0 {
|
||||
g_mu_list[k] = g_mu_list[k] + list
|
||||
|
|
@ -80,26 +80,45 @@ function module_friend_of(from: pointer, to: pointer) -> bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
function is_pkg_file(f: pointer) -> bool {
|
||||
# the package each of a package's files came from, as a module name: `ludic.render3d/r3d.ludic` is
|
||||
# ludic_render3d. A package with no `module` line of its own is public to the export rule, but a
|
||||
# module that says `uses` still has to name it - a mechanic reaching into the renderer is caught.
|
||||
var g_pk_file: []pointer = new []pointer
|
||||
var g_pk_name: []pointer = new []pointer
|
||||
function pkg_name_of(rel: pointer) -> pointer {
|
||||
var n = 0
|
||||
while n < len(rel) and rel[n] != '/' { n += 1 }
|
||||
let first = rel[0 .. n]
|
||||
var out = ""
|
||||
var i = 0
|
||||
while i < len(g_pkg_files) {
|
||||
if (g_pkg_files[i] == f) { return true }
|
||||
while i < len(first) {
|
||||
let c = first[i]
|
||||
if c == '.' or c == '-' { out = out + "_" } else { out = out + first[i .. i + 1] }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
return out
|
||||
}
|
||||
function mod_is_pkg(m: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_pkg_mods) {
|
||||
if (g_pkg_mods[i] == m) { return true }
|
||||
i += 1
|
||||
function pkg_file_set(f: pointer, name: pointer) -> void {
|
||||
push(g_pk_file, f)
|
||||
push(g_pk_name, name)
|
||||
}
|
||||
return false
|
||||
function pkg_of_file(f: pointer) -> pointer {
|
||||
var i = len(g_pk_file) - 1
|
||||
while i >= 0 {
|
||||
if (g_pk_file[i] == f) { return g_pk_name[i] }
|
||||
i -= 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
# the module a declaration is in, for the uses rule: its own, else its package's
|
||||
function module_for_uses(f: pointer) -> pointer {
|
||||
let m = module_of(f)
|
||||
if not (m == "") { return m }
|
||||
return pkg_of_file(f)
|
||||
}
|
||||
# may module `from` reach into module `to` at all? (L3 uses; the export rule is separate)
|
||||
function module_may_use(from: pointer, to: pointer) -> bool {
|
||||
if (from == "") or (to == "") or (from == to) { return true }
|
||||
if mod_is_pkg(to) { return true }
|
||||
let k = mod_find_uses(from)
|
||||
if k < 0 { return true }
|
||||
return mod_list_has(g_mu_list[k], to)
|
||||
|
|
|
|||
|
|
@ -1158,8 +1158,9 @@ function do_import(rel: pointer) -> void {
|
|||
# ludic_modules is not beside its importer and keeps its own module (or none)
|
||||
let beside = full == join_path(cur_dir, rel)
|
||||
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { push(g_trusted_files, full) }
|
||||
if (not beside and not is_runtime_path(rel)) or (beside and is_pkg_file(g_parse_file)) { push(g_pkg_files, full) }
|
||||
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
|
||||
if not beside and not is_runtime_path(rel) { pkg_file_set(full, pkg_name_of(rel)) } # modules.ludic
|
||||
if beside and not is_runtime_path(rel) and not (pkg_of_file(g_parse_file) == "") { pkg_file_set(full, pkg_of_file(g_parse_file)) }
|
||||
if (src == null) { perr(`cannot open import {full}`) }
|
||||
# the audio runtime can arrive through atlas.ludic's own import or the Assets
|
||||
# splice, not only through an Audio.* call in the game; a windowed build must
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
# required) and a global `Clock` of that record, in the port's module and exported with it. The
|
||||
# one `bind` is that global's initializer. When the program is read (ports_finish) every bind
|
||||
# must name a port, give each required member, give no member twice-over, and see the port;
|
||||
# a port used anywhere must be bound (port_check_bound, at the use). The binding's values are
|
||||
# a port used anywhere must be bound (port_check_bound, at the use) - unless every member has a
|
||||
# default, when an unbound port answers with its defaults. The binding's values are
|
||||
# checked for visibility from the bind's file, here, so the port's module needs no `uses` of it.
|
||||
var g_pt_name: []pointer = new []pointer
|
||||
var g_pt_comp: []Node = new []Node
|
||||
|
|
@ -108,6 +109,22 @@ function port_bind_one(b: Node) -> void {
|
|||
nw.line = b.line
|
||||
v.a = nw
|
||||
}
|
||||
# a port whose every member has a default needs no bind: left alone it answers with its defaults
|
||||
function port_defaults_only(k: int) -> void {
|
||||
let c = g_pt_comp[k]
|
||||
var i = 0
|
||||
while i < len(c.kids) {
|
||||
if c.kids[i].a == null { return }
|
||||
i += 1
|
||||
}
|
||||
let v = g_pt_var[k]
|
||||
let nw = node(E_NEW)
|
||||
nw.s = c.s
|
||||
nw.file = v.file
|
||||
nw.line = v.line
|
||||
v.a = nw
|
||||
g_pt_bind[k] = nw
|
||||
}
|
||||
function ports_finish() -> void {
|
||||
let saved = g_parsing
|
||||
g_parsing = false
|
||||
|
|
@ -116,5 +133,10 @@ function ports_finish() -> void {
|
|||
port_bind_one(g_bd[i])
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(g_pt_name) {
|
||||
if g_pt_bind[i] == null { port_defaults_only(i) }
|
||||
i += 1
|
||||
}
|
||||
g_parsing = saved
|
||||
}
|
||||
|
|
|
|||
46901
selfhost/ludicc.seed.ll
46901
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -446,6 +446,13 @@ function expect_fp_fail_case() -> void {
|
|||
if got == `{src}:4: expect_near failed (got 1.5, want 2)` { ok(lbl) } else { bad2(lbl, `said [{got}]`) }
|
||||
}
|
||||
|
||||
# every package's own tests, the way a package author runs them: `ludic test packages`
|
||||
function packages_test_case() -> void {
|
||||
let lbl = "ludic test packages: every package's tests pass"
|
||||
if shq(`bin/ludic test packages > {tmp_dir()}/pkgs.out 2>&1`) { ok(lbl) }
|
||||
else { bad2(lbl, capture_line(`grep -m1 FAIL {tmp_dir()}/pkgs.out`)) }
|
||||
}
|
||||
|
||||
# `ludic test <dir>`: the test programs under a package, each test block in a process of its own
|
||||
# (a global one test changes is fresh in the next), and a failed expect named by file:line
|
||||
function test_dir_case() -> void {
|
||||
|
|
@ -784,7 +791,10 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/port_unbound", "port Clock is used here but never bound", "a port used and never bound is refused at the use")
|
||||
reject_case("rejected/port_missing", "bind Clock leaves out day, which has no default", "a bind gives every required member")
|
||||
reject_case("rejected/port_twice", "port Clock is bound twice (first at", "a port is bound once")
|
||||
reject_case("rejected/port_new", "Clock is a port: it is filled once with 'bind Clock", "a port is bound, not made with new")
|
||||
reject_case("rejected/port_private", "Clock is private to module clock", "whoever binds a port has to see it")
|
||||
reject_case("rejected/uses_package", "mech uses ludic_inventory.inv_count", "a package's module counts under `uses` like any other")
|
||||
reject_case("rejected/uses_render3d", "mech uses ludic_render3d.PI", "a package with no module line is still named under `uses`")
|
||||
vis_report_case("rejected/uses_missing", "uses: examples/rejected/uses_valley/items/index.ludic:3: items.inv_add used from", "LUDIC_VIS_REPORT=1 lists a uses violation and builds")
|
||||
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
|
||||
reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused")
|
||||
|
|
@ -998,6 +1008,7 @@ function cmd_dev_test() -> int {
|
|||
scaffold_names_case()
|
||||
package_scripts_case()
|
||||
test_dir_case()
|
||||
packages_test_case()
|
||||
pack_roundtrip_case()
|
||||
packignore_case()
|
||||
os_dirs_case()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue