fix(lang): the action drain is the program's; --check runs every check a build makes; a registry key named count is refused; name lookups are tables
- the function that calls every reducer (and the action queue) is written in the program's own file: in the first action's file it belonged to that module, depended on every module with a reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a ludic deps case hold it); the state instances, the queue and the reducers make no deps edges - ludicc --check / ludic build --check lower the program too and write nothing, so the code writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key - def R count is refused: its constant would be PREFIX_COUNT, the registry's size - a file's module, package, trust and numbers-float are tables, and from the check on the lookups of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2 minutes to under 10 s; duplicate declarations are found by table, not a pair of loops - threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all on the caller before a worker wakes (it failed one run in three under load) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2fdefa6040
commit
8cf4b3fed6
24 changed files with 67169 additions and 64398 deletions
|
|
@ -350,7 +350,7 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
|
|||
--windowed force a windowed (Cocoa) build
|
||||
--headless force a headless build (stdin input, out.ppm output)
|
||||
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
||||
--check parse and check only (types, modules, uses, layers, ports); write nothing
|
||||
--check every check a build makes (types, modules, uses, layers, ports, binds); write nothing
|
||||
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
||||
(the check-docs gate; canonical formatting not yet restored)
|
||||
--save-temps keep the intermediate .ll
|
||||
|
|
|
|||
12
LANGUAGE.md
12
LANGUAGE.md
|
|
@ -2025,7 +2025,7 @@ ludic test # compile and run the project's `test`
|
|||
ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line)
|
||||
ludic test -j 4 # four tests at once (default: one per CPU)
|
||||
ludic test packages/ludic.base # the test programs under a directory (a package's)
|
||||
ludic build --check # only check: types, modules, uses and ports - nothing emitted
|
||||
ludic build --check # every check a build makes, nothing written
|
||||
ludic deps # how tangled the modules are, as the compiler resolved them
|
||||
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
|
||||
|
||||
|
|
@ -2033,11 +2033,11 @@ ludicc app.ludic -o build/app # the compiler directly: a native binar
|
|||
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
|
||||
```
|
||||
|
||||
`ludic build [file] --check` (the compiler's `ludicc --check`) runs everything the compiler checks
|
||||
before it writes code - the parse, the types, `export`, `uses` and layers, ports and binds, registries
|
||||
- and stops: no IR, no link. On Maroon Lake it takes about three seconds where a build takes about a
|
||||
minute, for iterating on `uses` lines. Two checks belong to the code writer and only a build makes
|
||||
them: a function that can reach its end without its result, and a local declared twice in a block.
|
||||
`ludic build [file] --check` (the compiler's `ludicc --check`) runs every check a build makes - the
|
||||
parse, the types, `export`, `uses` and layers, ports and binds, registries, and the code writer's own
|
||||
(a function that can reach its end without its result, a bind to a function that is not there, an
|
||||
unknown name) - and writes nothing: no IR, no link. On Maroon Lake it takes about four seconds, for
|
||||
iterating on `uses` lines.
|
||||
|
||||
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
|
||||
reference its visibility pass resolves - from the module it is written in to the module of what it
|
||||
|
|
|
|||
10
changes/toolchain-r3-fixes.md
Normal file
10
changes/toolchain-r3-fixes.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**The action drain is the program's; `--check` is every check; `count` is not a registry key.**
|
||||
The function that calls every reducer is written in the program's own file, so the module that
|
||||
declares the actions no longer depends on every module that reduces them (a game's `base` joined a
|
||||
69-module cycle). `ludic build --check` runs the code writer's checks too - a bind to a function that
|
||||
is gone, an unknown name - and still writes nothing. A registry entry keyed `count` is refused
|
||||
(`def Tools count: its constant would be TL_COUNT, which is the registry's size`). And a big
|
||||
program builds much faster: the emitter's and the checker's lookups by name are tables, not walks of
|
||||
the whole program (on Maroon Lake a check-only build, lowering included, went from about 20 s to under 10 s, its IR from about 2 minutes to under 10 s).
|
||||
4
examples/actions/mods/acts/index.ludic
Normal file
4
examples/actions/mods/acts/index.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# acts/index.ludic - the actions, in a module that uses nothing: dispatching or declaring them makes
|
||||
# no module depend on the modules whose reducers handle them
|
||||
module acts uses
|
||||
export action PickUp { item: int }
|
||||
5
examples/actions/mods/bag/index.ludic
Normal file
5
examples/actions/mods/bag/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# bag/index.ludic - the bag's state and its reducer; it uses acts for the action's type
|
||||
module bag uses acts
|
||||
export state Bag { items: []int = new []int }
|
||||
reducer Bag on PickUp(b: mut Bag, a: PickUp) { push(b.items, a.item) }
|
||||
export function bag_count(b: Bag) -> int { return len(b.items) }
|
||||
12
examples/actions/modules.ludic
Normal file
12
examples/actions/modules.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# modules.ludic - 0.R: an action declared in one module and reduced in another; the drain that calls
|
||||
# every reducer is the program's own, so acts does not come to depend on bag (ludic deps: no cycle)
|
||||
import "mods/acts"
|
||||
import "mods/bag"
|
||||
program ActionModules {
|
||||
entry (b: Bag) {
|
||||
dispatch PickUp { item: 4 }
|
||||
dispatch PickUp { item: 5 }
|
||||
drain_actions()
|
||||
print(bag_count(b))
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,13 @@ program Threads {
|
|||
Sync.unlock(threads_st.lock)
|
||||
}
|
||||
|
||||
# remember which calls ran on a pool thread
|
||||
function placed(i: int, out: words) -> void { if Job.is_worker() { out[i] = 1 } else { out[i] = 0 } }
|
||||
# remember which calls ran on a pool thread - each with a little work in it, so on a busy machine
|
||||
# the calling thread cannot claim every index before a worker wakes
|
||||
function placed(i: int, out: words) -> void {
|
||||
var spin = 0
|
||||
for k in 0 .. 2000 { spin = spin + k % 7 }
|
||||
if Job.is_worker() { out[i] = 1 + spin * 0 } else { out[i] = spin * 0 }
|
||||
}
|
||||
|
||||
entry (threads_st: mut ThreadsState) {
|
||||
if Sync.cpu_count() >= 1 { print(1) }
|
||||
|
|
|
|||
6
examples/rejected/bind_missing_fn.ludic
Normal file
6
examples/rejected/bind_missing_fn.ludic
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# a port bound to a function that is not there: refused by a check-only build too
|
||||
program BindMissingFn {
|
||||
port Clock { now: fn() -> int }
|
||||
bind Clock { now: fn no_such_clock }
|
||||
entry { print(Clock.now()) }
|
||||
}
|
||||
8
examples/rejected/registry_count_key.ludic
Normal file
8
examples/rejected/registry_count_key.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# a def keyed `count` would be TL_COUNT, the registry's own size
|
||||
program RegistryCountKey {
|
||||
property Tool { key: string = "", n: int = 0 }
|
||||
registry Tools of Tool as TL
|
||||
def Tools count { n: 1 }
|
||||
def Tools axe { n: 2 }
|
||||
entry { print(TL_COUNT) }
|
||||
}
|
||||
4
examples/rejected/unknown_name.ludic
Normal file
4
examples/rejected/unknown_name.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# a name that is nothing: refused by a check-only build as by a build
|
||||
program UnknownName {
|
||||
entry { print(no_such_name + 1) }
|
||||
}
|
||||
|
|
@ -151,7 +151,94 @@ function find_arch(name: pointer) -> Node {
|
|||
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i += 1 }
|
||||
return null
|
||||
}
|
||||
# ---- the emitter's lookups by name: tables, once the program is whole ---------------------
|
||||
# find_fn / find_enum / find_comp / find_global / find_extern are asked for every name the emitter
|
||||
# lowers; walking prog for each was most of a big program's build. From emit_program on, each kind
|
||||
# is a table, kept up with what is appended to prog; a hit whose node has since been renamed falls
|
||||
# back to the walk.
|
||||
const IX_SLOTS: int = 131072
|
||||
var g_ix_on: bool = false
|
||||
var g_ix_tagged: []Node = new []Node
|
||||
var g_ix_upto: int = 0
|
||||
var g_ix_keys: [][]pointer = new [][]pointer # per kind: 0 fn, 1 enum, 2 comp, 3 global, 4 extern
|
||||
var g_ix_vals: [][]pointer = new [][]pointer
|
||||
function ix_start() -> void {
|
||||
g_ix_keys = new [][]pointer
|
||||
g_ix_vals = new [][]pointer
|
||||
var t = 0
|
||||
while t < 5 {
|
||||
let k = new []pointer
|
||||
let v = new []pointer
|
||||
var i = 0
|
||||
while i < IX_SLOTS {
|
||||
push(k, null)
|
||||
push(v, null)
|
||||
i += 1
|
||||
}
|
||||
push(g_ix_keys, k)
|
||||
push(g_ix_vals, v)
|
||||
t += 1
|
||||
}
|
||||
g_ix_upto = 0
|
||||
g_ix_tagged = new []Node
|
||||
g_ix_on = true
|
||||
}
|
||||
function ix_kind(d: Node) -> int {
|
||||
if d.kind == N_FN { return 0 }
|
||||
if d.kind == N_ENUM { return 1 }
|
||||
if d.kind == N_COMP { return 2 }
|
||||
if d.kind == N_VAR or d.kind == N_CONST { return 3 }
|
||||
if d.kind == N_EXTERN { return 4 }
|
||||
return -1
|
||||
}
|
||||
function ix_slot(t: int, name: pointer) -> int {
|
||||
var h = 5381
|
||||
var i = 0
|
||||
let n = len(name)
|
||||
while i < n {
|
||||
h = (h * 33 + name[i]) % 1000003
|
||||
i += 1
|
||||
}
|
||||
h = h % IX_SLOTS
|
||||
let ks = g_ix_keys[t]
|
||||
while ks[h] != null and not (ks[h] == name) { h = (h + 1) % IX_SLOTS }
|
||||
return h
|
||||
}
|
||||
function ix_sync() -> void {
|
||||
while g_ix_upto < len(prog) {
|
||||
let d = prog[g_ix_upto]
|
||||
let t = ix_kind(d)
|
||||
if d.kind == N_ENUM and enum_is_tagged(d) { push(g_ix_tagged, d) }
|
||||
if t >= 0 and d.s != null {
|
||||
let h = ix_slot(t, d.s)
|
||||
if g_ix_keys[t][h] == null { # the first of a name wins, as the walk had it
|
||||
g_ix_keys[t][h] = d.s
|
||||
g_ix_vals[t][h] = d
|
||||
}
|
||||
}
|
||||
g_ix_upto += 1
|
||||
}
|
||||
}
|
||||
# the node of kind t called name, or null; -1 when the table cannot say (off, or a stale hit)
|
||||
var g_ix_miss: bool = false
|
||||
function ix_find(t: int, name: pointer) -> Node {
|
||||
g_ix_miss = false
|
||||
if not g_ix_on or name == null {
|
||||
g_ix_miss = true
|
||||
return null
|
||||
}
|
||||
ix_sync()
|
||||
let h = ix_slot(t, name)
|
||||
let p = g_ix_vals[t][h]
|
||||
if p == null { return null }
|
||||
let d: Node = p
|
||||
if (d.s == name) and ix_kind(d) == t { return d }
|
||||
g_ix_miss = true
|
||||
return null
|
||||
}
|
||||
function find_comp(name: pointer) -> Node {
|
||||
let x = ix_find(2, name)
|
||||
if not g_ix_miss { return x }
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i += 1 }
|
||||
return null
|
||||
|
|
@ -174,6 +261,8 @@ function field_type(s: Node, fname: pointer) -> pointer {
|
|||
|
||||
# find a global var/const by name
|
||||
function find_global(name: pointer) -> Node {
|
||||
let x = ix_find(3, name)
|
||||
if not g_ix_miss { return x }
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
|
|
@ -188,6 +277,14 @@ function find_global(name: pointer) -> Node {
|
|||
# enum with that variant. Enum names live in `prog` like any other declaration.
|
||||
function enum_ordinal(ename: pointer, vname: pointer) -> int {
|
||||
var i = 0
|
||||
if g_ix_on { # the table knows the one enum of that name
|
||||
let d = find_enum(ename)
|
||||
if d != null {
|
||||
var j = 0
|
||||
while j < len(d.kids) { if (d.kids[j].s == vname) { return j }; j += 1 }
|
||||
}
|
||||
i = len(prog)
|
||||
}
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_ENUM and (d.s == ename) {
|
||||
|
|
@ -234,6 +331,8 @@ function has_countdowns() -> bool {
|
|||
return false
|
||||
}
|
||||
function find_fn(name: pointer) -> Node {
|
||||
let x = ix_find(0, name)
|
||||
if not g_ix_miss { return x }
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i += 1 }
|
||||
return null
|
||||
|
|
@ -248,6 +347,8 @@ function find_fn(name: pointer) -> Node {
|
|||
var g_var_ord: int = 0
|
||||
|
||||
function find_enum(name: pointer) -> Node {
|
||||
let x = ix_find(1, name)
|
||||
if not g_ix_miss { return x }
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_ENUM and (d.s == name) { return d }; i += 1 }
|
||||
return null
|
||||
|
|
@ -276,6 +377,16 @@ function variant_ordinal(en: Node, vname: pointer) -> int {
|
|||
# Only tagged enums participate, so a plain enum's `Enum.Variant` member form and
|
||||
# any like-named function are left alone.
|
||||
function variant_enum(vname: pointer) -> Node {
|
||||
if g_ix_on { # the tagged enums, kept as prog grows
|
||||
ix_sync()
|
||||
var t = 0
|
||||
while t < len(g_ix_tagged) {
|
||||
let ord = variant_ordinal(g_ix_tagged[t], vname)
|
||||
if ord >= 0 { g_var_ord = ord; return g_ix_tagged[t] }
|
||||
t += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
|
|
@ -306,6 +417,8 @@ function enum_box_size(en: Node) -> int { return 8 * (1 + enum_max_arity(en)) }
|
|||
# is the transport seam (net_send/net_poll), the windowing/socket FFI, and any
|
||||
# C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
|
||||
function find_extern(name: pointer) -> Node {
|
||||
let x = ix_find(4, name)
|
||||
if not g_ix_miss { return x }
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i += 1 }
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -213,19 +213,19 @@ function emit_test_runner() -> void {
|
|||
# two `function`s with one name would collide in the object file; say so in
|
||||
# source terms (and name both files) instead of leaving it to the IR assembler.
|
||||
function check_duplicate_fns() -> void {
|
||||
let k = new []pointer # by name, in a table: a pair of loops was quadratic
|
||||
let v = new []Node
|
||||
ck_tab_init(k, v)
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_FN {
|
||||
var j = i + 1
|
||||
while j < len(prog) {
|
||||
let o = prog[j]
|
||||
if o.kind == N_FN and (o.s == d.s) {
|
||||
g_err_file = o.file; g_err_line = o.line
|
||||
perr(`function '{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
|
||||
}
|
||||
j += 1
|
||||
let first = ck_tab_get(k, v, d.s)
|
||||
if first != null {
|
||||
g_err_file = d.file; g_err_line = d.line
|
||||
perr(`function '{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
|
||||
}
|
||||
ck_tab_put(k, v, d.s, d)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -240,22 +240,23 @@ function decl_group(k: int) -> int {
|
|||
return 0
|
||||
}
|
||||
function check_duplicate_decls() -> void {
|
||||
let k = new []pointer
|
||||
let v = new []Node
|
||||
ck_tab_init(k, v)
|
||||
let n = g_prog_user_end
|
||||
var i = 0
|
||||
while i < n and i < len(prog) {
|
||||
let d = prog[i]
|
||||
let g = decl_group(d.kind)
|
||||
if g > 0 {
|
||||
var j = i + 1
|
||||
while j < n and j < len(prog) {
|
||||
let o = prog[j]
|
||||
if decl_group(o.kind) == g and (o.s == d.s) {
|
||||
g_err_file = o.file
|
||||
g_err_line = o.line
|
||||
perr(`'{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
|
||||
}
|
||||
j += 1
|
||||
let key = `{itoa(g)}:{d.s}`
|
||||
let first = ck_tab_get(k, v, key)
|
||||
if first != null {
|
||||
g_err_file = d.file
|
||||
g_err_line = d.line
|
||||
perr(`'{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
|
||||
}
|
||||
ck_tab_put(k, v, key, d)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -264,6 +265,7 @@ function check_duplicate_decls() -> void {
|
|||
function emit_program() -> void {
|
||||
check_duplicate_fns()
|
||||
check_duplicate_decls()
|
||||
ix_start() # the lookups by name, as tables from here on
|
||||
head = buf_new()
|
||||
code = buf_new()
|
||||
g_uses_str = false
|
||||
|
|
|
|||
|
|
@ -29,17 +29,29 @@ function vis_plain(what: pointer) -> pointer {
|
|||
function vis_say(m: pointer) -> void {
|
||||
file_write(file_stderr(), m, len(m))
|
||||
}
|
||||
# does s start with p - without making the slice a comparison of s[0 .. n] would (asked for every
|
||||
# reference)
|
||||
function str_prefix(s: pointer, p: pointer) -> bool {
|
||||
if s == null { return false }
|
||||
let n = len(p)
|
||||
if len(s) < n { return false }
|
||||
var i = 0
|
||||
while i < n {
|
||||
if s[i] != p[i] { return false }
|
||||
i += 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
function vis_check(d: Node, what0: pointer) -> void {
|
||||
if d == null { return }
|
||||
port_check_bound(d)
|
||||
# 0.S/0.R: a state's instance, the action queue and a reducer are the runtime's to supply and to
|
||||
# call - the code that names them depends on no module for it
|
||||
if d.kind == N_VAR and d.uns == 1 and str_prefix(d.s, "state$") and is_state_ty(d.ty) { return }
|
||||
if d.kind == N_FN and (str_prefix(d.s, "ludic_reduce__") or is_action_builtin(d.s)) { return }
|
||||
deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
|
||||
if g_vis_off { return }
|
||||
if d.file == null { return }
|
||||
# 0.S: a state's instance, supplied by the runtime (a component's or a view's glue, an entry's
|
||||
# parameters): the code that names it only passes it on
|
||||
if d.kind == N_VAR and d.uns == 1 and is_state_ty(d.ty) and len(d.s) > 6 and (d.s[0 .. 6] == "state$") { return }
|
||||
# 0.R: the action queue and the reducers are the runtime's to call, from any module
|
||||
if d.kind == N_FN and (is_action_builtin(d.s) or (len(d.s) > 14 and (d.s[0 .. 14] == "ludic_reduce__"))) { return }
|
||||
let what = vis_plain(what0)
|
||||
var here = g_err_file
|
||||
if here == null { return }
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ function ck_expr(e: Node) -> pointer {
|
|||
let f = ck_fn(e.s)
|
||||
ck_vis(f, e.s, e)
|
||||
if f != null { return fn_sig_of(f) }
|
||||
# the emitter's own refusal, here too, so a check-only build finds it (a bind to a function
|
||||
# that is gone)
|
||||
if find_fn(e.s) == null { ck_err("fnref", e, `fn {e.s}: no function called {e.s}`) }
|
||||
return "?"
|
||||
}
|
||||
if k == E_NEW { return ck_new(e) }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var g_red_action: []pointer = new []pointer
|
|||
var g_dsp_ids: []Node = new []Node # each dispatch's action number, filled at the end
|
||||
var g_dsp_names: []pointer = new []pointer
|
||||
var g_dsp_at: []Node = new []Node
|
||||
var g_act_main: pointer = null # the program's own file, where the drain is written
|
||||
const ACTION_PASSES: int = 64
|
||||
|
||||
# action NAME { fields } - a record
|
||||
|
|
@ -101,12 +102,11 @@ function actions_finish() -> void {
|
|||
g_dsp_ids[i].ival = k
|
||||
i += 1
|
||||
}
|
||||
# the queue and the drain are the program's, written in its own file: in the file of the first
|
||||
# action they would belong to its module, and it would call every module's reducers
|
||||
var file = g_parse_file
|
||||
var line = 1
|
||||
if len(g_act_nodes) > 0 {
|
||||
file = g_act_nodes[0].file
|
||||
line = g_act_nodes[0].line
|
||||
}
|
||||
if g_act_main != null { file = g_act_main }
|
||||
let line = 1
|
||||
# no actions: drain_actions() is still there (a ludic.base runner calls it), and does nothing
|
||||
if len(g_act_names) == 0 {
|
||||
vw_parse("export function drain_actions() -> void {\n}\n", file, line, 1)
|
||||
|
|
|
|||
|
|
@ -128,15 +128,9 @@ function pkg_name_of(rel: pointer) -> pointer {
|
|||
function pkg_file_set(f: pointer, name: pointer) -> void {
|
||||
push(g_pk_file, f)
|
||||
push(g_pk_name, name)
|
||||
fm_put(g_fm_pkg, f, name)
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
function pkg_of_file(f: pointer) -> pointer { return fm_get(g_fm_pkg, f) }
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -25,36 +25,77 @@ var g_trusted_files: []pointer = new []pointer
|
|||
var g_unsafe_all: bool = false
|
||||
function unsafe_trusted(f: pointer) -> bool {
|
||||
if g_unsafe_all { return true }
|
||||
return not (fm_get(g_fm_trusted, f) == "")
|
||||
}
|
||||
# a file's facts - its module, its package, whether it is trusted - asked of every reference the
|
||||
# checker and the emitter resolve: a table by file name, not a list walked each time (with a game's
|
||||
# nine hundred files that walk was most of a check-only build)
|
||||
const FM_SLOTS: int = 16384
|
||||
property FileMap {
|
||||
keys: []pointer = null
|
||||
vals: []pointer = null
|
||||
}
|
||||
function fm_new() -> FileMap {
|
||||
let m = new FileMap
|
||||
m.keys = new []pointer
|
||||
m.vals = new []pointer
|
||||
var i = 0
|
||||
while i < len(g_trusted_files) {
|
||||
if (g_trusted_files[i] == f) { return true }
|
||||
while i < FM_SLOTS {
|
||||
push(m.keys, null)
|
||||
push(m.vals, null)
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
return m
|
||||
}
|
||||
function fm_slot(m: FileMap, f: pointer) -> int {
|
||||
var h = 5381
|
||||
var i = 0
|
||||
let n = len(f)
|
||||
while i < n {
|
||||
h = (h * 33 + f[i]) % 1000003
|
||||
i += 1
|
||||
}
|
||||
h = h % FM_SLOTS
|
||||
while m.keys[h] != null and not (m.keys[h] == f) { h = (h + 1) % FM_SLOTS }
|
||||
return h
|
||||
}
|
||||
# the latest value set for f wins, as the lists' walk from the end had it
|
||||
function fm_put(m: FileMap, f: pointer, v: pointer) -> void {
|
||||
if f == null { return }
|
||||
let h = fm_slot(m, f)
|
||||
m.keys[h] = f
|
||||
m.vals[h] = v
|
||||
}
|
||||
function fm_get(m: FileMap, f: pointer) -> pointer {
|
||||
if f == null { return "" }
|
||||
let h = fm_slot(m, f)
|
||||
if m.keys[h] == null { return "" }
|
||||
return m.vals[h]
|
||||
}
|
||||
var g_fm_mod: FileMap = fm_new()
|
||||
var g_fm_pkg: FileMap = fm_new()
|
||||
var g_fm_trusted: FileMap = fm_new()
|
||||
function trust_file(f: pointer) -> void {
|
||||
push(g_trusted_files, f)
|
||||
fm_put(g_fm_trusted, f, "1")
|
||||
}
|
||||
var g_mod_file: []pointer = new []pointer
|
||||
var g_mod_name: []pointer = new []pointer
|
||||
var g_mod_friends: []pointer = new []pointer
|
||||
function module_of(f: pointer) -> pointer {
|
||||
var i = len(g_mod_file) - 1
|
||||
while i >= 0 {
|
||||
if (g_mod_file[i] == f) { return g_mod_name[i] }
|
||||
i -= 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
function module_of(f: pointer) -> pointer { return fm_get(g_fm_mod, f) }
|
||||
function module_set(f: pointer, name: pointer) -> void {
|
||||
push(g_mod_file, f)
|
||||
push(g_mod_name, name)
|
||||
fm_put(g_fm_mod, f, name)
|
||||
}
|
||||
function is_float_file(f: pointer) -> bool {
|
||||
if (f == null) { return false }
|
||||
var i = 0
|
||||
while i < len(g_float_files) {
|
||||
if (g_float_files[i] == f) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
return not (fm_get(g_fm_float, f) == "")
|
||||
}
|
||||
var g_fm_float: FileMap = fm_new()
|
||||
function float_file_add(f: pointer) -> void {
|
||||
push(g_float_files, f)
|
||||
fm_put(g_fm_float, f, "1")
|
||||
}
|
||||
var g_parsing: bool = true # false once lowering starts
|
||||
var g_err_file: pointer = "" # the statement being lowered
|
||||
|
|
@ -952,7 +993,7 @@ function parse_one_decl() -> void {
|
|||
}
|
||||
if is_id("numbers") and (toks[pi + 1].text == "float") {
|
||||
pi += 2
|
||||
if not is_float_file(g_parse_file) { push(g_float_files, g_parse_file) }
|
||||
if not is_float_file(g_parse_file) { float_file_add(g_parse_file) }
|
||||
return
|
||||
}
|
||||
if is_id("import") { pi += 1
|
||||
|
|
@ -1180,11 +1221,11 @@ function do_import(rel: pointer) -> void {
|
|||
}
|
||||
if already_loaded(full) { return }
|
||||
push(loaded_paths, full)
|
||||
if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { push(g_float_files, full) }
|
||||
if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { float_file_add(full) }
|
||||
# a module reaches as far as its own files: a package found through $LUDIC_HOME or
|
||||
# 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 is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { trust_file(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)) }
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ function reg_fill(r: int) -> void {
|
|||
j += 1
|
||||
}
|
||||
push(seen, key)
|
||||
if (reg_upper(key) == "COUNT") {
|
||||
g_err_file = rec.file
|
||||
g_err_line = rec.line
|
||||
g_parsing = false
|
||||
perr(`def {g_rg_name[r]} {key}: its constant would be {g_rg_prefix[r]}_COUNT, which is the registry's size; give the entry another key`)
|
||||
}
|
||||
if keyed and not reg_rec_has(rec, "key") {
|
||||
let fi = node(E_FINIT)
|
||||
fi.s = "key"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function res_read_into(reg: pointer, from: pointer, who: pointer) -> void {
|
|||
let saved_file = g_parse_file
|
||||
let saved_parsing = g_parsing
|
||||
if not (module_of(saved_file) == "") { module_set(path, module_of(saved_file)) }
|
||||
if is_float_file(saved_file) and not is_float_file(path) { push(g_float_files, path) }
|
||||
if is_float_file(saved_file) and not is_float_file(path) { float_file_add(path) }
|
||||
g_parse_file = path
|
||||
g_parsing = true
|
||||
g_res_mode = true
|
||||
|
|
|
|||
65588
selfhost/ludicc.seed.ll
65588
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -188,6 +188,7 @@ entry {
|
|||
cur_dir = dir_of(path)
|
||||
g_src_name = base_name(path) # for panic/expect file:line messages
|
||||
g_parse_file = path # for the compiler's own file:line diagnostics
|
||||
g_act_main = path # 0.R: where the action drain is written
|
||||
lex(src)
|
||||
parse_program()
|
||||
g_prog_user_end = len(prog)
|
||||
|
|
@ -217,9 +218,15 @@ entry {
|
|||
check_duplicate_decls()
|
||||
g_mg_entry = path
|
||||
if g_migrate { mg_collect() } # 0.S2: the vars that move (migrate.ludic)
|
||||
ix_start() # the lookups by name, as tables (emit_core.ludic)
|
||||
check_program() # L4: the types agree before anything is emitted
|
||||
if g_migrate { mg_finish() }
|
||||
if g_check_only { exit(0) } # --check: the checks are all there is
|
||||
# --check: every check a build makes - the emitter refuses things too (a bind to a function that is
|
||||
# gone, an unknown name) - and nothing written
|
||||
if g_check_only {
|
||||
emit_program()
|
||||
exit(0)
|
||||
}
|
||||
emit_program()
|
||||
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function dp_load(path: pointer) -> bool {
|
|||
if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) }
|
||||
if len(w) >= 5 and w[0] == "width" and w[4] == "0" and s_to_int(w[1]) > dp_wide_n {
|
||||
dp_wide_n = s_to_int(w[1])
|
||||
dp_wide_at = `{w[2]} ({w[3]})`
|
||||
dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})`
|
||||
}
|
||||
if len(w) >= 5 and w[0] == "write" {
|
||||
push(dp_wowner, w[1])
|
||||
|
|
@ -102,6 +102,15 @@ function dp_load(path: pointer) -> bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
# a function as its source names it: a reducer is `reducer Bag on PickUp`, not its symbol
|
||||
function dp_fn_name(n: pointer) -> pointer {
|
||||
if not s_starts(n, "ludic_reduce__") { return n }
|
||||
let rest = n[14 .. slen(n)]
|
||||
for i in 0 .. slen(rest) - 1 {
|
||||
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {rest[i + 2 .. slen(rest)]} on {rest[0 .. i]}` }
|
||||
}
|
||||
return n
|
||||
}
|
||||
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
|
||||
# an edge the numbers count: between two of the program's own modules
|
||||
function dp_counted(e: int) -> bool { return dp_own(dp_ef[e]) and dp_own(dp_et[e]) }
|
||||
|
|
|
|||
|
|
@ -500,9 +500,9 @@ function deps_case() -> void {
|
|||
# --check: the module rules, the ports and the types, with nothing emitted - a broken uses line is
|
||||
# refused as a full build refuses it, and a sound program writes no file
|
||||
function check_build_case() -> void {
|
||||
let lbl = "ludicc --check / ludic build --check: modules, uses, ports and types without emitting"
|
||||
let bad = ["rejected/uses_missing", "rejected/layer_reach", "rejected/private_module", "rejected/port_unbound", "rejected/registry_hidden", "rejected/port_new", "rejected/wrong_arity"]
|
||||
let want = ["fishing uses items.inv_add", "hud uses items.item_count", "add is private to module bank", "port Clock is used here but never bound", "Tools is private to module kit", "Clock is a port", "leaves out h"]
|
||||
let lbl = "ludicc --check / ludic build --check: every check a build makes (binds, names, registries too), nothing written"
|
||||
let bad = ["rejected/uses_missing", "rejected/layer_reach", "rejected/private_module", "rejected/port_unbound", "rejected/registry_hidden", "rejected/port_new", "rejected/wrong_arity", "rejected/bind_missing_fn", "rejected/unknown_name", "rejected/registry_count_key"]
|
||||
let want = ["fishing uses items.inv_add", "hud uses items.item_count", "add is private to module bank", "port Clock is used here but never bound", "Tools is private to module kit", "Clock is a port", "leaves out h", "fn no_such_clock: no function called no_such_clock", "unknown identifier no_such_name", "its constant would be TL_COUNT, which is the registry's size"]
|
||||
for i in 0 .. len(bad) {
|
||||
if shq(`bin/ludicc --check examples/{bad[i]}.ludic > {tmp_dir()}/chk.err 2>&1`) { bad2(lbl, `{bad[i]} passed the check`); return }
|
||||
let said = capture(`cat {tmp_dir()}/chk.err`)
|
||||
|
|
@ -547,6 +547,13 @@ function expect_str_case() -> void {
|
|||
let want = "ok - equal text built two ways\nexamples/library/testing_strings.ludic:11: expect_eq failed (got \"camp\", want \"lake\")\nFAIL - different text fails, and says both\nexamples/library/testing_strings.ludic:15: expect_eq failed (got \"<null>\", want \"camp\")\nFAIL - a null is not a string\n== 1 passed, 2 failed ==\n"
|
||||
if s_trim(got) == s_trim(want) { ok(lbl) } else { bad2(lbl, `said [{s_trim(got)}]`) }
|
||||
}
|
||||
# 0.R: the drain that calls every reducer is the program's own, so the module that declares the
|
||||
# actions gains no dependency on the modules that reduce them
|
||||
function actions_deps_case() -> void {
|
||||
let lbl = "the action drain is the program's: acts depends on no reducer's module (ludic deps)"
|
||||
let got = capture(`bin/ludic deps examples/actions/modules.ludic 2>&1`)
|
||||
if s_contains(got, "largest_cycle: 1") and s_contains(got, "dependencies: 0") and s_contains(got, "the widest function: reducer Bag on PickUp (") { ok(lbl) } else { bad2(lbl, s_trim(got)) }
|
||||
}
|
||||
# a program built headless with the toolchain and run: its first line
|
||||
function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
|
||||
let out = `{tmp_dir()}/h_{flat(path)}`
|
||||
|
|
@ -963,8 +970,11 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
|
||||
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
|
||||
feat_case("actions/pack", "", "0 1 9 13 picked 7 picked 9 too heavy", "pack.ludic (0.R: actions, reducers - one per state, in the order of the states' names - dispatch, drain_actions, an action a reducer dispatches goes behind)")
|
||||
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
|
||||
actions_deps_case()
|
||||
feat_case("actions/phases", "dd a q", "1 0 2 0 2 1 1 1 1 2 1 2", "phases.ludic (0.R: the frame loop drains the queue after every phase - Input's actions are reduced before Update)")
|
||||
feat_case("actions/runaway", "", "actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what dispatches it", "runaway.ludic (0.R: a reducer that dispatches what dispatches it is stopped by name)")
|
||||
reject_case("rejected/registry_count_key", "def Tools count: its constant would be TL_COUNT", "a registry key named count is refused")
|
||||
reject_case("rejected/reducer_two_states", "a reducer takes one state, and w is a Wallet", "a reducer takes exactly one state")
|
||||
reject_case("rejected/reducer_not_action", "Buy is not an action", "a reducer is on an action")
|
||||
reject_case("rejected/dispatch_unknown", "dispatch Sell: Sell is not an action", "only an action is dispatched")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue