Repository: benhoyt/goawk (go)

Issue:
Error parsing "cond && var=value"
This is valid in Gawk, onetrueawk, and mawk, but not in GoAWK (incidentally, frawk has the same issue).

```
$ goawk 'BEGIN { if (1 && x=1) print "t" }'
<cmdline>:1:19: expected ) instead of =
BEGIN { if (1 && x=1) print "t" }
                  ^

# Compare
$ gawk 'BEGIN { if (1 && x=1) print "t" }'
t
```

Discovered from the results at https://github.com/juntuu/advent_of_code_2022 (see [results](https://user-images.githubusercontent.com/47396753/209881903-28a16215-0cba-4194-be32-5bd88917a063.png))

There are probably more issues this repo has found in GoAWK -- would be worth going through all the failures to find other unique issues.

Pull request description:
Allow parsing of "cond && var=value" and similar expressions
Expressions like "1 && x=1" aren't really valid (IMO), because assignments are lower-precedence than binary operators, but onetrueawk, Gawk, and mawk all support this for logical, match and comparison operators.

The other awks support this by using a yacc grammar which supports backtracking, and as [Vitus13 said on reddit](https://www.reddit.com/r/ProgrammingLanguages/comments/10zfg1z/why_does_awk_parse_1x1_as_1x1_not_1x1_when_is/): "If there are two syntactically valid parsings and one is a semantic error, the error handling may resolve the ambiguity towards the valid parsing. In this case, you can only assign to L values, so trying to assign to (1&&x) doesn't make any sense."

In GoAWK, this requires a form of backtracking (I call it "partial backtracking" because it's not actually backing up the lexer). It works by parsing as (1&&x)=1 according to the operator precedence, then determining that you're trying to assign something that isn't an lvalue, then confirming that 1&&x is a binary expression, that the "x" part is an lvalue, and that the operator (&& in this case) is one that the other awks handle similarly for this case.

Also make the error message a bit clearer when you don't have an lvalue on the left hand side of an assignment, like "rand() = 1".

Fixes #166

Patch:
diff --git a/parser/parser.go b/parser/parser.go
index b863df5..137e20c 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -530,37 +530,57 @@ func (p *parser) getLine() ast.Expr {
 // An lvalue is a variable name, an array[expr] index expression, or
 // an $expr field expression.
 func (p *parser) _assign(higher func() ast.Expr) ast.Expr {
+	leftPos := p.pos
 	expr := higher()
-	_, isNamedField := expr.(*ast.NamedFieldExpr)
-	if (isNamedField || ast.IsLValue(expr)) && p.matches(ASSIGN, ADD_ASSIGN, DIV_ASSIGN,
-		MOD_ASSIGN, MUL_ASSIGN, POW_ASSIGN, SUB_ASSIGN) {
+	if p.matches(ASSIGN, ADD_ASSIGN, DIV_ASSIGN, MOD_ASSIGN, MUL_ASSIGN, POW_ASSIGN, SUB_ASSIGN) {
+		_, isNamedField := expr.(*ast.NamedFieldExpr)
 		if isNamedField {
 			panic(p.errorf("assigning @ expression not supported"))
 		}
 		op := p.tok
 		p.next()
 		right := p._assign(higher)
-		switch op {
-		case ASSIGN:
-			return &ast.AssignExpr{expr, right}
-		case ADD_ASSIGN:
-			op = ADD
-		case DIV_ASSIGN:
-			op = DIV
-		case MOD_ASSIGN:
-			op = MOD
-		case MUL_ASSIGN:
-			op = MUL
-		case POW_ASSIGN:
-			op = POW
-		case SUB_ASSIGN:
-			op = SUB
+		if !ast.IsLValue(expr) {
+			// Partial backtracking to allow expressions like "1 && x=1",
+			// which isn't really valid, as assignments are lower-precedence
+			// than binary operators, but onetrueawk, Gawk, and mawk all
+			// support this for logical, match and comparison operators. See
+			// issue #166.
+			binary, isBinary := expr.(*ast.BinaryExpr)
+			if isBinary && ast.IsLValue(binary.Right) {
+				switch binary.Op {
+				case AND, OR, MATCH, NOT_MATCH, EQUALS, NOT_EQUALS, LESS, LTE, GTE, GREATER:
+					assign := makeAssign(binary.Right, op, right)
+					return &ast.BinaryExpr{binary.Left, binary.Op, assign}
+				}
+			}
+			panic(ast.PosErrorf(leftPos, "expected lvalue before %s", op))
 		}
-		return &ast.AugAssignExpr{expr, op, right}
+		return makeAssign(expr, op, right)
 	}
 	return expr
 }
 
+func makeAssign(left ast.Expr, op Token, right ast.Expr) ast.Expr {
+	switch op {
+	case ASSIGN:
+		return &ast.AssignExpr{left, right}
+	case ADD_ASSIGN:
+		op = ADD
+	case DIV_ASSIGN:
+		op = DIV
+	case MOD_ASSIGN:
+		op = MOD
+	case MUL_ASSIGN:
+		op = MUL
+	case POW_ASSIGN:
+		op = POW
+	case SUB_ASSIGN:
+		op = SUB
+	}
+	return &ast.AugAssignExpr{left, op, right}
+}
+
 // Parse a ?: conditional expression:
 //
 //	or [QUESTION NEWLINE* cond COLON NEWLINE* cond]


Test patch:
diff --git a/interp/interp_test.go b/interp/interp_test.go
index ed4fc05..28e82e7 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -470,6 +470,10 @@ BEGIN {
 	{`function inca(a, k, n) { a[k] += n }  BEGIN { b["x"]=7; inca(b, "x", 2); print b["x"] }`, "", "9\n", "", ""},
 	{`BEGIN { NF += 3; print NF }`, "", "3\n", "", ""},
 	{`BEGIN { x=1; x += x+=3; print x }`, "", "8\n", "", ""},
+	{`BEGIN { if (1&&x=2) print "t", x }`, "", "t 2\n", "", ""},
+	{`BEGIN { if (0||x+=2) print "t", x }`, "", "t 2\n", "", ""},
+	{`BEGIN { print(1&&x=2, 1||x=2, 1~x=2, 1!~x=2, 1==x=2, 1!=x=2, 1<x=2, 1<=x=2, 1>x=2, 1>=x=2); print x }`,
+		"", "1 1 0 1 0 1 1 1 0 0\n2\n", "", ""},
 
 	// Incr/decr expressions
 	{`BEGIN { print x++; print x }`, "", "0\n1\n", "", ""},
@@ -843,6 +847,8 @@ BEGIN { foo(5); bar(10) }
 	{`BEGIN { print 1&*2 }`, "", "", "parse error at 1:17: unexpected char after '&'", "syntax"},
 	{"BEGIN { ` }", "", "", "parse error at 1:9: unexpected char", "invalid char"},
 	{"BEGIN { ++3 }", "", "", "parse error at 1:11: expected lvalue after ++", "syntax"},
+	{"BEGIN { rand() = 1 }", "", "", "parse error at 1:9: expected lvalue before =", "syntax"},
+	{"BEGIN { 1 && rand()=1 }", "", "", "parse error at 1:9: expected lvalue before =", "syntax"},
 
 	// Hex floating point and other number conversions
 	{`{ print $1+0 }  # +posix`, `
diff --git a/testdata/gawk/badassign1.ok b/testdata/gawk/badassign1.ok
index 88b0f87..caca7ae 100644
--- a/testdata/gawk/badassign1.ok
+++ b/testdata/gawk/badassign1.ok
@@ -1,1 +1,1 @@
-parse error at 1:14: expected ; or newline between statements
\ No newline at end of file
+parse error at 1:9: expected lvalue before =
\ No newline at end of file