ludic deps: the reach bitsets hold 30 states to a word, not 60 - an int is 32 bits, so states 32 apart shared a bit and a function could reach fewer states than it takes; examples/state/reach_wide.ludic and a deps case hold it; reseeded (bootstrap-cfree fixpoint)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 16:49:34 +03:00
parent d7adc28125
commit 2a18d9b51f
6 changed files with 2894 additions and 2814 deletions

View file

@ -0,0 +1,9 @@
bump: patch
type: fix
**`ludic deps`: a reach counts every state apart, however the states are numbered.** The reach and
write-reach bitsets packed 60 states to a word, but an `int` is 32 bits, so `1 << 45` came back as bit
13 and states 32 apart shared a bit: a function taking both counted one, fewer than it takes, and the
counts (`widest_reach`, `widest_write_reach`, `--reach`, `--wreach`) rose and fell with how a program's
states happened to be numbered. The sets now hold 30 to a word. On Maroon Lake `widest_reach` goes
58 -> 76 and `widest_write_reach` 54 -> 64 - the real numbers, which the old count hid.
`examples/state/reach_wide.ludic` (40 states, S00 and S32 taken together) holds it.

View file

@ -0,0 +1,58 @@
# ludic deps: states numbered past a word's 32 bits are still counted apart. S00 and S32 once shared
# a bit (an int is 32 bits and `1 << 32` wrapped), so `pair` reached 1 and `all4` reached 2
program ReachWide {
state S00 { n: int = 0 }
state S01 { n: int = 0 }
state S02 { n: int = 0 }
state S03 { n: int = 0 }
state S04 { n: int = 0 }
state S05 { n: int = 0 }
state S06 { n: int = 0 }
state S07 { n: int = 0 }
state S08 { n: int = 0 }
state S09 { n: int = 0 }
state S10 { n: int = 0 }
state S11 { n: int = 0 }
state S12 { n: int = 0 }
state S13 { n: int = 0 }
state S14 { n: int = 0 }
state S15 { n: int = 0 }
state S16 { n: int = 0 }
state S17 { n: int = 0 }
state S18 { n: int = 0 }
state S19 { n: int = 0 }
state S20 { n: int = 0 }
state S21 { n: int = 0 }
state S22 { n: int = 0 }
state S23 { n: int = 0 }
state S24 { n: int = 0 }
state S25 { n: int = 0 }
state S26 { n: int = 0 }
state S27 { n: int = 0 }
state S28 { n: int = 0 }
state S29 { n: int = 0 }
state S30 { n: int = 0 }
state S31 { n: int = 0 }
state S32 { n: int = 0 }
state S33 { n: int = 0 }
state S34 { n: int = 0 }
state S35 { n: int = 0 }
state S36 { n: int = 0 }
state S37 { n: int = 0 }
state S38 { n: int = 0 }
state S39 { n: int = 0 }
function low(a: mut S00) -> void { a.n += 1 }
function high(b: mut S32) -> void { b.n += 1 }
function pair(a: mut S00, b: mut S32) -> void {
low(a)
high(b)
}
function reads(c: S01, d: S33) -> int { return c.n + d.n }
function all4(a: mut S00, b: mut S32, c: S01, d: S33) -> int {
pair(a, b)
return reads(c, d)
}
entry (a: mut S00, b: mut S32, c: S01, d: S33) {
print(`{all4(a, b, c, d)}`)
}
}

View file

@ -274,7 +274,7 @@ function deps_alias_write(t: Node) -> void {
var g_dr_name: []pointer = new []pointer # the declarations a name can reach: functions, globals var g_dr_name: []pointer = new []pointer # the declarations a name can reach: functions, globals
var g_dr_node: []Node = new []Node var g_dr_node: []Node = new []Node
var g_dr_refs: [][]int = new [][]int # what each one names var g_dr_refs: [][]int = new [][]int # what each one names
var g_dr_bits: [][]int = new [][]int # the states it reaches, 60 to a word var g_dr_bits: [][]int = new [][]int # the states it reaches, DR_BITS to a word
var g_dr_wbits: [][]int = new [][]int # ... and of them the ones it takes `mut` somewhere down var g_dr_wbits: [][]int = new [][]int # ... and of them the ones it takes `mut` somewhere down
var g_dr_find_k: []pointer = new []pointer var g_dr_find_k: []pointer = new []pointer
var g_dr_rl_name: []pointer = new []pointer # a local holding one entry of a registry ... var g_dr_rl_name: []pointer = new []pointer # a local holding one entry of a registry ...
@ -294,8 +294,11 @@ function dr_state_ix(t: pointer) -> int {
} }
return -1 return -1
} }
function dr_set(bits: []int, s: int) -> void { bits[s / 60] = bits[s / 60] | (1 << (s % 60)) } # an int is 32 bits: `1 << 45` is not bit 45, it came back as bit 13, so states 32 apart shared a bit
function dr_words() -> int { return len(g_state_names) / 60 + 1 } # and a function that took both counted one - the counts rose and fell with how the states were numbered
const DR_BITS: int = 30
function dr_set(bits: []int, s: int) -> void { bits[s / DR_BITS] = bits[s / DR_BITS] | (1 << (s % DR_BITS)) }
function dr_words() -> int { return len(g_state_names) / DR_BITS + 1 }
function dr_index(name: pointer) -> int { function dr_index(name: pointer) -> int {
let n = ck_tab_get(g_dr_find_k, g_dr_find_v, name) let n = ck_tab_get(g_dr_find_k, g_dr_find_v, name)
if n == null { return -1 } if n == null { return -1 }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -698,6 +698,15 @@ function deps_wreach_case() -> void {
if not s_contains(top, " 0 2 2 changes (examples/state/write_reach.ludic:27)") or not s_contains(top, " 0 1 0 asks (examples/state/write_reach.ludic:22)") or s_contains(top, "asks_local") { bad2(lbl, s_trim(top)); return } if not s_contains(top, " 0 2 2 changes (examples/state/write_reach.ludic:27)") or not s_contains(top, " 0 1 0 asks (examples/state/write_reach.ludic:22)") or s_contains(top, "asks_local") { bad2(lbl, s_trim(top)); return }
ok(lbl) ok(lbl)
} }
# an int is 32 bits: states numbered 32 apart once shared a bit of the reach, so `pair` (S00 and S32)
# reached 1 and could change 1; the counts must not depend on how the states are numbered
function deps_reach_wide_case() -> void {
let lbl = "ludic deps: a reach counts states past 32 apart (S00 and S32 are two states, not one)"
let top = capture(`bin/ludic deps examples/state/reach_wide.ludic --wreach 2 2>&1`)
if not s_contains(top, " 4 4 2 all4 (examples/state/reach_wide.ludic:51)\n 2 2 2 pair (examples/state/reach_wide.ludic:46)") { bad2(lbl, s_trim(top)); return }
let sum = capture(`bin/ludic deps examples/state/reach_wide.ludic 2>&1`)
if s_contains(sum, "widest_reach: 4") and s_contains(sum, "widest_write_reach: 2") { ok(lbl) } else { bad2(lbl, s_trim(sum)) }
}
# a program built headless with the toolchain and run: its first line # a program built headless with the toolchain and run: its first line
function headless_case(path: pointer, exp: pointer, label: pointer) -> void { function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
let out = `{tmp_dir()}/h_{flat(path)}` let out = `{tmp_dir()}/h_{flat(path)}`
@ -1234,6 +1243,7 @@ function cmd_dev_test() -> int {
actions_deps_case() actions_deps_case()
deps_reach_case() deps_reach_case()
deps_wreach_case() deps_wreach_case()
deps_reach_wide_case()
feat_case("state/reach", "", "3", "reach.ludic (0.R4: a step list of fn values supplies each step its states)") feat_case("state/reach", "", "3", "reach.ludic (0.R4: a step list of fn values supplies each step its states)")
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/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)") 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)")