Reproducing the Traps in the Original Code

A companion to A Vector-Symbolic Clojure. The overview found five traps in a vector-symbolic Lisp. This page runs each one against the original authors’ own code. October 2026.

My Clojure started from Tomkins-Flanagan and Kelly’s “Hey Pentti, We Did It!” (ICCM 2024). That paper built Lisp 1.5 out of holographic vectors and showed that it can be done. Their follow-up with Hanley, “Hey Pentti, We Did (More of) It!” (IJCNN 2025), added integers in residue arithmetic. Both have public code. My five traps came from my code. The fair question is which of them exist in theirs. Here I answer it by running their code: I fetch it at a pinned commit, add noise the way my paper does, and count. The trap numbers 1–5 are those of the overview.

Their papers are proofs of concept, and holis’s README says so. Nothing below changes that. A proof of concept is where the next person starts, and this page lists what that next person (me) ran into.

If you maintain or build on either codebase, go to Errata for the authors: each defect with its location, a minimal program and a suggested fix.

The two codebases

holis (eilene-ftf/holis, commit db208d7, AGPL-3.0) is one Jupyter notebook. It adapts Norvig’s lispy to holographic reduced representations (HRR) at D = 512:

  • Vectors are random Gaussian, bound by circular convolution.
  • A cons cell is the bundle lhs⊗a + rhs⊗b + φ, but it is stored behind a fresh random pointer in a second memory, because, in the notebook’s words, “tuples turn out to degrade rather quickly”. Equal bundles share a pointer.
  • CAR and CDR follow the pointer, unbind a role and clean up against a lookup table (argmax).
  • The pointer memory looks up a noisy pointer by its nearest key, but only if the cosine exceeds θ = 0.2. Otherwise it returns the probe itself.
  • The evaluator tests with a fixed floor of 0.2, and COND is the paper’s saturating ⊕ with cutoffs 0.8 and 0.2.
  • Functions work by substitution, so there are no environments. Numbers are Peano lists: 0 is NIL and n+1 is (n).

residuelisp (hanleyc01/residuelisp, commit 09864c7, MIT) is the code of the second paper, a typed Python package. It uses complex FHRR vectors at D = 1000:

  • Cells sit behind pointers, as in holis.
  • Function calls bind parameters in a small memory keyed by symbol.
  • Integers have two schemes. The default, ListIntegers, is Peano lists as in holis. IntegerEncodingScheme.RHCIntegers, which main.py -i, the test_rhc_* tests and all my runs use, is Kymn et al.’s residue code over the moduli 3, 5, 7, 11 (range 0..1154), plus a tag: int(x) = ζ(x) + int. R1 and R4 need RHCIntegers.
  • It keeps holis’s numbers: θ = 0.2 in the pointer memory and a 0.2 floor in its approximate equality. Its similarity is not normalised, though.

What I ran

Nothing of theirs is copied into my repository. A small loader fetches each codebase at its pinned commit when a script runs. For holis it executes the notebook’s code cells as a module. The scripts in repro/holis/ then:

  • add probe noise exactly as in my paper: before every cleanup, the probe p becomes p + σ·|p|/√D · N(0, I). Cleanups are the item memory, the function memory and the pointer memory;
  • change at most one thing as a control, and say which;
  • count over seeds, and write every number on this page to a JSON file that the page reads.

First, their code works. Noise-free, holis computes these at D = 512 and 2048 over 5 seeds each:

program D correct
(CAR (a b c))
512 5 / 5
(CDR (a b c))
512 5 / 5
(CONS a (b c))
512 5 / 5
(EQ a a)
512 5 / 5
(EQ a b)
512 5 / 5
(ATOM a)
512 5 / 5
(ATOM (a b))
512 5 / 5
(COND ((EQ a a) . yes) (T . no))
512 5 / 5
(COND ((EQ a b) . yes) (T . no))
512 5 / 5
((LAMBDA (x) (CONS x (b c))) a)
512 4 / 5
(CAR (a b c))
2048 5 / 5
(CDR (a b c))
2048 5 / 5
(CONS a (b c))
2048 5 / 5
(EQ a a)
2048 5 / 5
(EQ a b)
2048 5 / 5
(ATOM a)
2048 5 / 5
(ATOM (a b))
2048 5 / 5
(COND ((EQ a a) . yes) (T . no))
2048 5 / 5
(COND ((EQ a b) . yes) (T . no))
2048 5 / 5
((LAMBDA (x) (CONS x (b c))) a)
2048 4 / 5

One program misses a seed. I return to it under E1 below. The authors’ own fizzbuzz helpers, verbatim from the notebook, also give the right answers. They are slow, which is why the noise sweeps below use smaller programs:

program result expected seconds
their fizzbuzz DEFINEs
["NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL"]
["NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL", "NIL"]
5.9
countdown 3 1
[[["NIL"]]]
[[["NIL"]]]
16.6
divisible 3 3
["T"]
["T"]
83.1
divisible 4 3
["F"]
["F"]
767.8
and T F
["F"]
["F"]
51.7
take 2 (a b c)
[["a", "b"]]
[["a", "b"]]
241.2

residuelisp, noise-free at D = 1000 over 5 seeds:

program expected correct
(+ 1 2 )
3
5 / 5
(+ 20 22 )
42
5 / 5
(- 7 3 )
4
5 / 5
(car (cons 1 2 ) )
1
5 / 5
(cdr (cons a b))
b
5 / 5
((lambda (x) ((lambda (y) (cons x (cons y nil))) n)) m)
["m", "n"]
5 / 5
(eq? a a)
#t
5 / 5
(eq? a b)
#f
5 / 5
(eq? 3 3 )
#t
5 / 5
(int? 3 )
#t
5 / 5
(int? a)
#f
5 / 5
(and #t #t)
#t
5 / 5
(and #f #t)
#f
5 / 5
(if #t yes no)
yes
5 / 5
(if #f yes no)
no
5 / 5

So both interpreters do what their papers show. What follows asks for more than the papers claim.

Trap 1: Superposed cells, which the paper has and the code does not

Applies? To the paper’s equation (2), cons(a, b) = ν(L⊗a + R⊗b + φ), yes. To the code, no: both codebases put each cell behind a random pointer, as my fix does. The holis notebook gives the reason in a comment.

To see the paper’s cell inside their code, I added a paper mode. It is two attributes on their pointer memory: a cell is stored as its own normalised bundle, and “following” it returns it unchanged. The evaluator, CAR, CDR and the reader stay theirs. At D = 2048, Peano numerals n and n+1 then look alike. Pointer cells do not:

mode mean cos(n, n+1) mean cos(n, n+2) seeds
pointer 0.000 0.005 5
paper 0.743 0.721 5

Reproduced? Yes, noise-free, and in a way I did not expect. Their evaluator decides that an expression “evaluates to itself”, and stops, when the result is similar to the input: (ee == e) > 0.2. With pointers, a form and its result are unrelated. With superposed cells, a form and its first evaluation step share terms, and their cosine is well above 0.2:

mode program correct results mean cos(form, first step)
pointer
(CAR (a b c))
10 / 10 ["a"] ×10 0.005
pointer
(COND ((EQ a b) . yes) (T . no))
10 / 10 ["no"] ×10 0.001
pointer
((LAMBDA (x) (CONS x (b c))) a)
9 / 10 [["a", "b", "c"]] ×9, [["a", "LAMBDA", ["c"], "LAMBDA"]] ×1 0.005
paper
(CAR (a b c))
10 / 10 ["a"] ×10 -0.003
paper
(COND ((EQ a b) . yes) (T . no))
10 / 10 ["no"] ×10 -0.001
paper
((LAMBDA (x) (CONS x (b c))) a)
0 / 10 [["CONS", "a", ["b", "c"]]] ×7, [["CONS", ["x", "b", "c"], ["b", "c"]]] ×2, [["CONS", ["b", "c"], ["b", "c"]]] ×1 0.435

In paper mode the LAMBDA program stops after the substitution and returns the body unevaluated. This is trap 1’s lesson in its plainest form: similarity of content is not identity.

Under noise the picture is less clean. Here numerals 1 to 8 are built, and then numeral 4 is read back with their reader:

σ pointer pointer, theta=5/sqrt(D) pointer, theta=-1 paper
0 20 / 20 20 / 20 0 / 20 20 / 20
0.500 20 / 20 20 / 20 0 / 20 20 / 20
1 20 / 20 20 / 20 0 / 20 20 / 20
1.500 20 / 20 20 / 20 0 / 20 20 / 20
2 20 / 20 20 / 20 0 / 20 20 / 20
3 20 / 20 20 / 20 0 / 20 19 / 20
4 16 / 20 20 / 20 0 / 20 17 / 20
5 0 / 20 18 / 20 0 / 20 15 / 20
6 0 / 20 17 / 20 0 / 20 14 / 20
8 0 / 20 1 / 20 0 / 20 5 / 20
10 0 / 20 0 / 20 0 / 20 1 / 20
12 0 / 20 0 / 20 0 / 20 0 / 20
16 0 / 20 0 / 20 0 / 20 0 / 20

With their θ = 0.2, pointer cells fail first, because of that threshold (trap 3 below). With θ set relative to chance, 5/√D, the comparison is between encodings alone. Then the curves cross: pointer cells read more numerals right at σ = 5 and 6, superposed cells at σ = 8. So this test does not show the loss of noise tolerance I measured in my own code (trap 1 on the overview). One difference: a superposed cell needs no pointer lookup, so each read makes fewer noisy cleanups. Plain argmax (θ = −1) breaks the pointer reads even at σ = 0. Their atom test needs a threshold, which I come back to in trap 3.

Verdict. The paper’s cell causes a noise-free failure in their code. The code’s own cell does not have that problem. The extra noise sensitivity did not reproduce in my read test.

Trap 2: Binding commutes

Applies? Mostly no. Neither codebase stores an unlabelled pair: cells carry the roles lhs and rhs. holis has no environments, and residuelisp’s are a keyed memory, not a sum of var⊗value. As a control, (x . y) and (y . x) read back correctly every time.

There is one opening. In holis the roles are also ordinary atoms: vsa_env enters lhs, rhs and φ in the lexicon. A program that mentions the symbol lhs therefore gets the role vector itself. The cell (x . lhs) then contains lhs⊗x + rhs⊗lhs. Unbinding lhs finds x and rhs at equal strength, because rhs⊗lhs = lhs⊗rhs.

Reproduced? Yes, through their parser, evaluator and reader, 40 seeds per row:

program D expected correct results
(CAR (x . lhs))
512 x 18 / 40 rhs ×22, x ×18
(CDR (rhs . x))
512 x 17 / 40 lhs ×23, x ×17
(CAR (x . rhs))
512 x 40 / 40 x ×40
(CDR (lhs . x))
512 x 40 / 40 x ×40
(CAR (x . y))
512 x 40 / 40 x ×40
(CAR (y . x))
512 y 40 / 40 y ×40
(CDR (x . y))
512 y 40 / 40 y ×40
(CDR (y . x))
512 x 40 / 40 x ×40
(CAR (x . lhs))
2048 x 19 / 40 rhs ×21, x ×19
(CDR (rhs . x))
2048 x 22 / 40 x ×22, lhs ×18
(CAR (x . rhs))
2048 x 40 / 40 x ×40
(CDR (lhs . x))
2048 x 40 / 40 x ×40
(CAR (x . y))
2048 x 40 / 40 x ×40
(CAR (y . x))
2048 y 40 / 40 y ×40
(CDR (x . y))
2048 y 40 / 40 y ×40
(CDR (y . x))
2048 x 40 / 40 x ×40
(CAR (x . lhs))
8192 x 20 / 40 x ×20, rhs ×20
(CDR (rhs . x))
8192 x 22 / 40 x ×22, lhs ×18
(CAR (x . rhs))
8192 x 40 / 40 x ×40
(CDR (lhs . x))
8192 x 40 / 40 x ×40
(CAR (x . y))
8192 x 40 / 40 x ×40
(CAR (y . x))
8192 y 40 / 40 y ×40
(CDR (x . y))
8192 y 40 / 40 y ×40
(CDR (y . x))
8192 x 40 / 40 x ×40

About half the reads give the wrong atom at every D. It is a tie, not noise, so a bigger D does not help. residuelisp names its roles __lhs and __rhs. Its lexer starts identifiers with a letter, so a program cannot name them (I read this in their lexer and did not run it).

Trap 3: Fixed thresholds cap robustness

Applies? To holis, yes. Its pointer memory accepts the nearest key only above cosine θ = 0.2. A probe with noise σ has cosine 1/√(1 + σ²) to its key. That falls below 0.2 at σ = √24 ≈ 4.9, whatever D is.

Reproduced? Yes. The control is the same code with θ = 5/√D, a threshold relative to chance, as my paper prescribes. It is one attribute set on their object. Plain argmax (θ = −1) would not do: their atom test needs the threshold to tell an atom, which matches no pointer, from a cell. With θ = −1 even noise-free reads fail. For D = 512, 5/√D = 0.22 is close to their 0.2. First, single CAR lookups on 25 stored 4-lists, 200 lookups per cell. The cells give correct lookups, and in parentheses how often θ made the memory return the probe itself:

σ D=512, theirs (theta=0.2) D=512, chance floor (theta=5/sqrt(D)) D=2048, theirs (theta=0.2) D=2048, chance floor (theta=5/sqrt(D)) D=8192, theirs (theta=0.2) D=8192, chance floor (theta=5/sqrt(D))
0 200 (0) 200 (0) 200 (0) 200 (0) 200 (0) 200 (0)
1 200 (0) 200 (0) 200 (0) 200 (0) 200 (0) 200 (0)
2 196 (0) 196 (0) 200 (0) 200 (0) 200 (0) 200 (0)
3 159 (0) 159 (1) 200 (0) 200 (0) 200 (0) 200 (0)
4 103 (26) 84 (54) 195 (4) 199 (0) 200 (0) 200 (0)
4.500 48 (75) 38 (101) 164 (37) 199 (0) 190 (10) 200 (0)
5 26 (104) 15 (147) 83 (118) 194 (0) 75 (126) 200 (0)
5.500 21 (141) 12 (165) 31 (165) 178 (0) 6 (197) 200 (0)
6 9 (160) 7 (180) 8 (194) 165 (0) 1 (200) 200 (0)
7 3 (183) 2 (194) 2 (199) 119 (15) 1 (200) 200 (0)
8 5 (191) 4 (197) 1 (200) 91 (46) 0 (200) 200 (0)
10 1 (198) 1 (200) 0 (200) 21 (147) 1 (200) 188 (0)
12 1 (200) 1 (200) 2 (200) 8 (179) 2 (200) 166 (0)
16 2 (200) 2 (200) 2 (200) 2 (200) 2 (200) 83 (46)

With their θ, the lookups collapse between σ = 5 and 5.5 at D = 2048 and at D = 8192 alike, just past the predicted 4.9, and the fallback count shows why. With the chance floor, D = 8192 still gets 200 of 200 at σ = 8. At D = 512 the two rows agree, because there the item memory gives out first, and 5/√512 is close to 0.2 anyway.

The same holds one level up. Here is a 4-list read back with their reader, 20 seeds per cell:

σ D=512, theirs (theta=0.2) D=512, chance floor (theta=5/sqrt(D)) D=2048, theirs (theta=0.2) D=2048, chance floor (theta=5/sqrt(D)) D=8192, theirs (theta=0.2) D=8192, chance floor (theta=5/sqrt(D))
0 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20
1 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20
2 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20 20 / 20
3 14 / 20 14 / 20 20 / 20 20 / 20 20 / 20 20 / 20
4 0 / 20 0 / 20 17 / 20 20 / 20 20 / 20 20 / 20
5 0 / 20 0 / 20 0 / 20 18 / 20 0 / 20 20 / 20
6 0 / 20 0 / 20 0 / 20 15 / 20 0 / 20 20 / 20
8 0 / 20 0 / 20 0 / 20 0 / 20 0 / 20 20 / 20
10 0 / 20 0 / 20 0 / 20 0 / 20 0 / 20 18 / 20
12 0 / 20 0 / 20 0 / 20 0 / 20 0 / 20 11 / 20

And a whole program, ((LAMBDA (x) (CONS x (b c))) a), 10 seeds per cell. Its noise-free row already misses some seeds (E1 below):

σ D=512, theirs (theta=0.2) D=512, chance floor (theta=5/sqrt(D)) D=2048, theirs (theta=0.2) D=2048, chance floor (theta=5/sqrt(D)) D=8192, theirs (theta=0.2) D=8192, chance floor (theta=5/sqrt(D))
0 9 / 10 9 / 10 9 / 10 9 / 10 7 / 10 7 / 10
2 6 / 10 6 / 10 10 / 10 10 / 10 7 / 10 7 / 10
4 0 / 10 0 / 10 3 / 10 10 / 10 8 / 10 8 / 10
5 0 / 10 0 / 10 0 / 10 7 / 10 0 / 10 8 / 10
6 0 / 10 0 / 10 0 / 10 0 / 10 0 / 10 8 / 10
8 0 / 10 0 / 10 0 / 10 0 / 10 0 / 10 7 / 10
10 0 / 10 0 / 10 0 / 10 0 / 10 0 / 10 3 / 10

Their θ stops the program at σ = 5 for D = 2048 and 8192 alike. With the chance floor, D = 8192 still gets 8 of 10 at σ = 5 and 6. That is as many as at σ = 4, and more than the 7 of 10 at σ = 0, which E1 costs.

residuelisp keeps the 0.2 but compares it with an unnormalised similarity. A noisy pointer keeps similarity about 1 to its own key at any σ. What grows is the spread of chance matches, about √((1 + σ²)/(2D)). So the cutoff should give way at a σ that grows like √D. Here is the two-lambda program, 10 seeds per cell:

σ D=500 D=2000 D=8000
0 10 / 10 10 / 10 10 / 10
1 10 / 10 10 / 10 10 / 10
2 7 / 10 10 / 10 10 / 10
3 0 / 10 10 / 10 10 / 10
4 0 / 10 8 / 10 10 / 10
6 0 / 10 0 / 10 10 / 10
8 0 / 10 0 / 10 9 / 10
12 0 / 10 0 / 10 1 / 10
16 0 / 10 0 / 10 0 / 10
24 0 / 10 0 / 10 0 / 10
32 0 / 10 0 / 10 0 / 10

The last σ with all 10 runs right is 1 at D = 500, 3 at D = 2000 and 6 at D = 8000. It roughly doubles for each 4× in D, as √D predicts. So in residuelisp the fixed numbers do not set a cap independent of D. Its failures are loud: every failing run raised an exception (mostly InterpreterError) or hit my cleanup budget, and none returned a wrong value (raw table in the appendix). A fixed floor has a different cost there, in trap 4.

Trap 4: A shared field (the integer tag)

Applies? Not to holis, which has no records. It does apply to residuelisp, in a form I did not foresee. Every integer carries the same tag, int(x) = ζ(x) + int, which is the paper’s equation (11). Atoms count as eq? when their unnormalised similarity exceeds 0.2, and the tag alone contributes 1. So any two integers are equal.

Reproduced? Yes, noise-free at D = 1000, in 5 of 5 seeds (rows R1 below). The similarities behind it:

seeds sim(3,3) sim(3,5) sim(3,1000) sim(a,b) cos(3,5) cos(3,1000)
5 1.988 1.022 0.952 0.013 0.509 0.482

Their own tests compare an arithmetic result with the expected integer through this equality. So those assertions pass whatever integer the arithmetic returns. In their test’s pattern, the result of (+ 5 5) “equals” 10, 7, 0 and 999:

seed program equals(result, n) for n = 10, 7, 0, 999
0
(+ 5 5 )
10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t
1
(+ 5 5 )
10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t
2
(+ 5 5 )
10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t
3
(+ 5 5 )
10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t
4
(+ 5 5 )
10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t

Trap 5: A sum of tuples

Applies? No. Neither codebase builds superpositions of values, and neither has positional vectors, so there are no joint worlds to lose.

Further errata

Running the code turned up problems that my list does not cover. All are noise-free.

E1 (holis). CAR of an atom. To decide “is this head a lambda expression?”, their evaluator compares car(head) with LAMBDA. It does this even when head is an atom, and an atom has no CAR. car() then cleans up a meaningless probe and returns some stored item. When that item happens to match, the evaluator takes the wrong branch. Here is ((LAMBDA (x) (CONS x (b c))) a) over 100 seeds. The control makes car() answer NIL for anything that is not a stored pointer, and changes nothing else:

D variant wrong
512 theirs 7 / 100
512 car(atom) = NIL 0 / 100
2048 theirs 11 / 100
2048 car(atom) = NIL 0 / 100

Some of the wrong results:

D seed result
512 0
[["a", "b", "LAMBDA", "x", ["CONS", "a", ["b", "c"]]]]
512 34
[["a", "LAMBDA"]]
512 41
[["a", "b", "LAMBDA", "a", ["x", ["b", "c"]]]]
512 43
[["a", "b"]]
2048 2
[["a", "LAMBDA", ["c"], "LAMBDA"]]
2048 11
[["LAMBDA", "b", "c"]]
2048 21
[["a", 1]]
2048 46
[["a", "b", "LAMBDA", "lhs", "CONS"]]

What car(a) returns for the atom a, after the run, over the 100 seeds. It is spread over the whole vocabulary:

D car(a) → item ×seeds
512 NIL ×11, CONS ×9, b ×9, 1 ×9, lhs ×8, a ×7, φ ×6, F ×6, T ×5, rhs ×5, c ×5, DEFINE ×4, QUOTE ×3, LABEL ×3, x ×2, CDR ×2, EQ ×2, ATOM ×1, COND ×1, CAR ×1, LAMBDA ×1
2048 a ×14, lhs ×11, F ×9, T ×7, x ×7, c ×7, φ ×5, CONS ×5, NIL ×5, EQ ×5, DEFINE ×4, LAMBDA ×4, 1 ×4, CAR ×3, QUOTE ×2, LABEL ×2, b ×2, CDR ×2, rhs ×1, ATOM ×1

E2, E3 (holis). CAR does not evaluate its argument (nor does CDR; CONS does not either, but their evaluator re-evaluates the new cell, which hides that for plain arguments and loops on quoted ones; see the errata), and LABEL is dispatched but raises. Their paper says it leaves LABEL out, so E3 is a leftover, not a broken claim:

program D results over seeds
(CAR (CDR (a b c)))
512 ["CDR"] ×5
(LABEL f (LAMBDA (x) x))
512 TypeError: vsa_env.<locals>.label() got an unexpected keyword argument 'ind' ×5

E5 (holis), a question. For a list whose head is no function, the code evaluates the tail as a call, where the paper says to leave the list unevaluated. So (a CAR (b c)) gives the pair (a . b). Their programs rely on this, so it may well be intended (see the errata).

E4 (holis). The item memory adds 100 rows per item past 100. SimpleCleanUp.memorize appends 100 empty rows once it is full, but it never raises its capacity. So past 100 items, every new item appends another 100 rows. An empty row scores 0, so it can win a lookup only when every stored row scores below 0. Mostly it costs memory and time. It is why the fizzbuzz calls above take minutes, and why my noise sweep at D = 8192 needed smaller memories to stay under 8 GB. residuelisp’s CleanupMemory does raise its capacity.

items stored matrix rows MB at D = 512
100 100 0.4
200 10100 41.4
400 30100 123.3
800 70100 287.1
after their fizzbuzz DEFINEs, D = 512 items stored matrix rows
seed 0 635 53600

R2–R5 (residuelisp).

  • R2: and tests its left argument twice and never the right one. Structural eq? on lists joins car and cdr with and, so it compares only the cars.
  • R3: if does not evaluate its condition. The condition is the unevaluated form, a pointer, which is neither nil nor #f.
  • R4: the decoder searches 0..199, while the moduli cover 0..1154.
  • R5: see the lexer table below.

D = 1000, 5 seeds:

erratum program expected results
R1
(eq? 3 5 )
#f #t ×5
R1
(eq? 3 1000 )
#f #t ×5
R1 control
(eq? 3 3 )
#t #t ×5
R2
(and #t #f)
#f #t ×5
R2
(and #t nil)
nil or #f #t ×5
R2
(eq? (cons a (cons b nil)) (cons a (cons c nil)))
#f #t ×5
R2 control
(eq? (cons a (cons b nil)) (cons a (cons b nil)))
#t #t ×5
R3
(if (eq? a b) yes no)
no yes ×5
R3
(if (and #f #f) yes no)
no yes ×5
R3 control
(if #f yes no)
no no ×5
R4
(+ 150 100 )
250 37 ×1, 15 ×1, 119 ×1, 77 ×1, 61 ×1
R4
(+ 199 1 )
200 173 ×1, 32 ×1, 16 ×2, 61 ×1
R4
(- 3 5 )
-2 (1153 mod 1155) 173 ×1, 15 ×1, 121 ×1, 127 ×1, 46 ×1
R4 control
(+ 100 99 )
199 199 ×5

R5: the lexer drops a ) that directly follows an integer, so nested arithmetic parses wrongly. My residuelisp programs put a space there.

source tokens result (seed 0)
(g 1)
( g 1
–
(g 1 )
( g 1 )
–
(+ (+ 1 2) 3)
( + ( + 1 2 3
InterpreterError: Segmentation fault in car hehe
(+ (+ 1 2 ) 3 )
( + ( + 1 2 ) 3 )
6

Errata for the authors

This section is for Tomkins-Flanagan, Kelly and Hanley, and for anyone who builds on their code. Each item says where the defect is, shows a minimal program, gives the cause in one sentence and suggests a small fix. Line numbers are for residuelisp at 09864c7. holis is one notebook, so for it I name the cell and the function, at db208d7. Most items are slips that a proof of concept is allowed to have. They are listed so they are quick to fix.

Every fix below is tested. My harness applies it as a runtime patch to their code and runs the erratum’s minimal programs. Every fix except trap 3’s then also runs the noise-free control programs from the start of this page again (holis_fixes.py, residuelisp_fixes.py; E1’s own control is in holis_errata.py). The trap 3 fix is tested only in holis_thresholds.py: lookups, reads and the LAMBDA program. Where a suggested fix turned out wrong or incomplete, I say so. The fix shown is always the one I ran.

The order is by harm: wrong answers without any noise first, then robustness under noise, then memory and leftovers. The counts below come from the same JSON files as the rest of the page:

item code program should give gives (×runs) wrong wrong with the fix
R1 (trap 4) residuelisp
(eq? 3 5 )
#f #t ×5 5 / 5 0 / 5
R2 residuelisp
(and #t #f)
#f #t ×5 5 / 5 0 / 5
R2 residuelisp
(eq? (cons a (cons b nil)) (cons a (cons c nil)))
#f #t ×5 5 / 5 0 / 5
R3 residuelisp
(if (eq? a b) yes no)
no yes ×5 5 / 5 0 / 5
E1 holis
((LAMBDA (x) (CONS x (b c))) a)
(a b c) varies, e.g. [["a", "LAMBDA", ["c"], "LAMBDA"]] (D = 2048, seed 2) 7 / 100 at D = 512, 11 / 100 at D = 2048 0 / 100 at D = 512, 0 / 100 at D = 2048 on this program; breaks every DEFINE, not recommended
R4 residuelisp
(+ 150 100 )
250 37 ×1, 15 ×1, 119 ×1, 77 ×1, 61 ×1 5 / 5 0 / 5
R5 residuelisp
(+ (+ 1 2) 3)
6 InterpreterError: Segmentation fault in car hehe seed 0 (lexer, no randomness) 0 / 5
trap 2 holis
(CAR (x . lhs))
x D = 512: rhs ×22, x ×18; D = 2048: rhs ×21, x ×19; D = 8192: x ×20, rhs ×20 22 / 40 at D = 512, 21 / 40 at D = 2048, 20 / 40 at D = 8192 0 / 40 at D = 512, 0 / 40 at D = 2048, 0 / 40 at D = 8192
E2 holis
(CAR (CDR (a b c)))
b ["CDR"] ×5 5 / 5 0 / 5 at D = 512, 0 / 5 at D = 2048
trap 3 holis CAR lookup on a stored 4-list, σ = 5.5, D = 8192 the stored head 6 right with θ = 0.2; 200 right with θ = 5/√D 194 / 200 0 / 200
E4 holis store 800 items in SimpleCleanUp about 800 rows 70100 rows, 287.1 MB at D = 512 – 800 rows
E3 holis
(LABEL f (LAMBDA (x) x))
the lambda TypeError: vsa_env.<locals>.label() got an unexpected keyword argument 'ind' ×5 5 / 5 0 / 5 (LABEL as DEFINE)

With each fix applied, the control programs from the start of the page still give the right answers. holis, 10 programs at D = 512 and 2048, 5 seeds each. The one program that misses a seed in most rows is the LAMBDA program of E1; only the rows with an E1 fix get it right:

fix programs right in every run runs right
theirs 18 / 20 (program × D) 98 / 100
E1 20 / 20 (program × D) 100 / 100
E1 admitting cells 20 / 20 (program × D) 100 / 100
trap 2 18 / 20 (program × D) 98 / 100
E2 18 / 20 (program × D) 98 / 100
E2 + unwrap QUOTE 18 / 20 (program × D) 98 / 100
E3 removed 18 / 20 (program × D) 98 / 100
E3 kept 18 / 20 (program × D) 98 / 100
E3 as DEFINE 18 / 20 (program × D) 98 / 100
E4 18 / 20 (program × D) 98 / 100
all but E1 (trap 2, E2 + unwrap QUOTE, E3 removed, E4) 18 / 20 (program × D) 98 / 100

residuelisp, its 15 control programs over 5 seeds:

fix programs right in every run runs right
theirs 15 / 15 75 / 75
R1 15 / 15 75 / 75
R2 15 / 15 75 / 75
R3 15 / 15 75 / 75
R4 15 / 15 75 / 75
R5 15 / 15 75 / 75
all 15 / 15 75 / 75

The holis control programs have no DEFINE, so I also ran their fizzbuzz helpers: their DEFINEs, then the five calls in one environment, D = 512, 5 seeds, each call stopped after 60 s. All these runs use the E4 fix, which changes no answer and turns minutes into seconds (E4 below). Runs right per call:

call E4 E1 + E4 E1 admitting cells + E4 E2 + unwrap QUOTE + E4
their fizzbuzz DEFINEs 5 / 5 5 / 5 5 / 5 5 / 5
countdown 3 1 5 / 5 0 / 5 4 / 5 5 / 5
divisible 3 3 5 / 5 0 / 5 4 / 5 4 / 5
divisible 4 3 5 / 5 0 / 5 4 / 5 4 / 5
and T F 5 / 5 0 / 5 5 / 5 4 / 5
take 2 (a b c) 5 / 5 0 / 5 4 / 5 4 / 5

What the wrong runs gave:

fix call results of the wrong runs
E1 + E4 and T F [["and", "T", "F"]] ×5
E2 + unwrap QUOTE + E4 and T F RecursionError ×1
E1 + E4 divisible 4 3 [["divisible", "divisible", ["countdown", "LAMBDA", ["x", "n"]], ["x", … ×1; [["divisible", [[[["T"]]]], [[["T"]]]]] ×1; [["divisible", "F"]] ×1; [["divisible", ["fb", "LAMBDA"], "fizzesbuzzes", "LAMBDA", ["n"], ["CO … ×2
E1 admitting cells + E4 divisible 3 3 stopped after 60 s ×1
E2 + unwrap QUOTE + E4 divisible 3 3 RecursionError ×1
E1 admitting cells + E4 take 2 (a b c) stopped after 60 s ×1
E2 + unwrap QUOTE + E4 take 2 (a b c) RecursionError ×1
E2 + unwrap QUOTE + E4 divisible 4 3 RecursionError ×1
E1 admitting cells + E4 divisible 4 3 stopped after 60 s ×1
E1 + E4 divisible 3 3 [["divisible", "divisible", ["countdown", "LAMBDA", ["x", "n"]], ["x", … ×1; [["divisible", [[["T"]]], [[["T"]]]]] ×1; [["divisible", "F"]] ×1; [["divisible", ["fb", "LAMBDA"], "fizzesbuzzes", "LAMBDA", ["n"], ["CO … ×2
E1 admitting cells + E4 countdown 3 1 stopped after 60 s ×1
E1 + E4 countdown 3 1 [["countdown", "divisible", ["countdown", "LAMBDA", ["x", "n"]], ["x", … ×1; [["countdown", [[["T"]]], ["T"]]] ×1; [["countdown", "F"]] ×1; [["countdown", ["fb", "LAMBDA"], "fizzesbuzzes", "LAMBDA", ["n"], ["CO … ×2
E1 + E4 take 2 (a b c) [["take", "LAMBDA", ["n"], [["LAMBDA", ["xx"], ["COND", [["EQ", "xx", … ×1; [["take", [["T"]], ["a", "b", "c"]]] ×1; [["take", "LAMBDA", ["b"], ["COND", [["EQ", ["a", "b", "c"], "T"], "CO … ×1; [["take", ["fb", ["a", "b", "c"]], "fizzesbuzzes", "a"]] ×2

Wrong answers without noise

R1 (residuelisp; my trap 4). Any two integers are eq?.

  • Where. src/language/interpreter.py, equals, lines 320–362; the atom comparison is line 331. The tag is added in src/language/encoding.py, encode_rhc_integer, lines 322–348.
  • Symptom. With RHCIntegers, (eq? 3 5 ) and (eq? 3 1000 ) give #t (table above). The trap 4 table shows the same for their test pattern: the result of (+ 5 5) “equals” 10, 7, 0 and 999. So test_rhc_add and test_rhc_sub in src/tests/test_interpret.py (lines 302–335) pass whatever the arithmetic returns.
  • Cause. Every integer is ζ(x) + __int, and the shared tag alone gives a similarity of about 1, far above the 0.2 floor.
  • Fix. Compare integers without their tag, as rhc_add already does. Normalising the similarity is not enough: the cosine of 3 and 5 is still about 0.5 (trap 4 table). In the “both atomic” branch of equals, before its return ( on line 329:
        tag = enc_env.codebook["__int"]
        if is_true(check_int(lhs, enc_env), enc_env) and is_true(check_int(rhs, enc_env), enc_env):
            lhs, rhs = lhs - tag, rhs - tag

In the tests, compare the decoded value instead. Then a wrong sum fails:

assert decode(value, enc_env, eval_env) == "3"
  • Status. Tested, as shown. Before and after, D = 1000, 5 seeds:
program should give theirs R1
(eq? 3 5 )
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(eq? 3 1000 )
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(eq? 3 3 )
#t 5 / 5 right: #t ×5 5 / 5 right: #t ×5
(eq? (+ 1 2 ) 3 )
#t 5 / 5 right: #t ×5 5 / 5 right: #t ×5
(eq? (+ 1 2 ) 4 )
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(eq? a 3 )
#f 5 / 5 right: #f ×5 5 / 5 right: #f ×5

Their test pattern now tells a right sum from a wrong one. The result of (+ 5 5 ) compared with 10, 7, 0 and 999, through equals and through the decoded value:

fix seed equals(result, n) for n = 10, 7, 0, 999 decode(result)
theirs 0 10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t 10
theirs 1 10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t 10
theirs 2 10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t 10
theirs 3 10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t 10
theirs 4 10 ×#t, 7 ×#t, 0 ×#t, 999 ×#t 10
R1 0 10 ×#t, 7 ×#f, 0 ×#f, 999 ×#f 10
R1 1 10 ×#t, 7 ×#f, 0 ×#f, 999 ×#f 10
R1 2 10 ×#t, 7 ×#f, 0 ×#f, 999 ×#f 10
R1 3 10 ×#t, 7 ×#f, 0 ×#f, 999 ×#f 10
R1 4 10 ×#t, 7 ×#f, 0 ×#f, 999 ×#f 10

And their own test_rhc_add and test_rhc_sub sources, checked against the right answer 3 and a wrong answer 4, counted over the seeds:

source fix assert passes for 3 assert passes for 4 decode gives 3
(+ 1 2)
theirs 5 / 5 5 / 5 5 / 5
(- 5 2)
theirs 5 / 5 5 / 5 5 / 5
(+ 1 2)
R1 5 / 5 0 / 5 5 / 5
(- 5 2)
R1 5 / 5 0 / 5 5 / 5
(+ 1 2)
all 5 / 5 0 / 5 5 / 5
(- 5 2)
all 5 / 5 0 / 5 5 / 5

With the fix their assertion fails for 4, as it should. The decoded value was right all along. The control programs still pass (the regression table above).

R2 (residuelisp). and never looks at its second argument.

  • Where. src/language/interpreter.py, _and_, lines 286–289. equals uses it at line 360 to join the car and cdr comparisons.
  • Symptom. (and #t #f) gives #t. Structural eq? compares only the cars, so (a b) and (a c) are eq? (table above).
  • Cause. The second clause tests lhs again instead of rhs.
  • Fix. Line 288:
        is_nil(rhs, enc_env) or is_false(rhs, enc_env)

Their test_and and test_and_comp only use true arguments, so a test of (and #t #f) would have caught it.

  • Status. Tested, as shown. Structural eq? now compares the cdrs too, and the control programs still pass:
program should give theirs R2
(and #t #f)
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(and #t nil)
#f or nil 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(and #t #t)
#t 5 / 5 right: #t ×5 5 / 5 right: #t ×5
(and #f #t)
#f 5 / 5 right: #f ×5 5 / 5 right: #f ×5
(eq? (cons a (cons b nil)) (cons a (cons c nil)))
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(eq? (cons a (cons b nil)) (cons a (cons b nil)))
#t 5 / 5 right: #t ×5 5 / 5 right: #t ×5

R3 (residuelisp). if does not evaluate its condition.

  • Where. src/language/interpreter.py, if_, lines 427–461; the condition is taken at line 447 and tested at line 458.
  • Symptom. (if (eq? a b) yes no) gives yes. Any compound condition takes the consequent. A literal #f works, which is why test_if_consequent and test_if_alternate pass.
  • Cause. The condition is the unevaluated form, a pointer, and a pointer is neither nil nor #f.
  • Fix. Line 447:
    condition = evaluate(car(rand, enc_env, eval_env), enc_env, eval_env)
  • Status. Tested, as shown. The literal conditions give the same answers as before, and the control programs still pass:
program should give theirs R3
(if (eq? a b) yes no)
no 0 / 5 right: yes ×5 5 / 5 right: no ×5
(if (eq? a a) yes no)
yes 5 / 5 right: yes ×5 5 / 5 right: yes ×5
(if (and #f #f) yes no)
no 0 / 5 right: yes ×5 5 / 5 right: no ×5
(if (and #t #t) yes no)
yes 5 / 5 right: yes ×5 5 / 5 right: yes ×5
(if #f yes no)
no 5 / 5 right: no ×5 5 / 5 right: no ×5
(if #t yes no)
yes 5 / 5 right: yes ×5 5 / 5 right: yes ×5

E1 (holis). CAR of an atom returns a random item.

  • Where. Code cell 2 (“Lisp environment definitions”), vsa_env, the inner car. It is called on non-pointers in three places: ev’s “is head a lambda expression?” (car(head), where head can be an atom), ev’s “is head a defined function?” (car(echo), where echo is a cell bundle from the function memory), and ev_until_done’s head = car(e) on every result.
  • Symptom. ((LAMBDA (x) (CONS x (b c))) a) gives a wrong list in some seeds (table above, and the E1 tables under “Further errata”).
  • Cause. For a probe that matches no pointer, SimpleAssoc.recall returns the probe itself, so car unbinds lhs from an atom and cleans up noise, which lands on an arbitrary stored item.
  • Tried, not recommended. A guard that answers NIL for anything that is not a stored pointer fixes the LAMBDA program but breaks every DEFINE’d function (see Status); do not apply it as is. Raising instead, as residuelisp’s car does (line 226), would be worse here: ev_until_done calls car on every result, so every program with an atom as its value, DEFINE included, would raise.
def car(c, ind=''):
    if c not in assoc.A: return env['NIL']
    return mem.recall(assoc.recall(c) * ~env["lhs"])
  • Status. Tested; breaks DEFINE. On the LAMBDA program, wrong results go to 0 of 100 at D = 512 and 2048 (E1 table), and the control programs all pass (regression table above). But it breaks every DEFINE’d function (the fizzbuzz table above, column E1 + E4): their calls come back unevaluated. Their function memory holds cell bundles, not pointers, and ev finds a function by car of such a bundle, which the guard now answers with NIL. Admitting stored cells too (c in assoc.A or c in assoc.A.reverse) brings DEFINE back, but in one of the 5 seeds four of the five calls then run past 60 s (column E1 admitting cells + E4). So I have no fix for E1 that I can recommend. The cause is that their code uses one car both on pointers and on bundles. A fix would give the function memory its own lookup, and let SimpleAssoc.recall report a miss instead of returning the probe.

R4 (residuelisp). The decoder stops at 199.

  • Where. src/language/interpreter.py, decode_rhc, lines 1253–1271; the range is line 1265.
  • Symptom. (+ 150 100 ) gives a different wrong number in each seed (table above). So do (+ 199 1 ) and (- 3 5 ).
  • Cause. The argmax runs over the codes of 0..199 (plus literals seen), while the moduli 3, 5, 7, 11 cover 0..1154.
  • Fix. Lines 1265–1267. RHC.moduli is the moduli the encoder used; enc_env.moduli is empty by default. RHC.z_ms are the roots the encoder used, so each code is built as RHC.encode builds it:
    for i in range(int(np.prod(RHC.moduli))):   # 0..1154
        if i not in codebook:
            codebook[i] = np.prod([z_m ** i for z_m in RHC.z_ms], axis=0)

Results then wrap modulo 1155, so (- 3 5 ) decodes as 1153. Map the upper half to negatives if that is wanted.

  • Status. Tested, as shown. My first version called RHC.encode(enc_env.dim, i, RHC.moduli).data in the loop. It gave the same answers, but each call builds an RHC object whose constructor recomputes the four roots, so one decode took about 20 s at D = 1000. The version above takes well under a second:
program should give theirs R4
(+ 150 100 )
250 0 / 5 right: 37 ×1, 15 ×1, 119 ×1, 77 ×1, 61 ×1 5 / 5 right: 250 ×5
(+ 199 1 )
200 0 / 5 right: 173 ×1, 32 ×1, 16 ×2, 61 ×1 5 / 5 right: 200 ×5
(- 3 5 )
1153 0 / 5 right: 173 ×1, 15 ×1, 121 ×1, 127 ×1, 46 ×1 5 / 5 right: 1153 ×5
(+ 1000 154 )
1154 0 / 5 right: 106 ×1, 81 ×1, 130 ×1, 173 ×1, 1 ×1 5 / 5 right: 1154 ×5
(+ 100 99 )
199 5 / 5 right: 199 ×5 5 / 5 right: 199 ×5

R5 (residuelisp). The lexer drops a ) right after a number.

  • Where. src/syntax/parser.py, _digit, lines 121–127.
  • Symptom. (+ (+ 1 2) 3) lexes as ( + ( + 1 2 3 and fails. With a space before each ) it gives 6 (table above, and the lexer table under “Further errata”).
  • Cause. The loop calls _next to look at the character after the digits, so that character is consumed and lost.
  • Fix. Peek first, as _key_or_ident does:
def _digit(d, conts):
    ds = [d]
    while (curr := _peek(conts)) is not None and curr.isdigit():
        ds.append(_next(conts))
    return Token("".join(ds), TokenKind.Int)
  • Status. Tested, as shown. The tokens:
fix source tokens
theirs
(g 1)
( g 1
theirs
(+ (+ 1 2) 3)
( + ( + 1 2 3
R5
(g 1)
( g 1 )
R5
(+ (+ 1 2) 3)
( + ( + 1 2 ) 3 )
program should give theirs R5
(+ (+ 1 2) 3)
6 0 / 5 right: InterpreterError: Segmentation fault in car hehe ×5 5 / 5 right: 6 ×5
(+ 1 2)
3 5 / 5 right: 3 ×5 5 / 5 right: 3 ×5
(car (cons 1 2))
1 5 / 5 right: 1 ×5 5 / 5 right: 1 ×5
(+ (+ 1 2 ) 3 )
6 5 / 5 right: 6 ×5 5 / 5 right: 6 ×5
fix control programs without the space before ), right in every run
theirs 15 / 15
R5 15 / 15

The control programs pass with the fix, with and without the space before each ). They pass without the space even unfixed: a ) lost at the very end is forgiven by the parser. Only a lost ) inside the form breaks a program. That is also why their tests, which write (+ 1 2), never saw it.

With all five fixes applied together, the programs of R1 to R5 give:

programs right in every run runs right
27 27 135 / 135

And programs that need two fixes at once:

program should give theirs all
(if (eq? 3 5) yes no)
no 0 / 5 right: InterpreterError: ERROR: Conditional expression requires `CONSEQUENT` and `ALTERNATE`, see documen ×5 5 / 5 right: no ×5
(if (eq? (+ 2 3) 5) yes no)
yes 0 / 5 right: InterpreterError: ERROR: Conditional expression requires `CONSEQUENT` and `ALTERNATE`, see documen ×5 5 / 5 right: yes ×5
(eq? (cons 1 (cons 2 nil)) (cons 1 (cons 3 nil)))
#f 0 / 5 right: #t ×5 5 / 5 right: #f ×5
(eq? (cons 1 (cons 2 nil)) (cons 1 (cons 2 nil)))
#t 5 / 5 right: #t ×5 5 / 5 right: #t ×5
(+ (+ 150 100) 1)
251 0 / 5 right: InterpreterError: Segmentation fault in car hehe ×5 5 / 5 right: 251 ×5

Trap 2 (holis). Programs can name the role vectors.

  • Where. Code cell 2, vsa_env, the reserved list. It puts lhs, rhs and φ into env['lex'] and the item memory, and make_atom returns the lexicon entry for any name a program uses.
  • Symptom. (CAR (x . lhs)) gives rhs in about half the runs, at every D (table above). read and car can also return lhs, rhs or φ as atoms (the car(a) spread under E1).
  • Cause. The symbol lhs is the role vector itself, and binding commutes, so unbinding lhs from rhs⊗lhs gives rhs as strongly as unbinding it from lhs⊗x gives x.
  • Fix. Keep the roles out of the lexicon and the item memory:
reserved = ['T', 'F', "NIL", 1]      # atoms a program may name
for role in ['lhs', 'rhs', 'φ']:
    env[role] = HRR(N=dim)           # roles only, not in env['lex'] or mem
  • Status. Tested. My harness lets vsa_env draw the roles as before and then deletes them from env['lex'] and their rows from the item memory. That is the state the change above gives, with the same random draws. lhs in a program is then a fresh atom. The trap 2 programs, 40 seeds per row:
program D should give theirs trap 2
(CAR (x . lhs))
512 x 18 / 40 right: rhs ×22, x ×18 40 / 40 right: x ×40
(CAR (x . lhs))
2048 x 19 / 40 right: rhs ×21, x ×19 40 / 40 right: x ×40
(CAR (x . lhs))
8192 x 20 / 40 right: x ×20, rhs ×20 40 / 40 right: x ×40
(CDR (rhs . x))
512 x 17 / 40 right: lhs ×23, x ×17 40 / 40 right: x ×40
(CDR (rhs . x))
2048 x 22 / 40 right: x ×22, lhs ×18 40 / 40 right: x ×40
(CDR (rhs . x))
8192 x 22 / 40 right: x ×22, lhs ×18 40 / 40 right: x ×40
(CAR (x . rhs))
512 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (x . rhs))
2048 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (x . rhs))
8192 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CDR (lhs . x))
512 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CDR (lhs . x))
2048 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CDR (lhs . x))
8192 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (x . y))
512 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (x . y))
2048 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (x . y))
8192 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CAR (y . x))
512 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CAR (y . x))
2048 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CAR (y . x))
8192 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CDR (x . y))
512 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CDR (x . y))
2048 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CDR (x . y))
8192 y 40 / 40 right: y ×40 40 / 40 right: y ×40
(CDR (y . x))
512 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CDR (y . x))
2048 x 40 / 40 right: x ×40 40 / 40 right: x ×40
(CDR (y . x))
8192 x 40 / 40 right: x ×40 40 / 40 right: x ×40

Nothing else reads the roles from env['lex']: the control programs give the same counts as without the fix (regression table above).

E2 (holis). CAR does not evaluate its argument.

  • Where. Code cell 2, vsa_env, caar, cdar and cons.
  • Symptom. (CAR (CDR (a b c))) gives CDR. Lisp 1.5 gives b. (CDR (CDR (a b c))) gives ((a b c)) instead of (c) (table below).
  • Cause. caar(c) is car(car(c)): it takes the first element of the argument form instead of its value.
  • Fix, if Lisp 1.5 semantics are intended. Evaluate first, and read their QUOTE form as its argument:
def val(x, ind=''):
    v = ev_until_done(x, ind)
    if (car(v) == env['lex']['QUOTE']) > floor:   # (QUOTE (x)) -> x
        v = car(car(cdr(v)))
    return v
def caar(c, ind=''):  return car(val(car(c), ind))
def cdar(c, ind=''):  return cdr(val(car(c), ind))
def cons(ab, ind=''): return ccons(val(car(ab), ind), val(car(cdr(ab)), ind))
  • Status. Tested. I first suggested only the two ev_until_done lines, and that quote return its argument. Both were incomplete. Without the unwrapping, a quoted list comes back with its wrapper, (QUOTE ((a b c))). With quote returning its argument, ev_until_done evaluates the argument again, so (QUOTE (CDR a)) runs CDR on an atom (E1) and gives a random item. The version above leaves quote alone. Unquoted literal lists such as (a b c) keep working, because their evaluator keeps an atom that is no function, at the head and after it (a function name or a lambda after the head is applied instead, E5). CONS evaluates by accident in their code: the loop in ev_until_done evaluates the new cell again. With quoted arguments that loop never ends: their QUOTE returns (QUOTE (x)), and each pass wraps it once more, so (CONS (QUOTE a) (QUOTE (b c))) runs until my harness stops it (“budget”: more than 300 cons cells for one program). The two E2 lines alone keep the loop; both QUOTE variants end it. Over 5 seeds:
program D should give theirs E2 E2 + QUOTE returns its argument E2 + unwrap QUOTE
(CAR (CDR (a b c)))
512 ["b"] 0 / 5 right: ["CDR"] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5
(CAR (CDR (a b c)))
2048 ["b"] 0 / 5 right: ["CDR"] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5
(CDR (CDR (a b c)))
512 [["c"]] 0 / 5 right: [[["a", "b", "c"]]] ×5 5 / 5 right: [["c"]] ×5 5 / 5 right: [["c"]] ×5 5 / 5 right: [["c"]] ×5
(CDR (CDR (a b c)))
2048 [["c"]] 0 / 5 right: [[["a", "b", "c"]]] ×5 5 / 5 right: [["c"]] ×5 5 / 5 right: [["c"]] ×5 5 / 5 right: [["c"]] ×5
(CONS (CAR (a b)) (c d))
512 [["a", "c", "d"]] 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5
(CONS (CAR (a b)) (c d))
2048 [["a", "c", "d"]] 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5 5 / 5 right: [["a", "c", "d"]] ×5
(CAR (CDR (QUOTE (a b c))))
512 ["b"] 0 / 5 right: ["CDR"] ×5 0 / 5 right: [[["a", "b", "c"]]] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5
(CAR (CDR (QUOTE (a b c))))
2048 ["b"] 0 / 5 right: ["CDR"] ×5 0 / 5 right: [[["a", "b", "c"]]] ×5 5 / 5 right: ["b"] ×5 5 / 5 right: ["b"] ×5
(CAR (QUOTE (CDR a)))
512 ["CDR"] 0 / 5 right: ["QUOTE"] ×5 0 / 5 right: ["QUOTE"] ×5 1 / 5 right: ["F"] ×2, ["CDR"] ×1, ["NIL"] ×1, budget ×1 5 / 5 right: ["CDR"] ×5
(CAR (QUOTE (CDR a)))
2048 ["CDR"] 0 / 5 right: ["QUOTE"] ×5 0 / 5 right: ["QUOTE"] ×5 0 / 5 right: [["QUOTE", ["CDR", "a"]]] ×1, ["NIL"] ×1, ["a"] ×1, ["rhs"] ×1, RecursionError ×1 5 / 5 right: ["CDR"] ×5
(CAR (QUOTE (a b)))
512 ["a"] 0 / 5 right: ["QUOTE"] ×5 0 / 5 right: ["QUOTE"] ×5 5 / 5 right: ["a"] ×5 5 / 5 right: ["a"] ×5
(CAR (QUOTE (a b)))
2048 ["a"] 0 / 5 right: ["QUOTE"] ×5 0 / 5 right: ["QUOTE"] ×5 5 / 5 right: ["a"] ×5 5 / 5 right: ["a"] ×5
(CONS (QUOTE a) (QUOTE (b c)))
512 [["a", "b", "c"]] 0 / 5 right: budget ×5 0 / 5 right: budget ×5 5 / 5 right: [["a", "b", "c"]] ×5 5 / 5 right: [["a", "b", "c"]] ×5
(CONS (QUOTE a) (QUOTE (b c)))
2048 [["a", "b", "c"]] 0 / 5 right: budget ×5 0 / 5 right: budget ×5 5 / 5 right: [["a", "b", "c"]] ×5 5 / 5 right: [["a", "b", "c"]] ×5

Here E2 is the two lines alone (and CONS), and E2 + unwrap QUOTE is the fix shown. The control programs keep their counts with it (regression table above). Their fizzbuzz helpers do not all survive it (the fizzbuzz table above, column E2 + unwrap QUOTE + E4). In seed 0, divisible 3 3 runs into unbounded recursion when it follows countdown, and every later call in the same environment then fails as well, even (and T F): the failed call leaves new pointers in SimpleAssoc, and restoring only that memory makes (and T F) pass again (restoring the item memory does not). Raising Python’s recursion limit to 200 000 did not let it finish within 15 minutes. Which call fails first depends on what ran before it: divisible 3 3 passes on its own and after (and T F) but fails after countdown or take, and reseeding numpy’s random generator after countdown makes it pass. Every cons draws a fresh vector from that generator, so each call changes the vectors the next one gets. With E4 alone, the same order passes. (These orderings are side runs, not in the JSON files.) I suspect E1 sets off the first failure: their internal car runs on every call, and with E2 more atoms reach it. I did not confirm this, and with no working E1 fix (above) I could not test the pair.

A question about the semantics

E5 (holis). A question rather than an erratum: code and paper differ on lists whose head is no function.

  • Where. Code cell 2, vsa_env: the last line of ev, ccons(ev(head), ev(tail)), and in ev_until_done the branch ccons(head, ev_until_done(cdr(e))).
  • The paper says “the interpreter should leave the list including f as an unevaluated expression if f is not in the function namespace”; in its eq. (11) such an f gives cons(f, a).
  • The code evaluates the tail as one expression: ev evaluates the head and then the tail, and ev_until_done keeps the head and evaluates the tail (“otherwise, just leave it and evaluate the tail”). Since (x f y) is (x . (f y)), a function name or lambda after the head is then applied to what follows it: (a CAR (b c)) gives the pair (a . b) and (g (LAMBDA (x) x)) gives (g LAMBDA (x) x) (table below, which prints the pair (a . b) as ["a", "b"]). The E3 result with LABEL removed is this effect.
  • Their programs rely on it. fizzesbuzzes and fizzingbuzzing cons onto their recursive call as ((fb n) . (fizzesbuzzes (CAR n))), where both the head (fb n) and the tail are evaluated, and the recursive call in take, (CONS (CAR l) (take (CAR n) (CDR l))), runs only this way, because CONS does not evaluate its arguments (E2). To keep a function name in data, quote it: (QUOTE (a CAR (b c))) keeps the list inside its QUOTE wrapper (a side run).
  • The question. Is evaluating the tail meant as the language’s semantics? Then the paper could say so. The paper’s rule taken literally, returning the list as it is, would also stop (fb n) and (CAR (b c)) inside a list from running; I did not test it. A third option keeps the head evaluation and evaluates each element of the tail on its own. This is what it takes:
def each(t, f, ind=''):
    if (atomic(t) == env['T']) > floor:
        return t
    return ccons(f(car(t), ind=ind), each(cdr(t), f, ind=ind))

inside vsa_env, and end ev with return ccons(ev(head, ind=ind+' '), each(tail, ev, ind+' ')) and that branch of ev_until_done with return ccons(head, each(cdr(e), ev_until_done, ind)). It needs E2, and fizzesbuzzes and fizzingbuzzing would have to use CONS.

  • Tested (holis_tail.py, as a wrapper around their ev and ev_until_done; column elementwise). Results over 5 seeds:
program D theirs elementwise
(a CAR (b c))
512 [["a", "b"]] ×5 [["a", "CAR", ["b", "c"]]] ×5
(a CAR (b c))
2048 [["a", "b"]] ×5 [["a", "CAR", ["b", "c"]]] ×5
(g (LAMBDA (x) x))
512 [["g", "LAMBDA", ["x"], "x"]] ×5 [["g", ["LAMBDA", ["x"], "x"]]] ×5
(g (LAMBDA (x) x))
2048 [["g", "LAMBDA", ["x"], "x"]] ×4, [["LAMBDA", ["x"], "x"]] ×1 [["g", ["LAMBDA", ["x"], "x"]]] ×4, [["LAMBDA", ["x"], "x"]] ×1
(a (LAMBDA (x) x) b)
512 [["a", "b"]] ×5 [["a", ["LAMBDA", ["x"], "x"], "b"]] ×5
(a (LAMBDA (x) x) b)
2048 [["a", "b"]] ×4, budget ×1 [["a", ["LAMBDA", ["x"], "x"], "b"]] ×4, budget ×1
(a (CAR (b c)) d)
512 [["a", "b", "d"]] ×5 [["a", "b", "d"]] ×5
(a (CAR (b c)) d)
2048 [["a", "b", "d"]] ×5 [["a", "b", "d"]] ×5
(a b c)
512 [["a", "b", "c"]] ×5 [["a", "b", "c"]] ×5
(a b c)
2048 [["a", "b", "c"]] ×5 [["a", "b", "c"]] ×5

The single odd results at D = 2048 happen in both columns, the same in each; I did not trace them. With LABEL removed (E3), (LABEL f (LAMBDA (x) x)) comes back as written with the alternative, in 4 of 5 seeds:

program D E3 removed E3 removed + elementwise
(LABEL f (LAMBDA (x) x))
512 [["LABEL", "f", "LAMBDA", ["x"], "x"]] ×4, [["LAMBDA", "LAMBDA", "x"]] ×1 [["LABEL", "f", ["LAMBDA", ["x"], "x"]]] ×4, [["LAMBDA", ["f"], "f"]] ×1

The control programs, with their code and with the alternative:

variant right
theirs 98 / 100
elementwise 100 / 100

The two misses with their code are the LAMBDA program of E1. There car of an atom reads back LAMBDA (E1), and the atom sits at the head of a call only because a tail such as (b c) is evaluated as a call. The alternative removes that path in these seeds; the misread still happens but does no harm. It does not replace the E1 guard: over 100 seeds the alternative still misses 2 at D = 512 and 4 at D = 2048, where the misread hits the lambda’s argument list, and the guard gets all 200 right (a side run).

Their fizzbuzz helpers, right per call over 5 seeds (D = 512, with E4). Alone, the alternative breaks take, which then returns its recursive call as data; with E2 it works again, and only seed 0 fails, as with E2 alone. I could not run fizzesbuzzes itself to the end: even with their code and E4, (fizzesbuzzes 3) did not finish within 120 s (a side run).

call E4 elementwise + E4 E2 + unwrap QUOTE + elementwise + E4
their fizzbuzz DEFINEs 5 / 5 5 / 5 5 / 5
countdown 3 1 5 / 5 5 / 5 5 / 5
divisible 3 3 5 / 5 5 / 5 4 / 5
divisible 4 3 5 / 5 5 / 5 4 / 5
and T F 5 / 5 5 / 5 4 / 5
take 2 (a b c) 5 / 5 0 / 5 4 / 5

So keeping list elements is a design change, not a fix: it needs E2 and rewritten programs.

Robustness under noise

Trap 3 (holis). A fixed θ = 0.2 in the pointer memory.

  • Where. Code cell 2, SimpleAssoc.__init__ (theta=0.2); vsa_env creates it with the default.
  • Symptom. Under probe noise, lookups fail past σ ≈ 4.9 at any D (table above, and the trap 3 tables).
  • Cause. A noisy probe has cosine 1/√(1 + σ²) to its key, which falls below 0.2 at σ = √24 whatever D is, while chance cosines shrink like 1/√D.
  • Fix. Set θ relative to chance:
assoc = SimpleAssoc(lexicon=env['lex'], theta=5 / np.sqrt(dim))
  • Status. Tested: at D = 8192 lookups stay at 200 of 200 up to σ = 8, and the LAMBDA program keeps 8 of 10 at σ = 5 and 6 (trap 3 tables). At D = 512 it changes nothing, since 5/√512 ≈ 0.22. I did not change the evaluator’s other 0.2 floors, and I did not run the fix on the other control programs. Without noise the LAMBDA program gives the same counts as with θ = 0.2 (9 of 10 at D = 512 and 2048, 7 of 10 at D = 8192).

Memory and leftovers

E4 (holis). The item memory adds 100 rows per item past 100.

  • Where. Code cell 2, SimpleCleanUp.memorize.
  • Symptom. 800 stored items take 70 100 rows (table above, and the E4 tables). Their fizzbuzz runs take minutes because of this.
  • Cause. When the matrix is full it appends k empty rows but never raises m, so every later item appends another k rows.
  • Fix. One line, as residuelisp’s CleanupMemory.memorize does (src/language/encoding.py, line 90):
        if self.i >= self.m:
            self.M = np.concatenate([self.M, np.zeros((self.k, self.n))], axis=0)
            self.m += self.k
  • Status. Tested, as shown. Rows of the matrix after storing n items:
items stored rows, theirs rows, with the fix MB at D = 512, theirs MB, with the fix
100 100 100 0.4 0.4
200 10100 200 41.4 0.8
400 30100 400 123.3 1.6
800 70100 800 287.1 3.3

The control programs keep their counts (regression table above). The fizzbuzz helpers give the right answers in all 5 seeds (fizzbuzz table above, column E4), and the time per call drops from minutes to seconds. Seed 0, against the control at the top of the page:

call seconds, theirs seconds, with the fix
their fizzbuzz DEFINEs 5.9 0.0
countdown 3 1 16.6 0.4
divisible 3 3 83.1 1.5
divisible 4 3 767.8 3.1
and T F 51.7 0.3
take 2 (a b c) 241.2 2.8

So here the empty rows changed no result. They only cost time.

E3 (holis). LABEL is a leftover. Their paper says it leaves LABEL out, so this breaks no claim.

  • Where. Code cell 2, vsa_env: 'LABEL' in basic_fs and in env.update, and the inner label.
  • Symptom. (LABEL f (LAMBDA (x) x)) raises a TypeError.
  • Cause. ev calls every basic function with ind=, and label has no such parameter. Inside, mem.memorize(lbl, exp) passes two arguments to a one-argument method, so it would fail next.
  • Fix. Let LABEL enter the name the way DEFINE does, and return the lambda:
def label(lbl_exp, ind=''):
    define(lbl_exp, ind)
    return car(cdr(lbl_exp))

Unlike Lisp 1.5, the name is then global. Removing 'LABEL' from basic_fs and env.update, as the paper leaves it out, also stops the error, but LABEL is then never applied and the form comes back as a wrong list: their evaluator keeps the unknown head LABEL and splices the evaluated lambda in (E5), giving (LABEL f LAMBDA (x) x) in 4 of 5 seeds.

  • Status. Tested. I first suggested keeping their label with an ind='' parameter and mem.memorize(lbl). That stops the error, but recursion still fails: their helper ll replaces the label only at the top level of the body, never inside it. With LABEL removed, it is never applied and the form comes back as a wrong list. D = 512, 5 seeds:
program D should give theirs E3 removed E3 kept E3 as DEFINE
(LABEL f (LAMBDA (x) x))
512 [["LAMBDA", ["x"], "x"]] 0 / 5 right: TypeError: vsa_env.<locals>.label() got an unexpected keyword argument 'ind' ×5 0 / 5 right: [["LABEL", "f", "LAMBDA", ["x"], "x"]] ×4, [["LAMBDA", "LAMBDA", "x"]] ×1 5 / 5 right: [["LAMBDA", ["x"], "x"]] ×5 5 / 5 right: [["LAMBDA", ["x"], "x"]] ×5
((LABEL f (LAMBDA (x) x)) a)
512 ["a"] 0 / 5 right: TypeError: vsa_env.<locals>.label() got an unexpected keyword argument 'ind' ×5 0 / 5 right: [[["LABEL", "f", "LAMBDA", ["x"], "x"], "a"]] ×4, [["LAMBDA", [["LAMBDA", ["x"], "x"]], "x"]] ×1 5 / 5 right: ["a"] ×5 5 / 5 right: ["a"] ×5
((LABEL f (LAMBDA (x) (COND ((EQ x NIL) . done) (T . (f (CAR x)))))) ((NIL)))
512 ["done"] 0 / 5 right: TypeError: vsa_env.<locals>.label() got an unexpected keyword argument 'ind' ×5 0 / 5 right: [[["LABEL", "f", "LAMBDA", ["x"], ["COND", [["EQ", "x", "NIL"], "done"], ["T", "f", ["CAR", "x"]]]], [["NIL"]]]] ×5 0 / 5 right: [["f", ["NIL"]]] ×5 5 / 5 right: ["done"] ×5

E3 kept is the first suggestion, E3 as DEFINE the version above. Every variant keeps the control programs’ counts (regression table above). The combined run there, all but E1, uses removal; the DEFINE version ran with the controls on its own.

What did not reproduce

  • Trap 1 does not apply to either codebase as written: both use pointer cells. In my paper mode of holis it shows up as a noise-free failure, but the loss of noise tolerance did not show in my read test.
  • Trap 2 shows up only through role names leaking into holis’s symbol table. The (x . y) / (y . x) controls are 40 of 40 correct.
  • Trap 3 does not reproduce in residuelisp in the form “a cap independent of D”, because its similarity is unnormalised (see the table above).
  • Trap 5 does not apply.

Summary

trap holis (2024) residuelisp (2025)
1 superposed cells code: n/a (pointers), paper mode: noise-free failure reproduced, noise margin not n/a (pointers)
2 commuting bind reproduced via role symbols n/a
3 fixed thresholds reproduced not reproduced (the cliff moves with √D)
4 shared field n/a reproduced (integer tag)
5 sum of tuples n/a n/a
new E1 car of an atom, E2, E3, E4 memory growth; E5 a question (list tails) R2 and, R3 if, R4 decoder range, R5 lexer

Appendix

A. Commands

cd repro/holis
python3 -m venv .venv          # Python 3.14.7
.venv/bin/pip install numpy more-itertools
export OPENBLAS_NUM_THREADS=1
ulimit -v 8000000
.venv/bin/python holis_control.py
.venv/bin/python holis_errata.py
.venv/bin/python holis_commute.py
.venv/bin/python holis_thresholds.py
.venv/bin/python holis_cells.py
.venv/bin/python residuelisp_control.py
.venv/bin/python residuelisp_errata.py
.venv/bin/python residuelisp_thresholds.py
.venv/bin/python holis_fixes.py
.venv/bin/python holis_tail.py
.venv/bin/python residuelisp_fixes.py

Each script writes the JSON file of the same name, which this page reads. The loader upstream.py fetches holis at db208d782863febfb50ec46b25a3700369f5cfea and residuelisp at 09864c74f1a35680a0008e59d7ab2571269a986f into a gitignored cache.

B. Settings, per file

file upstream python / numpy settings
holis_control.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"D":[512,2048],"seeds":[0,1,2,3,4],"fizzbuzz_D":512,"fizzbuzz_seed":0,"sigma":0}
holis_errata.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"program":"((LAMBDA (x) (CONS x (b c))) a)","seeds":100,"dims":[512,2048],"sigma":0}
holis_commute.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"dims":[512,2048,8192],"seeds":40,"sigma":0}
holis_thresholds.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"dims":[512,2048,8192],"sigma_read":[0,1,2,3,4,5,6,8,10,12],"lookup":"25 lists x 8 repeats per (D, sigma, mode), env seed 0","noise":"p + sigma*|p|\/sqrt(D)*N(0,I) before every SimpleCleanUp.recall and SimpleAssoc.recall","sigma_lookup":[0,1,2,3,4,4.5,5,5.5,6,7,8,10,12,16],"max_cells_program":300,"why_small":"their SimpleCleanUp grows by 100 zero rows per stored item past 100 rows (erratum E4); larger memories exceed 8 GB at D = 8192","read_seeds":20,"modes":["theirs (theta=0.2)","chance floor (theta=5\/sqrt(D))"],"sigma_program":[0,2,4,5,6,8,10],"program_seeds":10,"program":"((LAMBDA (x) (CONS x (b c))) a)"}
holis_cells.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"D":2048,"sigma_read":[0,0.5,1,1.5,2,3,4,5,6,8,10,12,16],"read_seeds":20,"program_seeds":10,"program_sigma":0,"max_cells_program":20000}
residuelisp_control.json https://github.com/hanleyc01/residuelisp @ 09864c7 3.14.7 / 2.5.4
{"D":1000,"vsa":"FHRR","integers":"RHCIntegers, moduli [3, 5, 7, 11]","seeds":[0,1,2,3,4],"sigma":0}
residuelisp_errata.json https://github.com/hanleyc01/residuelisp @ 09864c7 3.14.7 / 2.5.4
{"D":1000,"seeds":[0,1,2,3,4],"sigma":0}
residuelisp_thresholds.json https://github.com/hanleyc01/residuelisp @ 09864c7 3.14.7 / 2.5.4
{"program":"((lambda (x) ((lambda (y) (cons x (cons y nil))) n)) m)","max_steps":5000,"dims":[500,2000,8000],"sigmas":[0,1,2,3,4,6,8,12,16,24,32],"seeds":10,"noise":"p + sigma*|p|\/sqrt(D)*CN(0,I) before every CleanupMemory.recall and AssociativeMemory.deref"}
holis_fixes.json https://github.com/eilene-ftf/holis @ db208d7 3.14.7 / 2.5.4
{"seeds":5,"dims":[512,2048],"sigma":0,"commute_seeds":40,"commute_dims":[512,2048,8192],"fizzbuzz":{"D":512,"seeds":[0,1,2,3,4],"call_limit_s":60},"max_cells":300,"variants":["theirs","E1","E1 + E4","E1 admitting cells","E1 admitting cells + E4","4.2","E2","E2 + QUOTE returns its argument","E2 + unwrap QUOTE","E3 removed","E3 kept","E3 as DEFINE","E4","E2 + unwrap QUOTE + E4","all but E1 (4.2, E2 + unwrap QUOTE, E3 removed, E4)"]}
residuelisp_fixes.json https://github.com/hanleyc01/residuelisp @ 09864c7 3.14.7 / 2.5.4
{"D":1000,"seeds":[0,1,2,3,4],"sigma":0,"max_steps":5000,"fixes":["R1","R2","R3","R4","R5","all"]}

The holis notebook I ran has SHA-256 80eee8cdeed896fe1987bd9c391d3001c3e46f736227f8cac8f9784b2dd09175.

C. Harness excerpts (my code)

Probe noise, wrapped around their two cleanup methods (holis_common.py):

def _noisy(hrr):
    s = STATE["sigma"]
    if s <= 0:
        return hrr
    v = hrr.v
    n = STATE["rng"].normal(size=v.size) * (s * np.linalg.norm(v) / math.sqrt(v.size))
    return H.HRR(data=v + n)

H.SimpleCleanUp.recall = lambda self, hrr: _ORIG_MEM_RECALL(self, _noisy(hrr))
H.SimpleAssoc.recall   = ...  # the same, and counts theta fallbacks

The trap 3 control is one attribute, env["assoc"].theta = 5 / sqrt(D). The E1 control rebinds their inner car in the closures of vsa_env (holis_errata.py):

def car(c, ind=""):
    return orig(c) if c in assoc.A else nil

Paper mode, the trap 1 experiment, is two attributes on their pointer memory (holis_cells.py):

a.memorize = lambda probe, trace: (trace.unit(), trace.unit())
a.recall = lambda probe: probe

For residuelisp I also silence its debug output, stub the decode it calls inside log messages while a program runs, reset its class-level RHC state per seed, and stop a run after a budget of cleanups. holis runs stop after a budget of cons cells (300 per small program in holis_fixes.py and holis_thresholds.py, 20 000 in holis_cells.py) or, for the fizzbuzz calls, after 60 s. Under noise its tuple_to_list loops until car raises, which can be never.

D. Raw results

holis, trap 3 lookups (all rows):

D mode σ correct θ fallbacks
512 theirs (theta=0.2) 0 200 / 200 0
512 theirs (theta=0.2) 1 200 / 200 0
512 theirs (theta=0.2) 2 196 / 200 0
512 theirs (theta=0.2) 3 159 / 200 0
512 theirs (theta=0.2) 4 103 / 200 26
512 theirs (theta=0.2) 4.5 48 / 200 75
512 theirs (theta=0.2) 5 26 / 200 104
512 theirs (theta=0.2) 5.5 21 / 200 141
512 theirs (theta=0.2) 6 9 / 200 160
512 theirs (theta=0.2) 7 3 / 200 183
512 theirs (theta=0.2) 8 5 / 200 191
512 theirs (theta=0.2) 10 1 / 200 198
512 theirs (theta=0.2) 12 1 / 200 200
512 theirs (theta=0.2) 16 2 / 200 200
512 chance floor (theta=5/sqrt(D)) 0 200 / 200 0
512 chance floor (theta=5/sqrt(D)) 1 200 / 200 0
512 chance floor (theta=5/sqrt(D)) 2 196 / 200 0
512 chance floor (theta=5/sqrt(D)) 3 159 / 200 1
512 chance floor (theta=5/sqrt(D)) 4 84 / 200 54
512 chance floor (theta=5/sqrt(D)) 4.5 38 / 200 101
512 chance floor (theta=5/sqrt(D)) 5 15 / 200 147
512 chance floor (theta=5/sqrt(D)) 5.5 12 / 200 165
512 chance floor (theta=5/sqrt(D)) 6 7 / 200 180
512 chance floor (theta=5/sqrt(D)) 7 2 / 200 194
512 chance floor (theta=5/sqrt(D)) 8 4 / 200 197
512 chance floor (theta=5/sqrt(D)) 10 1 / 200 200
512 chance floor (theta=5/sqrt(D)) 12 1 / 200 200
512 chance floor (theta=5/sqrt(D)) 16 2 / 200 200
2048 theirs (theta=0.2) 0 200 / 200 0
2048 theirs (theta=0.2) 1 200 / 200 0
2048 theirs (theta=0.2) 2 200 / 200 0
2048 theirs (theta=0.2) 3 200 / 200 0
2048 theirs (theta=0.2) 4 195 / 200 4
2048 theirs (theta=0.2) 4.5 164 / 200 37
2048 theirs (theta=0.2) 5 83 / 200 118
2048 theirs (theta=0.2) 5.5 31 / 200 165
2048 theirs (theta=0.2) 6 8 / 200 194
2048 theirs (theta=0.2) 7 2 / 200 199
2048 theirs (theta=0.2) 8 1 / 200 200
2048 theirs (theta=0.2) 10 0 / 200 200
2048 theirs (theta=0.2) 12 2 / 200 200
2048 theirs (theta=0.2) 16 2 / 200 200
2048 chance floor (theta=5/sqrt(D)) 0 200 / 200 0
2048 chance floor (theta=5/sqrt(D)) 1 200 / 200 0
2048 chance floor (theta=5/sqrt(D)) 2 200 / 200 0
2048 chance floor (theta=5/sqrt(D)) 3 200 / 200 0
2048 chance floor (theta=5/sqrt(D)) 4 199 / 200 0
2048 chance floor (theta=5/sqrt(D)) 4.5 199 / 200 0
2048 chance floor (theta=5/sqrt(D)) 5 194 / 200 0
2048 chance floor (theta=5/sqrt(D)) 5.5 178 / 200 0
2048 chance floor (theta=5/sqrt(D)) 6 165 / 200 0
2048 chance floor (theta=5/sqrt(D)) 7 119 / 200 15
2048 chance floor (theta=5/sqrt(D)) 8 91 / 200 46
2048 chance floor (theta=5/sqrt(D)) 10 21 / 200 147
2048 chance floor (theta=5/sqrt(D)) 12 8 / 200 179
2048 chance floor (theta=5/sqrt(D)) 16 2 / 200 200
8192 theirs (theta=0.2) 0 200 / 200 0
8192 theirs (theta=0.2) 1 200 / 200 0
8192 theirs (theta=0.2) 2 200 / 200 0
8192 theirs (theta=0.2) 3 200 / 200 0
8192 theirs (theta=0.2) 4 200 / 200 0
8192 theirs (theta=0.2) 4.5 190 / 200 10
8192 theirs (theta=0.2) 5 75 / 200 126
8192 theirs (theta=0.2) 5.5 6 / 200 197
8192 theirs (theta=0.2) 6 1 / 200 200
8192 theirs (theta=0.2) 7 1 / 200 200
8192 theirs (theta=0.2) 8 0 / 200 200
8192 theirs (theta=0.2) 10 1 / 200 200
8192 theirs (theta=0.2) 12 2 / 200 200
8192 theirs (theta=0.2) 16 2 / 200 200
8192 chance floor (theta=5/sqrt(D)) 0 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 1 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 2 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 3 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 4 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 4.5 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 5 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 5.5 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 6 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 7 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 8 200 / 200 0
8192 chance floor (theta=5/sqrt(D)) 10 188 / 200 0
8192 chance floor (theta=5/sqrt(D)) 12 166 / 200 0
8192 chance floor (theta=5/sqrt(D)) 16 83 / 200 46

holis, trap 3 program runs, with what went wrong:

D mode σ correct errors wrong values
512 theirs (theta=0.2) 0 9 / 10 ["a", "b", "LAMBDA", "x", ["CONS", "a", ["b", "c"]]] ×1
512 theirs (theta=0.2) 2 6 / 10 ["x", "b", "c"] ×1, ["lhs", "b", "c"] ×1, ["c", "a", ["b", "c"]] ×1, ["a", "LAMBDA", ["x", ["b", "c"]], "c"] ×1
512 theirs (theta=0.2) 4 0 / 10 rhs ×1, [["x"], ["rhs", "lhs"]] ×1, [["rhs", ["LAMBDA", "x"]], ["rhs", ["x", 1]]] ×1, ["LAMBDA", "CONS", [["a"], [[["LAMBDA", "T"], ["x", "F"]]]]] ×1, [[[[[["F", ["T", ["CONS", "lhs"]], "x"], "LAMBDA"], ["NIL", ×1, ["LAMBDA", "rhs"] ×1, ["lhs", ["a", "LAMBDA"]] ×1, ["LAMBDA", 1, ["NIL", "lhs"]] ×1, [[["b", ["x"], "F"], "rhs"], "lhs"] ×1, ["F", "a"] ×1
512 theirs (theta=0.2) 5 0 / 10 [[["CONS", "rhs"], "ATOM"], "lhs"] ×1, ["lhs", "\u03c6"] ×1, LAMBDA ×1, a ×1, ["ATOM", "EQ"] ×1, ["F", ["NIL", "x"]] ×1, ["x", "\u03c6"] ×1, ["rhs", ["a", ["b", "lhs"]]] ×1, ["LAMBDA", ["rhs", "rhs"]] ×1, ["a", 1] ×1
512 theirs (theta=0.2) 6 0 / 10 ["ATOM", "T"] ×1, ["CONS", "a"] ×1, ["F", "EQ"] ×1, φ ×1, ["CDR", "CONS"] ×1, LAMBDA ×2, a ×1, ["x", "\u03c6"] ×1, c ×1
512 theirs (theta=0.2) 8 0 / 10 a ×1, c ×3, F ×1, ["NIL", "T"] ×1, [[1], ["NIL", "x"]] ×1, LAMBDA ×1, EQ ×1, x ×1
512 theirs (theta=0.2) 10 0 / 10 budget ×1 a ×2, c ×1, ["LAMBDA", "b"] ×1, lhs ×1, [["lhs", ["a", ["EQ", "T"]], "CONS"], [[[["ATOM", "a"], "LAM ×1, [[1], ["NIL", "x"]] ×1, LAMBDA ×1, x ×1
512 chance floor (theta=5/sqrt(D)) 0 9 / 10 ["a", "b", "LAMBDA", "x", ["CONS", "a", ["b", "c"]]] ×1
512 chance floor (theta=5/sqrt(D)) 2 6 / 10 ["x", "b", "c"] ×1, ["lhs", "b", "c"] ×1, ["c", "a", ["b", "c"]] ×1, ["a", "LAMBDA", ["x", ["b", "c"]], "c"] ×1
512 chance floor (theta=5/sqrt(D)) 4 0 / 10 [["CAR", "b", "F", "CONS", "F", "lhs", [["lhs", "rhs"], ["F" ×1, ["F", "EQ"] ×1, x ×1, ["lhs", "\u03c6"] ×1, ["a", ["LAMBDA", "x"]] ×1, ["LAMBDA", "c"] ×1, a ×1, ["LAMBDA", "LAMBDA"] ×1, ["lhs", [["x"], "ATOM"]] ×1, c ×1
512 chance floor (theta=5/sqrt(D)) 5 0 / 10 ["lhs", ["NIL", "x"]] ×1, φ ×1, [[["CONS", "rhs"], "ATOM"], "lhs"] ×1, ["lhs", "\u03c6"] ×1, F ×1, LAMBDA ×2, a ×1, ["x", "\u03c6"] ×1, c ×1
512 chance floor (theta=5/sqrt(D)) 6 0 / 10 a ×1, c ×2, b ×1, LAMBDA ×2, ["ATOM", "T"] ×1, φ ×1, ["CONS", "a"] ×1, x ×1
512 chance floor (theta=5/sqrt(D)) 8 0 / 10 a ×2, c ×3, F ×1, ["NIL", "T"] ×1, [[1], ["NIL", "x"]] ×1, LAMBDA ×1, x ×1
512 chance floor (theta=5/sqrt(D)) 10 0 / 10 budget ×1 a ×2, c ×1, ["LAMBDA", "b"] ×1, lhs ×1, EQ ×1, [[1], ["NIL", "x"]] ×1, LAMBDA ×1, x ×1
2048 theirs (theta=0.2) 0 9 / 10 ["a", "LAMBDA", ["c"], "LAMBDA"] ×1
2048 theirs (theta=0.2) 2 10 / 10
2048 theirs (theta=0.2) 4 3 / 10 rhs ×1, ["a", ["b", "LAMBDA"]] ×1, ["T", "b", "x"] ×1, [["x", "a", ["b", "c"]], "a", ["b", "c"]] ×1, ["a", "b", 1] ×1, c ×1, [["LAMBDA", ["x"], ["CONS", "x", ["b", "c"]]], "a"] ×1
2048 theirs (theta=0.2) 5 0 / 10 [1, "b"] ×1, ["LAMBDA", "CONS"] ×1, CONS ×2, DEFINE ×2, ["LAMBDA", "b"] ×1, NIL ×1, φ ×1, ["T", 1] ×1
2048 theirs (theta=0.2) 6 0 / 10 LAMBDA ×1, rhs ×1, DEFINE ×3, b ×1, x ×2, φ ×1, a ×1
2048 theirs (theta=0.2) 8 0 / 10 LAMBDA ×1, rhs ×1, DEFINE ×3, b ×1, x ×2, φ ×1, a ×1
2048 theirs (theta=0.2) 10 0 / 10 LAMBDA ×1, rhs ×1, DEFINE ×3, b ×1, x ×2, φ ×1, a ×1
2048 chance floor (theta=5/sqrt(D)) 0 9 / 10 ["a", "LAMBDA", ["c"], "LAMBDA"] ×1
2048 chance floor (theta=5/sqrt(D)) 2 10 / 10
2048 chance floor (theta=5/sqrt(D)) 4 10 / 10
2048 chance floor (theta=5/sqrt(D)) 5 7 / 10 ["x", "b", ["x", "b", "c"]] ×1, ["c"] ×1, ["rhs", "lhs"] ×1
2048 chance floor (theta=5/sqrt(D)) 6 0 / 10 ["a", "x"] ×1, ["a", "b"] ×1, [["x", ["a"]], "x", ["b", "c", "c"]] ×1, ["a", "b", "c", [["a"], "a"]] ×1, [["c"], [["b", "c"]]] ×1, ["x", "a"] ×1, a ×1, ["LAMBDA", "CONS", ["CONS", ["x", [["b", [[[["x"], ["CONS", ×1, ["a", "LAMBDA"] ×1, ["LAMBDA", ["x", ["b", "c"], "b", "c"], ["c"]] ×1
2048 chance floor (theta=5/sqrt(D)) 8 0 / 10 T ×1, ["LAMBDA", "a"] ×1, [["DEFINE", ["CONS", [["CAR"], "CONS"]]], [["b", "F"], [["NI ×1, x ×1, [1, "DEFINE"] ×1, a ×1, ["LAMBDA", "LABEL"] ×1, [["LAMBDA", "lhs"], ["a"]] ×1, ["LAMBDA", ["rhs", ["rhs", "x"]], "c"] ×1, [[["x"], "DEFINE", "x"], ["c", [["CONS", [[[1, "T"], "a"], [ ×1
2048 chance floor (theta=5/sqrt(D)) 10 0 / 10 rhs ×1, [["x"], "c"] ×1, φ ×1, x ×1, CONS ×1, DEFINE ×2, LAMBDA ×1, a ×1, 1 ×1
8192 theirs (theta=0.2) 0 7 / 10 ["a", "LAMBDA", ["NIL", ["CONS", "a", ["b", "c"]]], [["b", " ×1, ["a", "b", "LAMBDA", "a", "lhs"] ×1, ["a", "b", "LAMBDA", "x", ["b", "c"]] ×1
8192 theirs (theta=0.2) 2 7 / 10 ["a", ["a", "b", "c"]] ×1, ["a", ["b", "c"]] ×1, ["LAMBDA", "b", "c"] ×1
8192 theirs (theta=0.2) 4 8 / 10 ["a", "rhs"] ×1, ["a", "LAMBDA", [[["b", "c"]], "a"], 1] ×1
8192 theirs (theta=0.2) 5 0 / 10 T ×1, ["rhs", "T"] ×1, CONS ×1, ["NIL", "F"] ×1, ["T", "CONS"] ×1, LAMBDA ×2, a ×1, NIL ×1, [1, ["F", "a"]] ×1
8192 theirs (theta=0.2) 6 0 / 10 rhs ×1, φ ×1, CONS ×1, lhs ×1, LAMBDA ×2, a ×1, 1 ×1, NIL ×1, c ×1
8192 theirs (theta=0.2) 8 0 / 10 rhs ×1, φ ×1, CONS ×1, lhs ×1, LAMBDA ×2, a ×1, 1 ×1, NIL ×1, c ×1
8192 theirs (theta=0.2) 10 0 / 10 rhs ×1, LAMBDA ×2, lhs ×1, c ×1, 1 ×2, CONS ×1, a ×1, φ ×1
8192 chance floor (theta=5/sqrt(D)) 0 7 / 10 ["a", "LAMBDA", ["NIL", ["CONS", "a", ["b", "c"]]], [["b", " ×1, ["a", "b", "LAMBDA", "a", "lhs"] ×1, ["a", "b", "LAMBDA", "x", ["b", "c"]] ×1
8192 chance floor (theta=5/sqrt(D)) 2 7 / 10 ["a", ["a", "b", "c"]] ×1, ["a", ["b", "c"]] ×1, ["LAMBDA", "b", "c"] ×1
8192 chance floor (theta=5/sqrt(D)) 4 8 / 10 ["a", "rhs"] ×1, ["a", "LAMBDA", [[["b", "c"]], "a"], 1] ×1
8192 chance floor (theta=5/sqrt(D)) 5 8 / 10 ["a", "c"] ×1, ["a", "LAMBDA", [[["b", "c"]], "a"], 1] ×1
8192 chance floor (theta=5/sqrt(D)) 6 8 / 10 ["a", "a", "b", "c"] ×1, ["a", "LAMBDA", "\u03c6", 1] ×1
8192 chance floor (theta=5/sqrt(D)) 8 7 / 10 ["a", "a", "b", "c"] ×1, ["a", "LAMBDA", "\u03c6", 1] ×1, ["a", ["b", "c"]] ×1
8192 chance floor (theta=5/sqrt(D)) 10 3 / 10 ["NIL", "a", "b", "LAMBDA", "a", [["CONS", "x", ["b", "c"]]] ×1, ["a", "LAMBDA", "\u03c6", 1] ×1, ["a", ["b", "c"]] ×1, ["b", "a"] ×1, [["a", ["b", "c"]], "b", "c"] ×1, ["a", ["c"], "c"] ×1, ["LAMBDA", "lhs", ["CONS", "a", ["b", "c"]]] ×1

residuelisp, trap 3 runs, with what went wrong:

D σ correct errors wrong values
500 0 10 / 10
500 1 10 / 10
500 2 7 / 10 InterpreterError: Segmentation fault in car hehe ×3
500 3 0 / 10 InterpreterError: Segmentation fault in car hehe ×6, InterpreterError: Segmentation fault in cdr hehe ×2, InterpreterError: ERROR: argument number mismatch in `associate`. ×2
500 4 0 / 10 InterpreterError: Segmentation fault in car hehe ×7, InterpreterError: Segmentation fault in cdr hehe ×2, InterpreterError: ERROR: Tuple is not a list. ×1
500 6 0 / 10 InterpreterError: Segmentation fault in cdr hehe ×4, InterpreterError: Segmentation fault in car hehe ×4, Exception: TODO ×1, InterpreterError: ERROR: Expected function in `get_args`. ×1
500 8 0 / 10 InterpreterError: ERROR: Tuple is not a list. ×2, InterpreterError: ERROR: Expected function in `get_args`. ×1, Exception: TODO ×1, InterpreterError: Segmentation fault in car hehe ×2, InterpreterError: Segmentation fault in cdr hehe ×3, InterpreterError: ERROR: Conditional expression requires `CONSEQUENT` and `ALTERNATE`, see documen ×1
500 12 0 / 10 budget ×4, InterpreterError: ERROR: Expected function in `get_args`. ×2, InterpreterError: Segmentation fault in car hehe ×2, InterpreterError: ERROR: Expected function in `get_body`. ×1, InterpreterError: ERROR: Conditional expression requires `CONSEQUENT` and `ALTERNATE`, see documen ×1
500 16 0 / 10 budget ×7, InterpreterError: ERROR: Expected function in `get_args`. ×1, Exception: TODO ×1, InterpreterError: ERROR: Expected function in `get_body`. ×1
500 24 0 / 10 InterpreterError: ERROR: Conditional expression requires `CONSEQUENT` and `ALTERNATE`, see documen ×1, Exception: TODO ×4, InterpreterError: ERROR: Expected function in `get_body`. ×1, budget ×3, InterpreterError: ERROR: Expected function in `get_args`. ×1
500 32 0 / 10 Exception: TODO ×3, RecursionError ×2, budget ×3, InterpreterError: ERROR: Tuple is not a list. ×1, InterpreterError: ERROR: Expected function in `get_args`. ×1
2000 0 10 / 10
2000 1 10 / 10
2000 2 10 / 10
2000 3 10 / 10
2000 4 8 / 10 InterpreterError: ERROR: argument number mismatch in `associate`. ×1, InterpreterError: Segmentation fault in car hehe ×1
2000 6 0 / 10 InterpreterError: Segmentation fault in car hehe ×3, InterpreterError: ERROR: argument number mismatch in `associate`. ×5, InterpreterError: Segmentation fault in cdr hehe ×2
2000 8 0 / 10 InterpreterError: Segmentation fault in car hehe ×9, InterpreterError: Segmentation fault in cdr hehe ×1
2000 12 0 / 10 InterpreterError: ERROR: Tuple is not a list. ×3, InterpreterError: Segmentation fault in cdr hehe ×3, InterpreterError: Segmentation fault in car hehe ×2, InterpreterError: ERROR: Expected function in `get_args`. ×1, Exception: TODO ×1
2000 16 0 / 10 InterpreterError: ERROR: Expected function in `get_args`. ×3, InterpreterError: ERROR: Tuple is not a list. ×2, InterpreterError: Segmentation fault in cdr hehe ×2, Exception: TODO ×3
2000 24 0 / 10 InterpreterError: ERROR: Expected function in `get_body`. ×2, budget ×6, InterpreterError: Segmentation fault in car hehe ×1, InterpreterError: ERROR: Expected function in `get_args`. ×1
2000 32 0 / 10 InterpreterError: ERROR: Expected function in `get_args`. ×1, InterpreterError: Segmentation fault in car hehe ×1, InterpreterError: ERROR: Expected function in `get_body`. ×2, budget ×6
8000 0 10 / 10
8000 1 10 / 10
8000 2 10 / 10
8000 3 10 / 10
8000 4 10 / 10
8000 6 10 / 10
8000 8 9 / 10 InterpreterError: Segmentation fault in car hehe ×1
8000 12 1 / 10 InterpreterError: Segmentation fault in car hehe ×6, InterpreterError: ERROR: argument number mismatch in `associate`. ×2, InterpreterError: Segmentation fault in cdr hehe ×1
8000 16 0 / 10 InterpreterError: Segmentation fault in car hehe ×7, InterpreterError: ERROR: argument number mismatch in `associate`. ×1, InterpreterError: Segmentation fault in cdr hehe ×2
8000 24 0 / 10 InterpreterError: Segmentation fault in cdr hehe ×4, InterpreterError: Segmentation fault in car hehe ×6
8000 32 0 / 10 InterpreterError: Segmentation fault in cdr hehe ×2, InterpreterError: Segmentation fault in car hehe ×3, budget ×2, InterpreterError: ERROR: Tuple is not a list. ×2, InterpreterError: ERROR: Expected function in `get_args`. ×1

Generated by Claude on Benjamin Schwerdtner’s behalf.

source: notebooks/holis_reproduction.clj