From e2f471c608b0d7ba946f399204f634a5bb95dbc3 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 14 Jun 2025 02:52:03 +0200 Subject: [PATCH] Added setting of index values --- ucl/builtins.go | 33 +++++++++++++++++++++++++++++++++ ucl/errors.go | 2 ++ ucl/eval.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- ucl/inst_test.go | 11 +++++++++++ ucl/objs.go | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index 0c84824..e1edbcc 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -539,6 +539,39 @@ func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Obj return nil, nil } +func indexAssign(ctx context.Context, obj, elem, toVal Object, pos lexer.Position) (_ Object, err error) { + if obj == nil { + return nil, assignToNilIndex(pos) + } + switch v := obj.(type) { + case ModListable: + intIdx, ok := elem.(IntObject) + if !ok { + return nil, nil + } + if int(intIdx) >= 0 && int(intIdx) < v.Len() { + err = v.SetIndex(int(intIdx), toVal) + } else if int(intIdx) < 0 && int(intIdx) >= -v.Len() { + err = v.SetIndex(v.Len()+int(intIdx), toVal) + } + if err != nil { + return nil, err + } + return toVal, nil + case ModHashable: + strIdx, ok := elem.(StringObject) + if !ok { + return nil, nil + } + err = v.SetValue(string(strIdx), toVal) + if err != nil { + return nil, err + } + return toVal, nil + } + return nil, notModIndexableError(pos) +} + func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/ucl/errors.go b/ucl/errors.go index f7f80e9..8e20adb 100644 --- a/ucl/errors.go +++ b/ucl/errors.go @@ -14,6 +14,8 @@ var ( var ( tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") notIndexableError = newBadUsage("index only support on lists and hashes") + notModIndexableError = newBadUsage("list or hash cannot be modified") + assignToNilIndex = newBadUsage("assigning to nil index value") ) type errorWithPos struct { diff --git a/ucl/eval.go b/ucl/eval.go index 71635b3..c6469ae 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -220,7 +220,51 @@ func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal O return e.assignArg(ctx, ec, n.Arg, toVal) } - return nil, errors.New("TODO") + val, err := e.evalArgForDotAssign(ctx, ec, n.Arg) + if err != nil { + return nil, err + } + + for i, dot := range n.DotSuffix { + isLast := i == len(n.DotSuffix)-1 + + var idx Object + if dot.KeyIdent != nil { + idx = StringObject(dot.KeyIdent.String()) + } else { + idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) + if err != nil { + return nil, err + } + } + + if isLast { + val, err = indexAssign(ctx, val, idx, toVal, n.Pos) + } else { + val, err = indexLookup(ctx, val, idx, n.Pos) + } + if err != nil { + return nil, err + } + } + + return val, nil +} + +func (e evaluator) evalArgForDotAssign(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { + // Special case for dot assigns of 'a.b = c' where a is actually a var deref (i.e. $a) + // which is unnecessary for assignments. Likewise, having '$a.b = c' should be dissallowed + + switch { + case n.Ident != nil: + if v, ok := ec.getVar(n.Ident.String()); ok { + return v, nil + } + return nil, nil + case n.Var != nil: + return nil, errors.New("cannot assign to a dereferenced variable") + } + return e.evalArg(ctx, ec, n) } func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 52fdf3e..041c367 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -116,6 +116,17 @@ func TestInst_Eval(t *testing.T) { {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, {desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil}, {desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil}, + + // Assign dots + {desc: "assign dot 1", expr: `x = [1 2 3] ; x.(0) = 4 ; "$x"`, want: "[4 2 3]"}, + {desc: "assign dot 2", expr: `x = [1 2 3] ; x.(1) = 5 ; "$x"`, want: "[1 5 3]"}, + {desc: "assign dot 3", expr: `x = [1 2 3] ; x.(-1) = 6 ; "$x"`, want: "[1 2 6]"}, + {desc: "assign dot 4", expr: `y = [a:1 b:2] ; y.a = "hello" ; "$y"`, want: `[a:hello b:2]`}, + {desc: "assign dot 5", expr: `y = [a:1 b:2] ; y.b = "world" ; "$y"`, want: `[a:1 b:world]`}, + {desc: "assign dot 6", expr: `y = [a:"b" b:2] ; y.($y.a) = "world" ; "$y"`, want: `[a:b b:world]`}, + {desc: "assign dot 7", expr: `z = [a:[1 2] b:[3 3]] ; z.a.(1) = 3 ; "$z"`, want: `[a:[1 3] b:[3 3]]`}, + {desc: "assign dot 8", expr: `z = [[1 2] [3 4]] ; z.(1).(0) = 5 ; "$z"`, want: `[[1 2] [5 4]]`}, + {desc: "assign dot 7", expr: `z = [[a:1 b:2] [c:3 d:4]] ; z.(1).a = 5 ; "$z"`, want: `[[a:1 b:2] [a:5 c:3 d:4]]`}, } for _, tt := range tests { diff --git a/ucl/objs.go b/ucl/objs.go index 981f7ab..2ecde0f 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strconv" "strings" "time" @@ -35,9 +36,13 @@ type ModListable interface { // Insert adds a new item to the list. idx can be a positive // number from 0 to len(), in which case the object will be inserted - // at that position. If idx is negative, then the item will be inserted + // at that position, shifting all other elements to the right. + // If idx is negative, then the item will be inserted // at that position from the right. Insert(idx int, obj Object) error + + // SetIndex replaces the item at index position idx with obj. + SetIndex(idx int, obj Object) error } type Hashable interface { @@ -46,6 +51,11 @@ type Hashable interface { Each(func(k string, v Object) error) error } +type ModHashable interface { + Hashable + SetValue(k string, val Object) error +} + type ListObject []Object func NewListObject() *ListObject { @@ -80,6 +90,11 @@ func (s *ListObject) Index(i int) Object { return (*s)[i] } +func (s *ListObject) SetIndex(i int, toVal Object) error { + (*s)[i] = toVal + return nil +} + type StringListObject []string func (ss StringListObject) String() string { @@ -117,9 +132,17 @@ func (s HashObject) String() string { return "[:]" } + // Return the keys in sorted order + keys := make([]string, 0, len(s)) + for k := range s { + keys = append(keys, k) + } + sort.Strings(keys) + sb := strings.Builder{} sb.WriteString("[") - for k, v := range s { + for _, k := range keys { + v := s[k] if sb.Len() != 1 { sb.WriteString(" ") } @@ -152,6 +175,11 @@ func (s HashObject) Each(fn func(k string, v Object) error) error { return nil } +func (s HashObject) SetValue(k string, val Object) error { + s[k] = val + return nil +} + type StringObject string func (s StringObject) String() string {