Skip to content

Commit 3d5002c

Browse files
committed
Implement ordered persistent array maps
Preserve array-map insertion order, use the Clojure eight-entry promotion threshold, and optimize persistent and transient operations. Add coverage for ordering, duplicate keys, metadata, allocation behavior, Atom references, typed arrays, native slice reduction, and decimal printing.
1 parent 32d3444 commit 3d5002c

14 files changed

Lines changed: 688 additions & 60 deletions

File tree

pkg/lang/atom.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package lang
22

3-
import "sync/atomic"
3+
import (
4+
"sync"
5+
"sync/atomic"
6+
)
47

58
type (
69
Atom struct {
710
state atomic.Pointer[Box]
811
initial Box
912
watches IPersistentMap
1013

11-
meta IPersistentMap
14+
referenceMu sync.RWMutex
15+
meta IPersistentMap
16+
validator IFn
1217
}
1318
)
1419

@@ -36,8 +41,34 @@ func (a *Atom) Deref() interface{} {
3641
return a.state.Load().val
3742
}
3843

39-
func (a *Atom) SetValidator(vf IFn) { panic("not implemented") }
40-
func (a *Atom) Validator() IFn { panic("not implemented") }
44+
func (a *Atom) SetValidator(vf IFn) {
45+
if vf != nil && !IsTruthy(Apply1(vf, a.Deref())) {
46+
panic(NewIllegalStateError("Invalid reference state"))
47+
}
48+
a.referenceMu.Lock()
49+
a.validator = vf
50+
a.referenceMu.Unlock()
51+
}
52+
53+
func (a *Atom) Validator() IFn {
54+
a.referenceMu.RLock()
55+
defer a.referenceMu.RUnlock()
56+
return a.validator
57+
}
58+
59+
func (a *Atom) GetValidator() IFn {
60+
return a.Validator()
61+
}
62+
63+
func (a *Atom) validate(newVal any) {
64+
a.referenceMu.RLock()
65+
validator := a.validator
66+
a.referenceMu.RUnlock()
67+
if validator != nil && !IsTruthy(Apply1(validator, newVal)) {
68+
panic(NewIllegalStateError("Invalid reference state"))
69+
}
70+
}
71+
4172
func (a *Atom) Watches() IPersistentMap {
4273
return a.watches
4374
}
@@ -118,7 +149,7 @@ func (a *Atom) CompareAndSet(oldv, newv interface{}) bool {
118149
}
119150

120151
func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
121-
// TODO: validate
152+
a.validate(newv)
122153
if Identical(old.val, newv) &&
123154
(a.watches == nil || a.watches.Count() == 0) {
124155
return a.state.CompareAndSwap(old, old)
@@ -131,7 +162,7 @@ func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
131162
}
132163

133164
func (a *Atom) Reset(newVal interface{}) interface{} {
134-
// TODO: validate
165+
a.validate(newVal)
135166
for {
136167
old := a.state.Load()
137168
if Identical(old.val, newVal) &&
@@ -148,8 +179,22 @@ func (a *Atom) Reset(newVal interface{}) interface{} {
148179
}
149180

150181
func (a *Atom) Meta() IPersistentMap {
151-
if a.meta == nil {
152-
return nil
153-
}
182+
a.referenceMu.RLock()
183+
defer a.referenceMu.RUnlock()
154184
return a.meta
155185
}
186+
187+
func (a *Atom) AlterMeta(f IFn, args ISeq) IPersistentMap {
188+
meta := f.ApplyTo(NewCons(a.Meta(), args))
189+
if meta == nil {
190+
return a.ResetMeta(nil)
191+
}
192+
return a.ResetMeta(meta.(IPersistentMap))
193+
}
194+
195+
func (a *Atom) ResetMeta(meta IPersistentMap) IPersistentMap {
196+
a.referenceMu.Lock()
197+
a.meta = meta
198+
a.referenceMu.Unlock()
199+
return meta
200+
}

pkg/lang/atom_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,64 @@ func TestAtomIdenticalUpdatesStillNotifyWatches(t *testing.T) {
6868
t.Fatalf("watch called %d times, want 2", calls)
6969
}
7070
}
71+
72+
func TestAtomMetadata(t *testing.T) {
73+
atom := NewAtom(nil)
74+
meta := NewMap(NewKeyword("source"), "test")
75+
76+
if got := atom.ResetMeta(meta); got != meta {
77+
t.Fatalf("ResetMeta returned %v, want %v", got, meta)
78+
}
79+
if got := atom.Meta(); got != meta {
80+
t.Fatalf("Meta returned %v, want %v", got, meta)
81+
}
82+
83+
updated := atom.AlterMeta(
84+
FnFunc2(func(current, value any) any {
85+
return current.(IPersistentMap).Assoc(NewKeyword("updated"), value)
86+
}),
87+
NewList(true),
88+
)
89+
if got := updated.ValAt(NewKeyword("updated")); got != true {
90+
t.Fatalf("altered metadata value = %v, want true", got)
91+
}
92+
}
93+
94+
func TestAtomValidator(t *testing.T) {
95+
atom := NewAtom(int64(2))
96+
even := FnFunc1(func(value any) any {
97+
return value.(int64)%2 == 0
98+
})
99+
atom.SetValidator(even)
100+
101+
if got := Apply1(atom.Validator(), int64(2)); got != true {
102+
t.Fatalf("validator returned %v for 2, want true", got)
103+
}
104+
if got := atom.Reset(int64(4)); got != int64(4) {
105+
t.Fatalf("Reset returned %v, want 4", got)
106+
}
107+
108+
assertPanics(t, func() {
109+
atom.Reset(int64(3))
110+
})
111+
if got := atom.Deref(); got != int64(4) {
112+
t.Fatalf("rejected reset changed state to %v", got)
113+
}
114+
115+
assertPanics(t, func() {
116+
atom.SetValidator(FnFunc1(func(any) any { return false }))
117+
})
118+
if got := Apply1(atom.Validator(), int64(4)); got != true {
119+
t.Fatal("rejected validator replaced the current validator")
120+
}
121+
}
122+
123+
func assertPanics(t *testing.T, fn func()) {
124+
t.Helper()
125+
defer func() {
126+
if recover() == nil {
127+
t.Fatal("expected panic")
128+
}
129+
}()
130+
fn()
131+
}

0 commit comments

Comments
 (0)