Repository: benhoyt/goawk (go)

Issue:
Allow length(array) in addition to length(string)
Apparently length() can't be called with an array argument currently. If I'm not mistaken, that's POSIX behaviour. Though, various other `awk`s support it. The following program:

	BEGIN {
		split("abc", a, "")
		for (i=1; i<=length(a); i++) {
			print a[i]
		}
		exit
	}

, run with `goawk`, will print `parse error: can't use array "a" as scalar`. As I noticed, I can work around it using `n = split(...`. So perhaps in this case it is not much of a problem, but I opened the issue nonetheless, just in case this isn't intended.

Pull request description:
Make length(x) also work when x is an array
Fixes #20.

Patch:
diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go
index 9f888e0..ecfcba6 100644
--- a/internal/compiler/compiler.go
+++ b/internal/compiler/compiler.go
@@ -783,6 +783,22 @@ func (c *compiler) expr(expr ast.Expr) {
 			c.add(CallBuiltin, Opcode(op))
 			c.assign(target)
 			return
+		case lexer.F_LENGTH:
+			if len(e.Args) > 0 {
+				// Determine if the call is length(arrayVar) or length(stringExpr).
+				if varExpr, ok := e.Args[0].(*ast.VarExpr); ok {
+					scope, info, _ := c.resolved.LookupVar(c.funcName, varExpr.Name)
+					if info.Type == resolver.Array {
+						c.add(CallLengthArray, Opcode(scope), opcodeInt(info.Index))
+						return
+					}
+				}
+				c.expr(e.Args[0])
+				c.add(CallBuiltin, Opcode(BuiltinLengthArg))
+			} else {
+				c.add(CallBuiltin, Opcode(BuiltinLength))
+			}
+			return
 		}
 
 		for _, arg := range e.Args {
@@ -807,12 +823,6 @@ func (c *compiler) expr(expr ast.Expr) {
 			c.add(CallBuiltin, Opcode(BuiltinIndex))
 		case lexer.F_INT:
 			c.add(CallBuiltin, Opcode(BuiltinInt))
-		case lexer.F_LENGTH:
-			if len(e.Args) > 0 {
-				c.add(CallBuiltin, Opcode(BuiltinLengthArg))
-			} else {
-				c.add(CallBuiltin, Opcode(BuiltinLength))
-			}
 		case lexer.F_LOG:
 			c.add(CallBuiltin, Opcode(BuiltinLog))
 		case lexer.F_MATCH:
diff --git a/internal/compiler/disassembler.go b/internal/compiler/disassembler.go
index 923244c..8f944a3 100644
--- a/internal/compiler/disassembler.go
+++ b/internal/compiler/disassembler.go
@@ -328,6 +328,11 @@ func (d *disassembler) disassemble(prefix string) error {
 			builtinOp := BuiltinOp(d.fetch())
 			d.writeOpf("CallBuiltin %s", builtinOp)
 
+		case CallLengthArray:
+			arrayScope := resolver.Scope(d.fetch())
+			arrayIndex := int(d.fetch())
+			d.writeOpf("CallLengthArray %s", d.arrayName(arrayScope, arrayIndex))
+
 		case CallSplit:
 			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
diff --git a/internal/compiler/opcode_string.go b/internal/compiler/opcode_string.go
index c58f27e..2fa8ec4 100644
--- a/internal/compiler/opcode_string.go
+++ b/internal/compiler/opcode_string.go
@@ -82,28 +82,29 @@ func _() {
 	_ = x[ForIn-71]
 	_ = x[BreakForIn-72]
 	_ = x[CallBuiltin-73]
-	_ = x[CallSplit-74]
-	_ = x[CallSplitSep-75]
-	_ = x[CallSprintf-76]
-	_ = x[CallUser-77]
-	_ = x[CallNative-78]
-	_ = x[Return-79]
-	_ = x[ReturnNull-80]
-	_ = x[Nulls-81]
-	_ = x[Print-82]
-	_ = x[Printf-83]
-	_ = x[Getline-84]
-	_ = x[GetlineField-85]
-	_ = x[GetlineGlobal-86]
-	_ = x[GetlineLocal-87]
-	_ = x[GetlineSpecial-88]
-	_ = x[GetlineArray-89]
-	_ = x[EndOpcode-90]
+	_ = x[CallLengthArray-74]
+	_ = x[CallSplit-75]
+	_ = x[CallSplitSep-76]
+	_ = x[CallSprintf-77]
+	_ = x[CallUser-78]
+	_ = x[CallNative-79]
+	_ = x[Return-80]
+	_ = x[ReturnNull-81]
+	_ = x[Nulls-82]
+	_ = x[Print-83]
+	_ = x[Printf-84]
+	_ = x[Getline-85]
+	_ = x[GetlineField-86]
+	_ = x[GetlineGlobal-87]
+	_ = x[GetlineLocal-88]
+	_ = x[GetlineSpecial-89]
+	_ = x[GetlineArray-90]
+	_ = x[EndOpcode-91]
 }
 
-const _Opcode_name = "NopNumStrDupeDropSwapFieldFieldIntFieldByNameFieldByNameStrGlobalLocalSpecialArrayGlobalArrayLocalInGlobalInLocalAssignFieldAssignGlobalAssignLocalAssignSpecialAssignArrayGlobalAssignArrayLocalDeleteDeleteAllIncrFieldIncrGlobalIncrLocalIncrSpecialIncrArrayGlobalIncrArrayLocalAugAssignFieldAugAssignGlobalAugAssignLocalAugAssignSpecialAugAssignArrayGlobalAugAssignArrayLocalRegexIndexMultiConcatMultiAddSubtractMultiplyDividePowerModuloEqualsNotEqualsLessGreaterLessOrEqualGreaterOrEqualConcatMatchNotMatchNotUnaryMinusUnaryPlusBooleanJumpJumpFalseJumpTrueJumpEqualsJumpNotEqualsJumpLessJumpGreaterJumpLessOrEqualJumpGreaterOrEqualNextNextfileExitForInBreakForInCallBuiltinCallSplitCallSplitSepCallSprintfCallUserCallNativeReturnReturnNullNullsPrintPrintfGetlineGetlineFieldGetlineGlobalGetlineLocalGetlineSpecialGetlineArrayEndOpcode"
+const _Opcode_name = "NopNumStrDupeDropSwapFieldFieldIntFieldByNameFieldByNameStrGlobalLocalSpecialArrayGlobalArrayLocalInGlobalInLocalAssignFieldAssignGlobalAssignLocalAssignSpecialAssignArrayGlobalAssignArrayLocalDeleteDeleteAllIncrFieldIncrGlobalIncrLocalIncrSpecialIncrArrayGlobalIncrArrayLocalAugAssignFieldAugAssignGlobalAugAssignLocalAugAssignSpecialAugAssignArrayGlobalAugAssignArrayLocalRegexIndexMultiConcatMultiAddSubtractMultiplyDividePowerModuloEqualsNotEqualsLessGreaterLessOrEqualGreaterOrEqualConcatMatchNotMatchNotUnaryMinusUnaryPlusBooleanJumpJumpFalseJumpTrueJumpEqualsJumpNotEqualsJumpLessJumpGreaterJumpLessOrEqualJumpGreaterOrEqualNextNextfileExitForInBreakForInCallBuiltinCallLengthArrayCallSplitCallSplitSepCallSprintfCallUserCallNativeReturnReturnNullNullsPrintPrintfGetlineGetlineFieldGetlineGlobalGetlineLocalGetlineSpecialGetlineArrayEndOpcode"
 
-var _Opcode_index = [...]uint16{0, 3, 6, 9, 13, 17, 21, 26, 34, 45, 59, 65, 70, 77, 88, 98, 106, 113, 124, 136, 147, 160, 177, 193, 199, 208, 217, 227, 236, 247, 262, 276, 290, 305, 319, 335, 355, 374, 379, 389, 400, 403, 411, 419, 425, 430, 436, 442, 451, 455, 462, 473, 487, 493, 498, 506, 509, 519, 528, 535, 539, 548, 556, 566, 579, 587, 598, 613, 631, 635, 643, 647, 652, 662, 673, 682, 694, 705, 713, 723, 729, 739, 744, 749, 755, 762, 774, 787, 799, 813, 825, 834}
+var _Opcode_index = [...]uint16{0, 3, 6, 9, 13, 17, 21, 26, 34, 45, 59, 65, 70, 77, 88, 98, 106, 113, 124, 136, 147, 160, 177, 193, 199, 208, 217, 227, 236, 247, 262, 276, 290, 305, 319, 335, 355, 374, 379, 389, 400, 403, 411, 419, 425, 430, 436, 442, 451, 455, 462, 473, 487, 493, 498, 506, 509, 519, 528, 535, 539, 548, 556, 566, 579, 587, 598, 613, 631, 635, 643, 647, 652, 662, 673, 688, 697, 709, 720, 728, 738, 744, 754, 759, 764, 770, 777, 789, 802, 814, 828, 840, 849}
 
 func (i Opcode) String() string {
 	if i < 0 || i >= Opcode(len(_Opcode_index)-1) {
diff --git a/internal/compiler/opcodes.go b/internal/compiler/opcodes.go
index e8938d9..41f1817 100644
--- a/internal/compiler/opcodes.go
+++ b/internal/compiler/opcodes.go
@@ -113,10 +113,11 @@ const (
 	BreakForIn
 
 	// Builtin functions
-	CallBuiltin  // builtinOp
-	CallSplit    // arrayScope arrayIndex
-	CallSplitSep // arrayScope arrayIndex
-	CallSprintf  // numArgs
+	CallBuiltin     // builtinOp
+	CallLengthArray // arrayScope arrayIndex
+	CallSplit       // arrayScope arrayIndex
+	CallSplitSep    // arrayScope arrayIndex
+	CallSprintf     // numArgs
 
 	// User and native functions
 	CallUser   // funcIndex numArrayArgs [arrayScope1 arrayIndex1 ...]
diff --git a/internal/resolver/resolve.go b/internal/resolver/resolve.go
index 8c1f9d3..da8a32e 100644
--- a/internal/resolver/resolve.go
+++ b/internal/resolver/resolve.go
@@ -404,6 +404,18 @@ func (v *mainVisitor) Visit(node ast.Node) ast.Visitor {
 			varExpr := n.Args[1].(*ast.VarExpr) // split()'s 2nd arg is always an array
 			v.r.recordVar(v.curFunc, varExpr.Name, Array, varExpr.Pos)
 			ast.WalkExprList(v, n.Args[2:])
+
+		case lexer.F_LENGTH:
+			if len(n.Args) > 0 {
+				if varExpr, ok := n.Args[0].(*ast.VarExpr); ok {
+					// In a call to length(x), x may be a scalar or an array,
+					// so set it to unknown for now.
+					v.r.recordVar(v.curFunc, varExpr.Name, unknown, varExpr.Pos)
+					return nil
+				}
+			}
+			ast.WalkExprList(v, n.Args)
+
 		default:
 			ast.WalkExprList(v, n.Args)
 		}
diff --git a/interp/vm.go b/interp/vm.go
index feb7b5f..2ab593c 100644
--- a/interp/vm.go
+++ b/interp/vm.go
@@ -637,6 +637,13 @@ func (p *interp) execute(code []compiler.Opcode) error {
 				return err
 			}
 
+		case compiler.CallLengthArray:
+			arrayScope := code[ip]
+			arrayIndex := code[ip+1]
+			ip += 2
+			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
+			p.push(num(float64(len(array))))
+
 		case compiler.CallSplit:
 			arrayScope := code[ip]
 			arrayIndex := code[ip+1]


Test patch:
diff --git a/interp/interp_test.go b/interp/interp_test.go
index 54874bb..c663965 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -513,6 +513,11 @@ BEGIN {
 	{`BEGIN { print match("x food y", /[fod]+/), RSTART, RLENGTH }`, "", "3 3 4\n", "", ""},
 	{`BEGIN { print match("a\nb\nc", /^a.*c$/), RSTART, RLENGTH }`, "", "1 1 5\n", "", ""},
 	{`{ print length, length(), length("buzz"), length("") }`, "foo bar", "7 7 4 0\n", "", ""},
+	{`{ a[$0]++ } END { print length(a) }  # !posix`, "a\nc\nb\na\na\nb", "3\n", "", ""},
+	{`BEGIN { 1 in a; print length(a); a[1]; a[2]=2; a[2]=3; print length(a) }  # !gawk !posix`, "", "0\n2\n", "", ""},
+	// There's a quirk in Gawk where "a" has to be referenced as an array first, so skip Gawk for now:
+	{`BEGIN { print length(a); a[1]; a[2]=2; a[2]=3; print length(a) }  # !gawk !posix`, "", "0\n2\n", "", ""},
+	{`BEGIN { a[1]=1; a[2]=2; a[3]=3; print len(a) } function len(x) { return length(x) }  # !posix`, "", "3\n", "", ""},
 	{`BEGIN { print index("foo", "f"), index("foo0", 0), index("foo", "o"), index("foo", "x") }`, "", "1 4 2 0\n", "", ""},
 	{`BEGIN { print atan2(1, 0.5), atan2(-1, 0) }`, "", "1.10715 -1.5708\n", "", ""},
 	{`BEGIN { print sprintf("%3d", 42) }`, "", " 42\n", "", ""},
diff --git a/testdata/gawk/funlen.awk b/testdata/gawk/funlen.awk
new file mode 100644
index 0000000..e881fcb
--- /dev/null
+++ b/testdata/gawk/funlen.awk
@@ -0,0 +1,69 @@
+# Date: Sat, 15 Mar 2008 16:21:19 +0100
+# From: Hermann Peifer <peifer@gmx.net>
+# Subject: [Fwd: Gawk length(array) bug]
+# To: bug-gawk@gnu.org
+# Cc: Aharon Robbins <arnold@skeeve.com>
+# Message-id: <47DBE96F.1060406@gmx.net>
+# 
+# See below. Regards, Hermann
+# 
+# -------- Original Message --------
+# Subject: Re: Gawk length(array) question
+# Date: Sat, 15 Mar 2008 08:02:03 -0500
+# From: Ed Morton <morton@lsupcaemnt.com>
+# Newsgroups: comp.lang.awk
+# References: <47DBAE29.4050709@gmx.eu>
+# 
+# On 3/15/2008 6:08 AM, Hermann Peifer wrote:
+# > Hi All,
+# > 
+# > The Gawk man page says:
+# >  > Starting with version 3.1.5, as a non-standard extension,
+# >  > with an array  argument, length() returns the number
+# >  > of elements in the array.
+# > 
+# > It looks like Gawk's length(array) extension does not work inside 
+# > functions. Is this a bug or feature or am I missing something? See the 
+# > example below. I am using GNU Awk 3.1.6
+# > 
+# > $ cat testdata
+# > CD NAME
+# > AT Austria
+# > BG Bulgaria
+# > CH Switzerland
+# > DE Germany
+# > EE Estonia
+# > FR France
+# > GR Greece
+# > 
+# > $ cat test.awk
+# > 
+# Populate array
+NR > 1 { array[$1] = $2 }
+
+# Print array length and call function A
+END { print "array:",length(array) ; A(array) }
+
+function A(array_A) { print "array_A:", length(array_A) }
+# > 
+# > $ gawk -f test.awk testdata
+# > array: 7
+# > gawk: test.awk:8: (FILENAME=data FNR=8) fatal: attempt to use array 
+# > `array_A (from array)' in a scalar context
+# > 
+# > BTW, there is no such error if I have asort(array_A) or asorti(array_A) 
+# > inside the function.
+# > 
+# > Hermann
+# 
+# I get the same result with gawk 3.1.6 for cygwin. Obviously you can work
+# around
+# it since asort() returns the number of elements in an array just like
+# length()
+# is supposed to (or "for (i in array) lgth++" if you don't want to be
+# gawk-specific) but it does seem like a bug. Anyone know if there's a list of
+# known gawk bugs on-line somewhere?
+# 
+# 	Ed.
+# 
+# 
diff --git a/testdata/gawk/funlen.in b/testdata/gawk/funlen.in
new file mode 100644
index 0000000..d0e0b04
--- /dev/null
+++ b/testdata/gawk/funlen.in
@@ -0,0 +1,8 @@
+CD NAME
+AT Austria
+BG Bulgaria
+CH Switzerland
+DE Germany
+EE Estonia
+FR France
+GR Greece
diff --git a/testdata/gawk/funlen.ok b/testdata/gawk/funlen.ok
new file mode 100644
index 0000000..2e2ba9d
--- /dev/null
+++ b/testdata/gawk/funlen.ok
@@ -0,0 +1,2 @@
+array: 7
+array_A: 7
diff --git a/testdata/gawk/tailrecurse.awk b/testdata/gawk/tailrecurse.awk
index c4d0f4f..b287d16 100644
--- a/testdata/gawk/tailrecurse.awk
+++ b/testdata/gawk/tailrecurse.awk
@@ -3,20 +3,13 @@ BEGIN {
 }
 
 
-function array_length(a,   k, n) {
-    n = 0
-    for (k in a) n++
-    return n
-}
-
 function abc(c, A, B)
 {
-	print "abc(" c ", " array_length(A) ")"
-	if (!c) {
+	print "abc(" c ", " length(A) ")"
+	if (! c--) {
 		return 
 	}
-    c--
-	B[""] = 1
-	print array_length(B)
+	B[""]
+	print length(B)
 	return abc(c, B)
 }
diff --git a/testdata/gawk/uninit5.awk b/testdata/gawk/uninit5.awk
index bf743f1..e63abe1 100644
--- a/testdata/gawk/uninit5.awk
+++ b/testdata/gawk/uninit5.awk
@@ -70,17 +70,11 @@
 # behaves differently, iterating once.  so, length() behaves differently
 # on two calls to the same variable, which was not changed.
 
-function array_length(a,   k, n) {
-    n = 0
-    for (k in a) n++
-    return n
-}
-
 function prt1(a, len)
 {
-    print "length:  " array_length(a)
+    print "length:  " length(a)
 
-    for (i = 1; i <= array_length(a); i++)
+    for (i = 1; i <= length(a); i++)
         printf "<" i "," a[i] "> "
 
     print "\n"
@@ -100,7 +94,7 @@ BEGIN {
 
 function prt(a, len)
 {
-    len = array_length(a)
+    len = length(a)
     print "length:  " len
 
     for (i = 1; i <= len; i++)