Skip to content

Commit 2553cf8

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 efbce6e commit 2553cf8

15 files changed

Lines changed: 481 additions & 33 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ TEST-SUITE-BRANCH := glojure
7878
TEST-SUITE-DIR := test/clojure-test-suite
7979
TEST-SUITE-FILE := test-glojure.glj
8080
TEST-SUITE-EXPECT-FAILURES ?= 0
81-
TEST-SUITE-EXPECT-ERRORS ?= 1
81+
TEST-SUITE-EXPECT-ERRORS ?= 0
8282
TEST-SUITE-EXPECT-LOAD-ERRORS ?= 7
8383

8484
MAKES-CLEAN := \

pkg/lang/atom.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,11 @@ func (a *Atom) Meta() IPersistentMap {
204204
}
205205

206206
func (a *Atom) AlterMeta(f IFn, args ISeq) IPersistentMap {
207-
meta := ApplySeq(f, NewCons(a.Meta(), args)).(IPersistentMap)
208-
return a.ResetMeta(meta)
207+
meta := ApplySeq(f, NewCons(a.Meta(), args))
208+
if meta == nil {
209+
return a.ResetMeta(nil)
210+
}
211+
return a.ResetMeta(meta.(IPersistentMap))
209212
}
210213

211214
func (a *Atom) ResetMeta(meta IPersistentMap) IPersistentMap {

pkg/lang/atom_test.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,27 @@ func TestAtomValidator(t *testing.T) {
105105
t.Fatalf("validated Reset returned %v", got)
106106
}
107107

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()
108125
defer func() {
109126
if recover() == nil {
110-
t.Fatal("validator accepted invalid state")
111-
}
112-
if got := atom.Deref(); got != int64(4) {
113-
t.Fatalf("failed validation changed atom to %v", got)
127+
t.Fatal("expected panic")
114128
}
115129
}()
116-
atom.Reset(int64(3))
130+
fn()
117131
}

pkg/lang/persistentarraymap.go

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,18 @@ func (s *KeywordMapShape) indexOf(key Keyword) int {
192192
}
193193

194194
func newArrayMap(keyVals []any) *Map {
195-
if len(keyVals) <= arrayMapInlineSize {
195+
m := newArrayMapWithSize(len(keyVals))
196+
copy(m.keyVals, keyVals)
197+
return m
198+
}
199+
200+
func newArrayMapWithSize(size int) *Map {
201+
if size <= arrayMapInlineSize {
196202
storage := &inlineMapStorage{}
197-
copy(storage.keyVals[:], keyVals)
198-
storage.Map.keyVals = storage.keyVals[:len(keyVals)]
203+
storage.Map.keyVals = storage.keyVals[:size]
199204
return &storage.Map
200205
}
201-
return &Map{keyVals: append([]any(nil), keyVals...)}
206+
return &Map{keyVals: make([]any, size)}
202207
}
203208

204209
// canBePersistentArrayMap mirrors Clojure's PersistentArrayMap thresholds.
@@ -221,7 +226,8 @@ func canBePersistentArrayMap(keyVals []any) bool {
221226
}
222227

223228
func NewPersistentArrayMapAsIfByAssoc(init []any) IPersistentMap {
224-
complexPath := (len(init) & 1) == 1
229+
hasTrailing := (len(init) & 1) == 1
230+
complexPath := hasTrailing
225231
for i := 0; i < len(init) && !complexPath; i += 2 {
226232
for j := 0; j < i; j += 2 {
227233
if equalKey(init[i], init[j]) {
@@ -232,13 +238,28 @@ func NewPersistentArrayMapAsIfByAssoc(init []any) IPersistentMap {
232238
}
233239

234240
if complexPath {
235-
return newPersistentArrayMapAsIfByAssocComplexPath(init)
241+
return newPersistentArrayMapAsIfByAssocComplexPath(init, hasTrailing)
236242
}
237243

238-
return NewMap(init...)
244+
return newArrayMap(init)
239245
}
240246

241-
func newPersistentArrayMapAsIfByAssocComplexPath(init []any) IPersistentMap {
247+
func newPersistentArrayMapAsIfByAssocComplexPath(init []any, hasTrailing bool) IPersistentMap {
248+
if hasTrailing {
249+
trailing := emptyMap.Cons(init[len(init)-1]).(IPersistentMap)
250+
seedCount := len(init) - 1
251+
grown := make([]any, seedCount+trailing.Count()*2)
252+
copy(grown, init[:seedCount])
253+
i := seedCount
254+
for seq := trailing.Seq(); seq != nil; seq = seq.Next() {
255+
entry := seq.First().(IMapEntry)
256+
grown[i] = entry.Key()
257+
grown[i+1] = entry.Val()
258+
i += 2
259+
}
260+
init = grown
261+
}
262+
242263
n := 0
243264
for i := 0; i < len(init); i += 2 {
244265
duplicateKey := false
@@ -283,7 +304,7 @@ func newPersistentArrayMapAsIfByAssocComplexPath(init []any) IPersistentMap {
283304
}
284305
init = nodups
285306
}
286-
return NewMap(init...)
307+
return newArrayMap(init)
287308
}
288309

289310
func (m *Map) ValAt(key any) any {
@@ -479,13 +500,25 @@ func (m *Map) Without(k any) IPersistentMap {
479500
}
480501
return NewMapUniqueKeys(keyVals...).(IObj).WithMeta(m.meta).(IPersistentMap)
481502
}
482-
newKeyVals := make([]any, 0, len(m.keyVals))
503+
remove := -1
483504
for i := 0; i < len(m.keyVals); i += 2 {
484-
if !Equiv(m.keyVals[i], k) {
485-
newKeyVals = append(newKeyVals, m.keyVals[i], m.keyVals[i+1])
505+
if Equiv(m.keyVals[i], k) {
506+
remove = i
507+
break
486508
}
487509
}
488-
return NewMap(newKeyVals...).(IObj).WithMeta(m.meta).(IPersistentMap)
510+
if remove < 0 {
511+
return m
512+
}
513+
if len(m.keyVals) == 2 {
514+
return emptyMap.WithMeta(m.meta).(IPersistentMap)
515+
}
516+
517+
result := newArrayMapWithSize(len(m.keyVals) - 2)
518+
result.meta = m.meta
519+
copy(result.keyVals, m.keyVals[:remove])
520+
copy(result.keyVals[remove:], m.keyVals[remove+2:])
521+
return result
489522
}
490523

491524
func (m *Map) Count() int {
@@ -596,8 +629,7 @@ func (m *Map) ReduceInit(f IFn, init any) any {
596629
}
597630

598631
func (m *Map) AsTransient() ITransientCollection {
599-
// TODO: implement transients
600-
return &TransientMap{IPersistentMap: m}
632+
return &TransientMap{IPersistentMap: m.clone()}
601633
}
602634

603635
////////////////////////////////////////////////////////////////////////////////
@@ -650,12 +682,49 @@ func (m *TransientMap) Conj(v any) Conjer {
650682

651683
func (m *TransientMap) Assoc(k, v any) Associative {
652684
m.ensureEditable()
685+
if arrayMap, ok := m.IPersistentMap.(*Map); ok && arrayMap.keywordShape == nil {
686+
for i := 0; i < len(arrayMap.keyVals); i += 2 {
687+
if Equiv(arrayMap.keyVals[i], k) {
688+
arrayMap.keyVals[i+1] = v
689+
arrayMap.hash = 0
690+
arrayMap.hasheq = 0
691+
return m
692+
}
693+
}
694+
695+
threshold := arrayMapHashThreshold
696+
if _, ok := k.(Keyword); ok {
697+
threshold = arrayMapKeywordThreshold
698+
}
699+
if len(arrayMap.keyVals) < threshold {
700+
arrayMap.keyVals = append(arrayMap.keyVals, k, v)
701+
arrayMap.hash = 0
702+
arrayMap.hasheq = 0
703+
return m
704+
}
705+
}
653706
m.IPersistentMap = m.IPersistentMap.Assoc(k, v).(IPersistentMap)
654707
return m
655708
}
656709

657710
func (m *TransientMap) Without(key any) IPersistentMap {
658711
m.ensureEditable()
712+
if arrayMap, ok := m.IPersistentMap.(*Map); ok && arrayMap.keywordShape == nil {
713+
for i := 0; i < len(arrayMap.keyVals); i += 2 {
714+
if !Equiv(arrayMap.keyVals[i], key) {
715+
continue
716+
}
717+
newLen := len(arrayMap.keyVals) - 2
718+
copy(arrayMap.keyVals[i:], arrayMap.keyVals[i+2:])
719+
arrayMap.keyVals[newLen] = nil
720+
arrayMap.keyVals[newLen+1] = nil
721+
arrayMap.keyVals = arrayMap.keyVals[:newLen]
722+
arrayMap.hash = 0
723+
arrayMap.hasheq = 0
724+
break
725+
}
726+
return m
727+
}
659728
m.IPersistentMap = m.IPersistentMap.Without(key).(IPersistentMap)
660729
return m
661730
}

pkg/lang/persistentarraymap_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,149 @@
11
package lang
22

33
import (
4+
"reflect"
45
"runtime"
56
"strconv"
67
"testing"
78
)
89

10+
func mapKeys(m IPersistentMap) []any {
11+
keys := make([]any, 0, m.Count())
12+
for seq := m.Seq(); seq != nil; seq = seq.Next() {
13+
keys = append(keys, seq.First().(IMapEntry).Key())
14+
}
15+
return keys
16+
}
17+
18+
func TestPersistentArrayMapConstructionPreservesOrder(t *testing.T) {
19+
init := []any{
20+
"a", 1,
21+
"b", 2,
22+
"c", 3,
23+
"d", 4,
24+
"e", 5,
25+
"f", 6,
26+
"g", 7,
27+
"h", 8,
28+
"i", 9,
29+
}
30+
m, ok := NewPersistentArrayMapAsIfByAssoc(init).(*Map)
31+
if !ok {
32+
t.Fatalf("array-map constructor returned %T, want *Map", m)
33+
}
34+
if got, want := mapKeys(m), []any{"a", "b", "c", "d", "e", "f", "g", "h", "i"}; !reflect.DeepEqual(got, want) {
35+
t.Fatalf("keys = %v, want %v", got, want)
36+
}
37+
38+
updated := m.Assoc("a", 10)
39+
if _, ok := updated.(*Map); !ok {
40+
t.Fatalf("existing-key assoc returned %T, want *Map", updated)
41+
}
42+
grown := m.Assoc("j", 10)
43+
if _, ok := grown.(*PersistentHashMap); !ok {
44+
t.Fatalf("new-key assoc returned %T, want *PersistentHashMap", grown)
45+
}
46+
47+
init[0] = "changed"
48+
if !m.ContainsKey("a") || m.ContainsKey("changed") {
49+
t.Fatal("array-map retained mutable constructor storage")
50+
}
51+
}
52+
53+
func TestPersistentArrayMapConstructionHandlesTrailingMapEntry(t *testing.T) {
54+
m := NewPersistentArrayMapAsIfByAssoc([]any{
55+
"a", 1,
56+
NewVector("b", 2),
57+
}).(*Map)
58+
59+
if got, want := mapKeys(m), []any{"a", "b"}; !reflect.DeepEqual(got, want) {
60+
t.Fatalf("keys = %v, want %v", got, want)
61+
}
62+
if got := m.ValAt("b"); got != 2 {
63+
t.Fatalf("value at b = %v, want 2", got)
64+
}
65+
}
66+
67+
func TestPersistentArrayMapConstructionHandlesDuplicateKeys(t *testing.T) {
68+
m := NewPersistentArrayMapAsIfByAssoc([]any{
69+
"a", 1,
70+
"b", 2,
71+
"a", 3,
72+
}).(*Map)
73+
74+
if got := m.Count(); got != 2 {
75+
t.Fatalf("count = %d, want 2", got)
76+
}
77+
if got := m.ValAt("a"); got != 3 {
78+
t.Fatalf("value at a = %v, want 3", got)
79+
}
80+
if got, want := mapKeys(m), []any{"a", "b"}; !reflect.DeepEqual(got, want) {
81+
t.Fatalf("keys = %v, want %v", got, want)
82+
}
83+
}
84+
85+
func TestPersistentArrayMapWithoutPreservesTypeOrderAndMeta(t *testing.T) {
86+
init := []any{
87+
"a", 1,
88+
"b", 2,
89+
"c", 3,
90+
"d", 4,
91+
"e", 5,
92+
"f", 6,
93+
"g", 7,
94+
"h", 8,
95+
"i", 9,
96+
}
97+
meta := NewMap(NewKeyword("source"), "test")
98+
m := NewPersistentArrayMapAsIfByAssoc(init).(*Map).WithMeta(meta).(*Map)
99+
100+
without := m.Without("e")
101+
got, ok := without.(*Map)
102+
if !ok {
103+
t.Fatalf("without returned %T, want *Map", without)
104+
}
105+
if got.Meta() != meta {
106+
t.Fatal("without discarded metadata")
107+
}
108+
wantKeys := []any{"a", "b", "c", "d", "f", "g", "h", "i"}
109+
if keys := mapKeys(got); !reflect.DeepEqual(keys, wantKeys) {
110+
t.Fatalf("keys = %v, want %v", keys, wantKeys)
111+
}
112+
if same := m.Without("missing"); same != m {
113+
t.Fatal("removing a missing key did not return the original map")
114+
}
115+
}
116+
117+
func TestTransientArrayMapMutatesPrivateStorage(t *testing.T) {
118+
original := NewMap("a", 1, "b", 2).(*Map)
119+
transient := original.AsTransient().(*TransientMap)
120+
121+
key := any("a")
122+
value := any(10)
123+
if got := testing.AllocsPerRun(1_000, func() {
124+
transient.Assoc(key, value)
125+
}); got != 0 {
126+
t.Fatalf("transient existing-key assoc allocated %v objects, want 0", got)
127+
}
128+
transient.Assoc("c", 3)
129+
transient.Without("b")
130+
131+
if got, want := mapKeys(original), []any{"a", "b"}; !reflect.DeepEqual(got, want) {
132+
t.Fatalf("original keys = %v, want %v", got, want)
133+
}
134+
if got := original.ValAt("a"); got != 1 {
135+
t.Fatalf("original value at a = %v, want 1", got)
136+
}
137+
138+
persistent := transient.Persistent().(*Map)
139+
if got, want := mapKeys(persistent), []any{"a", "c"}; !reflect.DeepEqual(got, want) {
140+
t.Fatalf("persistent keys = %v, want %v", got, want)
141+
}
142+
if got := persistent.ValAt("a"); got != 10 {
143+
t.Fatalf("persistent value at a = %v, want 10", got)
144+
}
145+
}
146+
9147
type testStaticKeywordMapStorage struct {
10148
Map
11149
values [9]any

0 commit comments

Comments
 (0)