feat(lang): module NAME uses A, B - a module reaches only the modules it names
A reference from a module that declares uses into a module it does not name is refused, exported or not, naming the use and the fix. A module with no uses clause keeps the old rule; a package's module is always usable; a friend is not held to it; a cycle in the declared graph is refused; LUDIC_VIS_REPORT=1 lists the violations as uses: lines. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd6a449921
commit
f4533331e7
19 changed files with 53571 additions and 50826 deletions
29
LANGUAGE.md
29
LANGUAGE.md
|
|
@ -110,6 +110,35 @@ To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=
|
||||||
reference that would be refused is printed as `vis: <file>:<line>: <module>.<name> used from
|
reference that would be refused is printed as `vis: <file>:<line>: <module>.<name> used from
|
||||||
<file>` and the build goes on, so a script can add the `export`s the program already relies on.
|
<file>` and the build goes on, so a script can add the `export`s the program already relies on.
|
||||||
|
|
||||||
|
### What a module may reach (`uses`)
|
||||||
|
|
||||||
|
`export` says what a module offers; `uses` says what a module takes. A module line may name the
|
||||||
|
only other modules its files reach:
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
# doc-check: skip — a module spans files
|
||||||
|
# fishing/index.ludic
|
||||||
|
module fishing uses base, data
|
||||||
|
```
|
||||||
|
|
||||||
|
From then on a reference from `fishing` into any other module is refused, even to a name that
|
||||||
|
module exports:
|
||||||
|
|
||||||
|
```
|
||||||
|
fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): add 'uses items' to fishing's module line, or take it through a port
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 friend of a module (below) 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
|
||||||
|
(`the modules' uses go round in a circle: a -> b -> a`) - one of them takes the other through a
|
||||||
|
port instead.
|
||||||
|
- `LUDIC_VIS_REPORT=1` lists these too, as `uses: <file>:<line>: <module>.<name> used from <file>
|
||||||
|
(module <m>)`, and builds.
|
||||||
|
|
||||||
### Types are checked before anything is emitted
|
### Types are checked before anything is emitted
|
||||||
|
|
||||||
Between the parse and the emitter a checker walks every function, the entry, the tests, the
|
Between the parse and the emitter a checker walks every function, the entry, the tests, the
|
||||||
|
|
|
||||||
8
changes/module-uses.md
Normal file
8
changes/module-uses.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`module fishing uses base, data` - a module declares what it may reach, and the compiler holds it
|
||||||
|
to that.** A reference from a module that says `uses` into a module it does not name is refused,
|
||||||
|
exported or not, with the use and the fix in the message ("fishing uses items.inv_add (...): add
|
||||||
|
'uses items' to fishing's module line, or take it through a port"). A module with no `uses` keeps
|
||||||
|
the old rule, a package's module is always usable, a friend is not held to it, a cycle in the
|
||||||
|
declared graph is refused, and `LUDIC_VIS_REPORT=1` lists the violations as `uses:` lines.
|
||||||
12
examples/modules/uses.ludic
Normal file
12
examples/modules/uses.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# uses.ludic — L3: `module fishing uses base` names the only modules fishing may reach; the program
|
||||||
|
# itself is in no module, so it may call anything exported. A module with no `uses` line keeps the
|
||||||
|
# old rule, which is how the rule comes into a codebase one module at a time.
|
||||||
|
#
|
||||||
|
# Running it prints: 8 3
|
||||||
|
import "valley/base"
|
||||||
|
import "valley/fishing"
|
||||||
|
program Uses {
|
||||||
|
entry {
|
||||||
|
print(`{cast(3)} {bait()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/modules/valley/base/index.ludic
Normal file
5
examples/modules/valley/base/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# base/index.ludic - a module with no uses line keeps the old rule: its exports are anyone's
|
||||||
|
module base
|
||||||
|
export function bait() -> int {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
4
examples/modules/valley/fishing/catch.ludic
Normal file
4
examples/modules/valley/fishing/catch.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# fishing/catch.ludic - inherits `module fishing uses base` from its barrel
|
||||||
|
export function cast(times: int) -> int {
|
||||||
|
return times * bait() - 1
|
||||||
|
}
|
||||||
3
examples/modules/valley/fishing/index.ludic
Normal file
3
examples/modules/valley/fishing/index.ludic
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# fishing/index.ludic - fishing names the one module it may reach
|
||||||
|
module fishing uses base
|
||||||
|
import "catch.ludic"
|
||||||
8
examples/rejected/uses_cycle.ludic
Normal file
8
examples/rejected/uses_cycle.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# L3: two modules that each say they use the other are refused, whatever they call
|
||||||
|
import "uses_cycle/a"
|
||||||
|
import "uses_cycle/b"
|
||||||
|
program UsesCycle {
|
||||||
|
entry {
|
||||||
|
print(two())
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/rejected/uses_cycle/a/index.ludic
Normal file
5
examples/rejected/uses_cycle/a/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# a/index.ludic - a uses b
|
||||||
|
module a uses b
|
||||||
|
export function one() -> int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
5
examples/rejected/uses_cycle/b/index.ludic
Normal file
5
examples/rejected/uses_cycle/b/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# b/index.ludic - and b uses a: the declared graph goes round
|
||||||
|
module b uses a
|
||||||
|
export function two() -> int {
|
||||||
|
return one() + 1
|
||||||
|
}
|
||||||
8
examples/rejected/uses_missing.ludic
Normal file
8
examples/rejected/uses_missing.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# L3: fishing says `uses base` and calls items.inv_add - exported, but not a module fishing names
|
||||||
|
import "uses_valley/items"
|
||||||
|
import "uses_valley/fishing"
|
||||||
|
program UsesMissing {
|
||||||
|
entry {
|
||||||
|
print(land())
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/rejected/uses_valley/fishing/index.ludic
Normal file
5
examples/rejected/uses_valley/fishing/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# fishing/index.ludic - says it uses base, and then reaches into items
|
||||||
|
module fishing uses base
|
||||||
|
export function land() -> int {
|
||||||
|
return inv_add(1)
|
||||||
|
}
|
||||||
5
examples/rejected/uses_valley/items/index.ludic
Normal file
5
examples/rejected/uses_valley/items/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# items/index.ludic - exports inv_add, but that is not enough for a module that says `uses`
|
||||||
|
module items
|
||||||
|
export function inv_add(n: int) -> int {
|
||||||
|
return n + 1
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
# emit_vis.ludic — L3: module scope. A file belongs to the module its own `module NAME` line or
|
# 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
|
# 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
|
# 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).
|
# declaration `export`ed, and - when the referring module says `uses` - that module named there
|
||||||
# LUDIC_VIS_REPORT=1 prints every violation as `vis: <file>: <module>.<name>` and goes on, so a
|
# (frontend/modules.ludic), unless the referring module is a friend of it (a test harness).
|
||||||
# tool can add the exports a codebase needs before the rule is switched on for it.
|
# 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.
|
||||||
var g_vis_report: int = -1
|
var g_vis_report: int = -1
|
||||||
|
var g_vis_off: bool = false # a port's binding: checked where it is written (ports.ludic)
|
||||||
function vis_reporting() -> bool {
|
function vis_reporting() -> bool {
|
||||||
if g_vis_report < 0 {
|
if g_vis_report < 0 {
|
||||||
g_vis_report = 0
|
g_vis_report = 0
|
||||||
|
|
@ -12,14 +15,6 @@ function vis_reporting() -> bool {
|
||||||
}
|
}
|
||||||
return 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
|
# `what` is the name as written at the reference
|
||||||
# a generic function's instance is named for its type arguments (grow$int); a message names grow
|
# a generic function's instance is named for its type arguments (grow$int); a message names grow
|
||||||
function vis_plain(what: pointer) -> pointer {
|
function vis_plain(what: pointer) -> pointer {
|
||||||
|
|
@ -30,17 +25,30 @@ function vis_plain(what: pointer) -> pointer {
|
||||||
}
|
}
|
||||||
return what
|
return what
|
||||||
}
|
}
|
||||||
|
function vis_say(m: pointer) -> void {
|
||||||
|
file_write(file_stderr(), m, len(m))
|
||||||
|
}
|
||||||
function vis_check(d: Node, what0: pointer) -> void {
|
function vis_check(d: Node, what0: pointer) -> void {
|
||||||
if d == null { return }
|
if d == null { return }
|
||||||
|
if g_vis_off { return }
|
||||||
|
if d.file == null { return }
|
||||||
let what = vis_plain(what0)
|
let what = vis_plain(what0)
|
||||||
var here = g_err_file
|
var here = g_err_file
|
||||||
if here == null { return }
|
if here == null { return }
|
||||||
let from = module_of(here)
|
let from = module_of(here)
|
||||||
if vis_allowed(d, from) { return }
|
|
||||||
let to = module_of(d.file)
|
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() {
|
if vis_reporting() {
|
||||||
let m = `vis: {d.file}:{itoa(d.line)}: {to}.{what} used from {here}\n`
|
vis_say(`uses: {d.file}:{itoa(d.line)}: {to}.{what} used from {here} (module {from})\n`)
|
||||||
file_write(file_stderr(), m, len(m))
|
} 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`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
perr(`{what} is private to module {to}; mark it 'export' where it is declared ({d.file})`)
|
perr(`{what} is private to module {to}; mark it 'export' where it is declared ({d.file})`)
|
||||||
|
|
|
||||||
146
selfhost/frontend/modules.ludic
Normal file
146
selfhost/frontend/modules.ludic
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# modules.ludic — L3 the module graph. `module fishing uses base, data` says which other modules
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# `friend module lab` sees every module's private names; `friend module lab of fishing, data`
|
||||||
|
# sees only those modules'.
|
||||||
|
var g_mu_mod: []pointer = new []pointer # a module that said `uses`
|
||||||
|
var g_mu_list: []pointer = new []pointer # ",a,b," - what it may use (union of its lines)
|
||||||
|
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},`)
|
||||||
|
}
|
||||||
|
function mod_find_uses(m: pointer) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_mu_mod) {
|
||||||
|
if (g_mu_mod[i] == m) { return i }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
# `A, B, C` after `uses` / `of`, as ",A,B,C,"
|
||||||
|
function mod_parse_names() -> pointer {
|
||||||
|
var out = ","
|
||||||
|
out = out + eat_id() + ","
|
||||||
|
while is_op(",") {
|
||||||
|
pi += 1
|
||||||
|
out = out + eat_id() + ","
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
# `module NAME [uses A, B]`
|
||||||
|
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()
|
||||||
|
let k = mod_find_uses(name)
|
||||||
|
if k >= 0 {
|
||||||
|
g_mu_list[k] = g_mu_list[k] + list
|
||||||
|
return
|
||||||
|
}
|
||||||
|
push(g_mu_mod, name)
|
||||||
|
push(g_mu_list, list)
|
||||||
|
push(g_mu_file, g_parse_file)
|
||||||
|
push(g_mu_line, ln)
|
||||||
|
}
|
||||||
|
# `friend module NAME [of A, B]`
|
||||||
|
function mod_parse_friend() -> void {
|
||||||
|
pi += 2
|
||||||
|
let fm = eat_id()
|
||||||
|
module_set(g_parse_file, fm)
|
||||||
|
let scope = ""
|
||||||
|
push(g_mod_friends, fm)
|
||||||
|
push(g_fr_scope, scope)
|
||||||
|
}
|
||||||
|
# may `from` see `to`'s private names?
|
||||||
|
function module_friend_of(from: pointer, to: pointer) -> bool {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_mod_friends) {
|
||||||
|
if (g_mod_friends[i] == from) {
|
||||||
|
if (g_fr_scope[i] == "") or mod_list_has(g_fr_scope[i], to) { return true }
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
function is_pkg_file(f: pointer) -> bool {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_pkg_files) {
|
||||||
|
if (g_pkg_files[i] == f) { return true }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
# 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)
|
||||||
|
}
|
||||||
|
# the names in ",a,b," as a slice
|
||||||
|
function mod_names(list: pointer) -> []pointer {
|
||||||
|
let out = new []pointer
|
||||||
|
var a = 1
|
||||||
|
var j = 1
|
||||||
|
while j < len(list) {
|
||||||
|
if list[j] == ',' {
|
||||||
|
if j > a { push(out, list[a .. j]) }
|
||||||
|
a = j + 1
|
||||||
|
}
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
# the declared uses graph has no cycle: a depth-first walk that carries its path, "a -> b -> "
|
||||||
|
var g_mu_done: pointer = "," # modules whose every path is known to end
|
||||||
|
function mod_walk(m: pointer, path: pointer, first: pointer) -> void {
|
||||||
|
if has_sub(`-> {path}`, `-> {m} -> `) {
|
||||||
|
let k0 = mod_find_uses(first)
|
||||||
|
g_err_file = g_mu_file[k0]
|
||||||
|
g_err_line = g_mu_line[k0]
|
||||||
|
perr(`the modules' uses go round in a circle: {path}{m}; one of them has to take the other through a port`)
|
||||||
|
}
|
||||||
|
if has_sub(g_mu_done, `,{m},`) { return }
|
||||||
|
let k = mod_find_uses(m)
|
||||||
|
if k < 0 { return }
|
||||||
|
let next = mod_names(g_mu_list[k])
|
||||||
|
var n = 0
|
||||||
|
while n < len(next) {
|
||||||
|
mod_walk(next[n], `{path}{m} -> `, first)
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
g_mu_done = g_mu_done + m + ","
|
||||||
|
}
|
||||||
|
function modules_finish() -> void {
|
||||||
|
let saved = g_parsing
|
||||||
|
g_parsing = false
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_mu_mod) {
|
||||||
|
mod_walk(g_mu_mod[i], "", g_mu_mod[i])
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
g_parsing = saved
|
||||||
|
}
|
||||||
|
|
@ -47,14 +47,6 @@ function module_set(f: pointer, name: pointer) -> void {
|
||||||
push(g_mod_file, f)
|
push(g_mod_file, f)
|
||||||
push(g_mod_name, name)
|
push(g_mod_name, name)
|
||||||
}
|
}
|
||||||
function module_is_friend(name: pointer) -> bool {
|
|
||||||
var i = 0
|
|
||||||
while i < len(g_mod_friends) {
|
|
||||||
if (g_mod_friends[i] == name) { return true }
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
function is_float_file(f: pointer) -> bool {
|
function is_float_file(f: pointer) -> bool {
|
||||||
if (f == null) { return false }
|
if (f == null) { return false }
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -921,15 +913,11 @@ function parse_one_decl() -> void {
|
||||||
skipnl()
|
skipnl()
|
||||||
}
|
}
|
||||||
if is_id("friend") and (toks[pi + 1].text == "module") {
|
if is_id("friend") and (toks[pi + 1].text == "module") {
|
||||||
pi += 2
|
mod_parse_friend() # frontend/modules.ludic
|
||||||
let fm = eat_id()
|
|
||||||
module_set(g_parse_file, fm)
|
|
||||||
push(g_mod_friends, fm)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if is_id("module") and toks[pi + 1].kind == TK_ID {
|
if is_id("module") and toks[pi + 1].kind == TK_ID {
|
||||||
pi += 1
|
mod_parse_line() # module NAME [uses A, B]
|
||||||
module_set(g_parse_file, eat_id())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
# `export function f`, `export var v`, ...: visible from other modules (L3)
|
# `export function f`, `export var v`, ...: visible from other modules (L3)
|
||||||
|
|
@ -1158,6 +1146,7 @@ function do_import(rel: pointer) -> void {
|
||||||
# ludic_modules is not beside its importer and keeps its own module (or none)
|
# ludic_modules is not beside its importer and keeps its own module (or none)
|
||||||
let beside = full == join_path(cur_dir, rel)
|
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 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 beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
|
||||||
if (src == null) { perr(`cannot open import {full}`) }
|
if (src == null) { perr(`cannot open import {full}`) }
|
||||||
# the audio runtime can arrive through atlas.ludic's own import or the Assets
|
# the audio runtime can arrive through atlas.ludic's own import or the Assets
|
||||||
|
|
@ -1513,6 +1502,7 @@ function parse_program() -> void {
|
||||||
if is_op("}") { break }
|
if is_op("}") { break }
|
||||||
parse_one_decl()
|
parse_one_decl()
|
||||||
}
|
}
|
||||||
|
modules_finish() # L3: the declared uses have no cycle
|
||||||
registries_finish() # L8: each registry gets its defs
|
registries_finish() # L8: each registry gets its defs
|
||||||
views_finish() # L11: each view gets its model and call
|
views_finish() # L11: each view gets its model and call
|
||||||
components_finish() # L11: each component gets its class
|
components_finish() # L11: each component gets its class
|
||||||
|
|
|
||||||
52042
selfhost/ludicc.seed.ll
52042
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@ function selfhost_frags() -> []pointer {
|
||||||
push(f, "selfhost/frontend/parse_game.ludic")
|
push(f, "selfhost/frontend/parse_game.ludic")
|
||||||
push(f, "selfhost/frontend/generics.ludic")
|
push(f, "selfhost/frontend/generics.ludic")
|
||||||
push(f, "selfhost/frontend/aliases.ludic")
|
push(f, "selfhost/frontend/aliases.ludic")
|
||||||
|
push(f, "selfhost/frontend/modules.ludic")
|
||||||
push(f, "selfhost/frontend/registry.ludic")
|
push(f, "selfhost/frontend/registry.ludic")
|
||||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||||
push(f, "selfhost/frontend/resource.ludic")
|
push(f, "selfhost/frontend/resource.ludic")
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ function reject_case(path: pointer, want: pointer, label: pointer) -> void {
|
||||||
if s_contains(got, want) { ok(label) } else { bad2(label, `refused, but said [{got}]`) }
|
if s_contains(got, want) { ok(label) } else { bad2(label, `refused, but said [{got}]`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# LUDIC_VIS_REPORT=1: a program the module rules refuse builds, and says why on stderr
|
||||||
|
function vis_report_case(path: pointer, want: pointer, label: pointer) -> void {
|
||||||
|
let ll = `{tmp_dir()}/vr_{flat(path)}.ll`
|
||||||
|
if not shq(`LUDIC_VIS_REPORT=1 bin/ludicc examples/{path}.ludic --emit-llvm -o {ll} 2>{tmp_dir()}/vr.err`) {
|
||||||
|
bad2(label, capture_line(`grep -i error {tmp_dir()}/vr.err | head -1`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let got = capture(`cat {tmp_dir()}/vr.err`)
|
||||||
|
if s_contains(got, want) { ok(label) } else { bad2(label, `said [{s_trim(got)}]`) }
|
||||||
|
}
|
||||||
|
|
||||||
# a program the checker refuses for exactly `n` reasons, all of them reported in one run
|
# a program the checker refuses for exactly `n` reasons, all of them reported in one run
|
||||||
function reject_count(path: pointer, n: int, label: pointer) -> void {
|
function reject_count(path: pointer, n: int, label: pointer) -> void {
|
||||||
let ll = `{tmp_dir()}/rj_{flat(path)}.ll`
|
let ll = `{tmp_dir()}/rj_{flat(path)}.ll`
|
||||||
|
|
@ -724,6 +735,10 @@ function cmd_dev_test() -> int {
|
||||||
reject_case("rejected/fn_type_mismatch", "op wants a fn(int)->int and this is a fn(float)->float", "a function of the wrong type is refused")
|
reject_case("rejected/fn_type_mismatch", "op wants a fn(int)->int and this is a fn(float)->float", "a function of the wrong type is refused")
|
||||||
feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)")
|
feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)")
|
||||||
reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused")
|
reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused")
|
||||||
|
feat_case("modules/uses", "", "8 3", "uses.ludic (L3: `module fishing uses base` - a module reaches only the modules it names)")
|
||||||
|
reject_case("rejected/uses_missing", "fishing uses items.inv_add", "a module that says `uses` cannot reach a module it does not name, exported or not")
|
||||||
|
reject_case("rejected/uses_cycle", "go round in a circle: a -> b -> a", "a cycle in the declared uses is refused")
|
||||||
|
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)")
|
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")
|
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")
|
||||||
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
|
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue