From 51e35aa9a67557abe1e9d5867a498de8bd68ad89 Mon Sep 17 00:00:00 2001
From: Leon Mika <lmika@lmika.org>
Date: Sun, 18 May 2025 07:20:52 +1000
Subject: [PATCH] Added support for assignments

---
 ucl/ast.go  |  8 +++++---
 ucl/eval.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 ucl/objs.go | 18 +++++++++---------
 3 files changed, 66 insertions(+), 14 deletions(-)

diff --git a/ucl/ast.go b/ucl/ast.go
index 250587d..4814288 100644
--- a/ucl/ast.go
+++ b/ucl/ast.go
@@ -103,9 +103,10 @@ type astDot struct {
 }
 
 type astCmd struct {
-	Pos  lexer.Position
-	Name astDot   `parser:"@@"`
-	Args []astDot `parser:"@@*"`
+	Pos        lexer.Position
+	Name       astDot   `parser:"@@"`
+	Assign     *astDot  `parser:"( EQ @@"`
+	InvokeArgs []astDot `parser:" | @@+ )?"`
 }
 
 type astPipeline struct {
@@ -141,6 +142,7 @@ var scanner = lexer.MustStateful(lexer.Rules{
 		{"RC", `\}`, nil},
 		{"NL", `[;\n][; \n\t]*`, nil},
 		{"PIPE", `\|`, nil},
+		{"EQ", `=`, nil},
 		{"Ident", `[-]*[a-zA-Z_][\w-!?]*`, nil},
 	},
 	"String": {
diff --git a/ucl/eval.go b/ucl/eval.go
index ab9f26a..4f6894f 100644
--- a/ucl/eval.go
+++ b/ucl/eval.go
@@ -72,6 +72,14 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline
 
 func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd) (Object, error) {
 	switch {
+	case ast.Assign != nil:
+		// Assignment
+		assignVal, err := e.evalDot(ctx, ec, *ast.Assign)
+		if err != nil {
+			return nil, err
+		}
+
+		return e.assignDot(ctx, ec, ast.Assign, assignVal)
 	case (ast.Name.Arg.Ident != nil) && len(ast.Name.DotSuffix) == 0:
 		name := ast.Name.Arg.Ident.String()
 
@@ -85,7 +93,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object,
 		} else {
 			return nil, errors.New("unknown command: " + name)
 		}
-	case len(ast.Args) > 0:
+	case len(ast.InvokeArgs) > 0:
 		nameElem, err := e.evalDot(ctx, ec, ast.Name)
 		if err != nil {
 			return nil, err
@@ -117,7 +125,7 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe O
 	if currentPipe != nil {
 		argsPtr.Append(currentPipe)
 	}
-	for _, arg := range ast.Args {
+	for _, arg := range ast.InvokeArgs {
 		if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' {
 			// Arg switch
 			if kwargs == nil {
@@ -176,6 +184,14 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object,
 	return res, nil
 }
 
+func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n *astDot, toVal Object) (Object, error) {
+	if len(n.DotSuffix) == 0 {
+		return e.assignArg(ctx, ec, n.Arg, toVal)
+	}
+
+	panic("TODO")
+}
+
 func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) {
 	switch {
 	case n.Literal != nil:
@@ -211,6 +227,40 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
 	return nil, errors.New("unhandled arg type")
 }
 
+func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
+	switch {
+	case n.Literal != nil:
+		// We may use this for variable setting?
+		return nil, errors.New("cannot assign to a literal")
+	case n.Var != nil:
+		ec.setOrDefineVar(*n.Var, toVal)
+		return toVal, nil
+	case n.PseudoVar != nil:
+		pvar, ok := ec.getPseudoVar(*n.PseudoVar)
+		if ok {
+			if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
+				return nil, err
+			}
+			return toVal, nil
+		}
+
+		if pvar := e.inst.missingPseudoVarHandler; pvar != nil {
+			if err := pvar.set(ctx, *n.PseudoVar, toVal); err != nil {
+				return nil, err
+			}
+			return toVal, nil
+		}
+		return nil, errors.New("unknown pseudo-variable: " + *n.Var)
+	case n.MaybeSub != nil:
+		return nil, errors.New("cannot assign to a subexpression")
+	case n.ListOrHash != nil:
+		return nil, errors.New("cannot assign to a list or hash")
+	case n.Block != nil:
+		return nil, errors.New("cannot assign to a block")
+	}
+	return nil, errors.New("unhandled arg type")
+}
+
 func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) {
 	if loh.EmptyList {
 		return &ListObject{}, nil
diff --git a/ucl/objs.go b/ucl/objs.go
index e2bfda9..c17e246 100644
--- a/ucl/objs.go
+++ b/ucl/objs.go
@@ -279,7 +279,7 @@ type macroArgs struct {
 }
 
 func (ma macroArgs) nargs() int {
-	return len(ma.ast.Args[ma.argShift:])
+	return len(ma.ast.InvokeArgs[ma.argShift:])
 }
 
 func (ma *macroArgs) shift(n int) {
@@ -287,15 +287,15 @@ func (ma *macroArgs) shift(n int) {
 }
 
 func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bool {
-	if n >= len(ma.ast.Args[ma.argShift:]) {
+	if n >= len(ma.ast.InvokeArgs[ma.argShift:]) {
 		return false
 	}
 
-	if len(ma.ast.Args[ma.argShift+n].DotSuffix) != 0 {
+	if len(ma.ast.InvokeArgs[ma.argShift+n].DotSuffix) != 0 {
 		return false
 	}
 
-	lit := ma.ast.Args[ma.argShift+n].Arg.Ident
+	lit := ma.ast.InvokeArgs[ma.argShift+n].Arg.Ident
 	if lit == nil {
 		return false
 	}
@@ -304,15 +304,15 @@ func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bo
 }
 
 func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) {
-	if ma.argShift >= len(ma.ast.Args) {
+	if ma.argShift >= len(ma.ast.InvokeArgs) {
 		return "", false
 	}
 
-	if len(ma.ast.Args[ma.argShift].DotSuffix) != 0 {
+	if len(ma.ast.InvokeArgs[ma.argShift].DotSuffix) != 0 {
 		return "", false
 	}
 
-	lit := ma.ast.Args[ma.argShift].Arg.Ident
+	lit := ma.ast.InvokeArgs[ma.argShift].Arg.Ident
 	if lit != nil {
 		ma.argShift += 1
 		return lit.String(), true
@@ -321,11 +321,11 @@ func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) {
 }
 
 func (ma macroArgs) evalArg(ctx context.Context, n int) (Object, error) {
-	if n >= len(ma.ast.Args[ma.argShift:]) {
+	if n >= len(ma.ast.InvokeArgs[ma.argShift:]) {
 		return nil, errors.New("not enough arguments") // FIX
 	}
 
-	return ma.eval.evalDot(ctx, ma.ec, ma.ast.Args[ma.argShift+n])
+	return ma.eval.evalDot(ctx, ma.ec, ma.ast.InvokeArgs[ma.argShift+n])
 }
 
 func (ma macroArgs) evalBlock(ctx context.Context, n int, args []Object, pushScope bool) (Object, error) {