Skip to content

Commit cacf124

Browse files
authored
Adding del() to remove map entries (e.g del(x[y]) / del(x.s)) or variables; optimize small map Set (#302)
* Adding del() to remove map entries (e.g del(x[y])) or variables * linter * Implement the regular identifier delete * Fix bug in shell where result map wasn't sorted, now caused failure thanks to optimization in small map get * handle the . case for delete, share code * slight improvement in comment/debug * It's better when I don't forget to git add new files * del() implies no caching/memoization. added test for that and some negative cases too
1 parent acc96d7 commit cacf124

File tree

8 files changed

+205
-22
lines changed

8 files changed

+205
-22
lines changed

Diff for: eval/eval.go

+64-1
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,64 @@ func (s *State) evalPrintLogError(node *ast.Builtin) object.Object {
420420

421421
var ErrorKey = object.String{Value: "err"} // can't use error as that's a builtin.
422422

423+
func (s *State) evalDelete(node ast.Node) object.Object {
424+
s.env.TriggerNoCache()
425+
switch node.Value().Type() {
426+
case token.IDENT:
427+
name := node.Value().Literal()
428+
if name == "" {
429+
return s.NewError("delete empty identifier")
430+
}
431+
return s.env.Delete(name)
432+
case token.DOT:
433+
idxE := node.(*ast.IndexExpression)
434+
// index is the string value and not an identifier to resolve.
435+
key := idxE.Index.Value()
436+
if key.Type() != token.STRING && key.Type() != token.IDENT {
437+
return s.Errorf("del expression with . not a string: %s", key.Literal())
438+
}
439+
index := object.String{Value: key.Literal()}
440+
return s.deleteMapEntry(idxE, index)
441+
case token.LBRACKET:
442+
// Map/array [] index
443+
idxE := node.(*ast.IndexExpression)
444+
index := s.Eval(idxE.Index)
445+
if index.Type() == object.ERROR {
446+
return index
447+
}
448+
return s.deleteMapEntry(idxE, index)
449+
default:
450+
return s.NewError("delete not supported on " + node.Value().Type().String())
451+
}
452+
}
453+
454+
func (s *State) deleteMapEntry(idxE *ast.IndexExpression, index object.Object) object.Object {
455+
if idxE.Left.Value().Type() != token.IDENT {
456+
return s.NewError("delete index on non identifier: " + idxE.Left.Value().DebugString())
457+
}
458+
id := idxE.Left.Value().Literal()
459+
obj, ok := s.env.Get(id)
460+
if !ok {
461+
// Nothing to delete, we're done
462+
return object.FALSE
463+
}
464+
// TODO: handle arrays too? though delete arr[idx] == arr[0:idx]+arr[idx+1:] so... no point
465+
if obj.Type() != object.MAP {
466+
return s.NewError("delete index on non map: " + id + " " + obj.Type().String())
467+
}
468+
log.LogVf("remove map: %s from %s", index.Inspect(), id)
469+
m := obj.(object.Map)
470+
m, changed := m.Delete(index)
471+
if !changed {
472+
return object.FALSE
473+
}
474+
oerr := s.env.Set(id, m)
475+
if oerr.Type() == object.ERROR {
476+
return oerr
477+
}
478+
return object.TRUE
479+
}
480+
423481
func (s *State) evalBuiltin(node *ast.Builtin) object.Object {
424482
// all take 1 arg exactly except print and log which take 1+.
425483
t := node.Type()
@@ -432,8 +490,13 @@ func (s *State) evalBuiltin(node *ast.Builtin) object.Object {
432490
if oerr := argCheck(s, node.Literal(), minV, varArg, node.Parameters); oerr != nil {
433491
return *oerr
434492
}
435-
if t == token.QUOTE {
493+
// builtins that don't eval arguments (quote, del)
494+
switch t {
495+
case token.QUOTE:
436496
return s.quote(node.Parameters[0])
497+
case token.DEL:
498+
return s.evalDelete(node.Parameters[0])
499+
default:
437500
}
438501
var val object.Object
439502
var rt object.Type

Diff for: extensions/shell.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ func createShellFunctions() {
4949
cmd.Stdin = bytes.NewReader(obj)
5050
}
5151
err := cmd.Run()
52-
res := object.MakeQuad(stdout, object.String{Value: sout.String()},
53-
stderr, object.String{Value: serr.String()})
52+
// keys must be sorted. stdErr before stdOut.
53+
res := object.MakeQuad(stderr, object.String{Value: serr.String()},
54+
stdout, object.String{Value: sout.String()})
5455
if err != nil {
5556
res = res.Set(eval.ErrorKey, object.String{Value: err.Error()})
5657
} else {

Diff for: object/object.go

+59-15
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,17 @@ func CompareKeys(a, b keyValuePair) int {
247247
}
248248

249249
func (m *BigMap) Get(key Object) (Object, bool) {
250+
v, found, _ := m.get(key)
251+
return v, found
252+
}
253+
254+
func (m *BigMap) get(key Object) (Object, bool, int) {
250255
kv := keyValuePair{Key: key}
251256
i, ok := slices.BinarySearchFunc(m.kv, kv, CompareKeys) // log(n) search as we keep it sorted.
252257
if !ok {
253-
return NULL, false
258+
return NULL, false, i
254259
}
255-
return m.kv[i].Value, true
260+
return m.kv[i].Value, true, i
256261
}
257262

258263
func (m *BigMap) Set(key, value Object) Map {
@@ -325,6 +330,16 @@ func (m *BigMap) Range(l, r int64) Object {
325330
return res
326331
}
327332

333+
func (m *BigMap) Delete(key Object) (Map, bool) {
334+
_, found, idx := m.get(key)
335+
if !found {
336+
return m, false
337+
}
338+
copy(m.kv[idx:], m.kv[idx+1:])
339+
m.kv = m.kv[:len(m.kv)-1]
340+
return m, true
341+
}
342+
328343
func NewMapSize(size int) Map {
329344
if size <= MaxSmallMap {
330345
return SmallMap{}
@@ -333,30 +348,46 @@ func NewMapSize(size int) Map {
333348
}
334349

335350
func (m SmallMap) Get(key Object) (Object, bool) {
351+
r, found, _ := m.get(key)
352+
return r, found
353+
}
354+
355+
// return the index where the key if not found would be inserted at,
356+
// replaces slices.BinarySearchFunc(m.smallKV[:m.len], kv, CompareKeys) for small maps.
357+
func (m SmallMap) get(key Object) (Object, bool, int) {
336358
for i := range m.len {
337-
if Cmp(m.smallKV[i].Key, key) == 0 {
338-
return m.smallKV[i].Value, true
359+
c := Cmp(m.smallKV[i].Key, key)
360+
switch c {
361+
case 1:
362+
return NULL, false, i
363+
case 0:
364+
return m.smallKV[i].Value, true, i
339365
}
340366
}
341-
return NULL, false
367+
return NULL, false, m.len
342368
}
343369

344370
func (m SmallMap) Set(key, value Object) Map {
345-
kv := keyValuePair{Key: key, Value: value}
346-
i, ok := slices.BinarySearchFunc(m.smallKV[:m.len], kv, CompareKeys)
371+
_, ok, i := m.get(key) // slices.BinarySearchFunc(m.smallKV[:m.len], kv, CompareKeys)
347372
if ok {
348373
m.smallKV[i].Value = value
349374
return m
350375
}
351-
r := slices.Insert(m.smallKV[0:m.len:MaxSmallMap], i, kv)
352-
if m.len < MaxSmallMap {
353-
copy(m.smallKV[0:MaxSmallMap], r)
354-
m.len++
355-
return m
376+
m.len++
377+
if m.len > MaxSmallMap {
378+
// We need to switch to a big map.
379+
res := &BigMap{kv: make([]keyValuePair, 0, m.len)}
380+
res.kv = append(res.kv, m.smallKV[:i]...)
381+
res.kv = append(res.kv, keyValuePair{Key: key, Value: value})
382+
res.kv = append(res.kv, m.smallKV[i:m.len-1]...)
383+
return res
356384
}
357-
// We need to switch to a real map.
358-
res := &BigMap{kv: r}
359-
return res
385+
// create the space for the new key.
386+
for j := m.len - 1; j > i; j-- {
387+
m.smallKV[j] = m.smallKV[j-1]
388+
}
389+
m.smallKV[i] = keyValuePair{Key: key, Value: value}
390+
return m
360391
}
361392

362393
func (m SmallMap) Len() int {
@@ -385,6 +416,18 @@ func (m SmallMap) Range(l, r int64) Object {
385416
return res
386417
}
387418

419+
func (m SmallMap) Delete(key Object) (Map, bool) {
420+
_, found, where := m.get(key)
421+
if !found {
422+
return m, false
423+
}
424+
for i := where; i < m.len-1; i++ {
425+
m.smallKV[i] = m.smallKV[i+1]
426+
}
427+
m.len--
428+
return m, true
429+
}
430+
388431
func (m SmallMap) Append(right Map) Map {
389432
if right.Len() <= MaxSmallMap { // Maybe same keys, try to keep it a SmallMap
390433
res := SmallMap{len: m.len}
@@ -929,6 +972,7 @@ type Map interface {
929972
Rest() Object
930973
mapElements() []keyValuePair
931974
Append(right Map) Map
975+
Delete(key Object) (Map, bool)
932976
}
933977

934978
func NewMap() Map {

Diff for: object/state.go

+17
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,23 @@ func (e *Environment) Get(name string) (Object, bool) {
264264
return nil, false
265265
}
266266

267+
// Delete removes the first entry found under that name from the environment.
268+
// TODO: check if references need special handling.
269+
func (e *Environment) Delete(name string) Object {
270+
if e.depth == 0 {
271+
e.numSet++
272+
}
273+
if _, ok := e.store[name]; ok {
274+
delete(e.store, name)
275+
log.Debugf("Delete(%s) found at %d %v", name, e.depth, e.cacheKey)
276+
return TRUE
277+
}
278+
if e.outer != nil {
279+
return e.outer.Delete(name)
280+
}
281+
return FALSE
282+
}
283+
267284
// TriggerNoCache is used prevent this call stack from caching.
268285
// Meant to be used by extensions that for instance return random numbers or change state.
269286
func (e *Environment) TriggerNoCache() {

Diff for: parser/parser.go

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func New(l *lexer.Lexer) *Parser {
9494
p.registerPrefix(token.CATCH, p.parseBuiltin)
9595
p.registerPrefix(token.QUOTE, p.parseBuiltin)
9696
p.registerPrefix(token.UNQUOTE, p.parseBuiltin)
97+
p.registerPrefix(token.DEL, p.parseBuiltin)
9798

9899
p.infixParseFns = make(map[token.Type]infixParseFn)
99100
p.registerInfix(token.PLUS, p.parseInfixExpression)

Diff for: tests/delete.gr

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
smallmap:={1:1,2:2,3:3,4:4}
3+
4+
i:=3
5+
log("before del", smallmap)
6+
Assert("first delete should be true", del(smallmap[i-1]))
7+
log("after del", i-1, smallmap)
8+
// 2nd delete is false/noop
9+
Assert("second delete should be false", del(smallmap[i-1])==false)
10+
Assert("smallmap should be as expected", smallmap == {1:1,3:3,4:4})
11+
12+
Assert("first del i should be true", del(i))
13+
Assert("second del i should be false", del(i)==false)
14+
IsErr("i shouldn't exist anymore", i, "identifier not found: i")
15+
16+
largemap:=smallmap + {5:5,6:6,7:7,8:8,9:9,10:10}
17+
log("before del large", largemap)
18+
Assert("first delete should be true", del(largemap[10]))
19+
log("after del large", largemap)
20+
Assert("2nd delete should be false", del(largemap[10])==false)
21+
Assert("largemap should be as expected", largemap == {1:1,3:3,4:4,5:5,6:6,7:7,8:8,9:9})
22+
Assert("first delete should be true", del(largemap[7]))
23+
log("after del large", largemap)
24+
Assert("largemap should be as expected", largemap == {1:1,3:3,4:4,5:5,6:6,8:8,9:9})
25+
26+
Assert("deleting whole map", del(largemap))
27+
IsErr("largemap shouldn't exist anymore", largemap, "identifier not found: largemap")
28+
29+
// string . access syntax
30+
strmap := {"foo":"bar", "baz":"quux"}
31+
log("before del str", strmap)
32+
Assert("first delete should be true", del(strmap.foo))
33+
log("after del str", strmap)
34+
Assert("2nd delete should be false", del(strmap.foo)==false)
35+
Assert("strmap should be as expected", strmap == {"baz":"quux"})
36+
37+
x="outer"
38+
func delTest(x) {
39+
log("before del x", x)
40+
del(x)
41+
log("after del x", x)
42+
x
43+
}
44+
45+
Assert("delTest(str) should return outer", delTest("foo") == "outer")
46+
IsErr("can't (for now) delete registers", delTest(3), "delete not supported on REGISTER")
47+
IsErr("del needs to be of identifier/map", del(1+2), "delete not supported on PLUS")
48+
49+
func delTest() {
50+
del(x)
51+
}
52+
53+
Assert("delTest() should be true first time", delTest())
54+
// Without no cache this wasn't working
55+
Assert("delTest() should be false second time", delTest() == false)

Diff for: token/token.go

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ const (
145145
LOG
146146
ERROR
147147
CATCH
148+
DEL
148149

149150
endIdentityTokens
150151

Diff for: token/type_string.go

+5-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)