Repository: benhoyt/goawk (go)

Issue:
Add support for "getline lvalue"
Currently we support most getline forms, but not `getline lvalue`. For example, see the commented-out tests in `interp/interp_test.go`:

```
		// TODO: currently we support "getline var" but not "getline lvalue"
		// TODO: {`BEGIN { getline a[1]; print a[1] }`, "foo", "foo\n", "", ""},
		// TODO: {`BEGIN { getline $1; print $1 }`, "foo", "foo\n", "", ""},
```

Pull request description:
Add support for "getline lvalue"
Parsing getline is tricky, and I needed to add a PeekByte function to
the lexer so the parser can tell the difference between "getline foo"
and "getline foo()".

There are still a few arcane forms not supported by GoAWK (see
commented-out interp tests), but most forms are supported now.

Also ensure data returned by getline is treated as numeric string.

Fixes #48.

Patch:
diff --git a/internal/ast/ast.go b/internal/ast/ast.go
index 1d707c4..1007d44 100644
--- a/internal/ast/ast.go
+++ b/internal/ast/ast.go
@@ -299,7 +299,7 @@ func (e *MultiExpr) String() string {
 // GetlineExpr is an expression read from file or pipe input.
 type GetlineExpr struct {
 	Command Expr
-	Var     *VarExpr
+	Target  Expr
 	File    Expr
 }
 
@@ -309,8 +309,8 @@ func (e *GetlineExpr) String() string {
 		s += e.Command.String() + " |"
 	}
 	s += "getline"
-	if e.Var != nil {
-		s += " " + e.Var.String()
+	if e.Target != nil {
+		s += " " + e.Target.String()
 	}
 	if e.File != nil {
 		s += " <" + e.File.String()
diff --git a/interp/interp.go b/interp/interp.go
index 82ca600..4a29d63 100644
--- a/interp/interp.go
+++ b/interp/interp.go
@@ -910,8 +910,8 @@ func (p *interp) eval(expr Expr) (value, error) {
 				return num(-1), nil
 			}
 		}
-		if e.Var != nil {
-			err := p.setVar(e.Var.Scope, e.Var.Index, str(line))
+		if e.Target != nil {
+			err := p.assign(e.Target, numStr(line))
 			if err != nil {
 				return null(), err
 			}
diff --git a/lexer/lexer.go b/lexer/lexer.go
index 656f888..30147e9 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -453,3 +453,9 @@ func (l *Lexer) choice(ch byte, one, two Token) Token {
 	}
 	return one
 }
+
+// PeekByte returns the next unscanned byte; used when parsing
+// "getline lvalue" expressions. Returns 0 at end of input.
+func (l *Lexer) PeekByte() byte {
+	return l.ch
+}
diff --git a/parser/parser.go b/parser/parser.go
index 92ca198..d8ff2c6 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -235,7 +235,7 @@ func (p *parser) simpleStmt() Stmt {
 			return &PrintStmt{args, redirect, dest}
 		} else {
 			if len(args) == 0 {
-				panic(p.error("expected printf args, got none"))
+				panic(p.errorf("expected printf args, got none"))
 			}
 			return &PrintfStmt{args, redirect, dest}
 		}
@@ -248,13 +248,13 @@ func (p *parser) simpleStmt() Stmt {
 			p.next()
 			index = p.exprList(p.expr)
 			if len(index) == 0 {
-				panic(p.error("expected expression instead of ]"))
+				panic(p.errorf("expected expression instead of ]"))
 			}
 			p.expect(RBRACKET)
 		}
 		return &DeleteStmt{ref, index}
 	case IF, FOR, WHILE, DO, BREAK, CONTINUE, NEXT, EXIT, RETURN:
-		panic(p.error("expected print/printf, delete, or expression"))
+		panic(p.errorf("expected print/printf, delete, or expression"))
 	default:
 		return &ExprStmt{p.expr()}
 	}
@@ -302,18 +302,18 @@ func (p *parser) stmt() Stmt {
 			p.optionalNewlines()
 			exprStmt, ok := pre.(*ExprStmt)
 			if !ok {
-				panic(p.error("expected 'for (var in array) ...'"))
+				panic(p.errorf("expected 'for (var in array) ...'"))
 			}
 			inExpr, ok := (exprStmt.Expr).(*InExpr)
 			if !ok {
-				panic(p.error("expected 'for (var in array) ...'"))
+				panic(p.errorf("expected 'for (var in array) ...'"))
 			}
 			if len(inExpr.Index) != 1 {
-				panic(p.error("expected 'for (var in array) ...'"))
+				panic(p.errorf("expected 'for (var in array) ...'"))
 			}
 			varExpr, ok := (inExpr.Index[0]).(*VarExpr)
 			if !ok {
-				panic(p.error("expected 'for (var in array) ...'"))
+				panic(p.errorf("expected 'for (var in array) ...'"))
 			}
 			body := p.loopStmts()
 			s = &ForInStmt{varExpr, inExpr.Array, body}
@@ -355,19 +355,19 @@ func (p *parser) stmt() Stmt {
 		s = &DoWhileStmt{body, cond}
 	case BREAK:
 		if p.loopDepth == 0 {
-			panic(p.error("break must be inside a loop body"))
+			panic(p.errorf("break must be inside a loop body"))
 		}
 		p.next()
 		s = &BreakStmt{}
 	case CONTINUE:
 		if p.loopDepth == 0 {
-			panic(p.error("continue must be inside a loop body"))
+			panic(p.errorf("continue must be inside a loop body"))
 		}
 		p.next()
 		s = &ContinueStmt{}
 	case NEXT:
 		if !p.inAction && p.funcName == "" {
-			panic(p.error("next can't be inside BEGIN or END"))
+			panic(p.errorf("next can't be inside BEGIN or END"))
 		}
 		p.next()
 		s = &NextStmt{}
@@ -380,7 +380,7 @@ func (p *parser) stmt() Stmt {
 		s = &ExitStmt{status}
 	case RETURN:
 		if p.funcName == "" {
-			panic(p.error("return must be inside a function"))
+			panic(p.errorf("return must be inside a function"))
 		}
 		p.next()
 		var value Expr
@@ -397,7 +397,7 @@ func (p *parser) stmt() Stmt {
 
 	// Ensure statements are separated by ; or newline
 	if !p.matches(NEWLINE, SEMICOLON, RBRACE) && p.prevTok != NEWLINE && p.prevTok != SEMICOLON && p.prevTok != RBRACE {
-		panic(p.error("expected ; or newline between statements"))
+		panic(p.errorf("expected ; or newline between statements"))
 	}
 	for p.matches(NEWLINE, SEMICOLON) {
 		p.next()
@@ -421,12 +421,12 @@ func (p *parser) function() Function {
 	if p.funcName != "" {
 		// Should never actually get here (FUNCTION token is only
 		// handled at the top level), but just in case.
-		panic(p.error("can't nest functions"))
+		panic(p.errorf("can't nest functions"))
 	}
 	p.next()
 	name := p.val
 	if _, ok := p.functions[name]; ok {
-		panic(p.error("function %q already defined", name))
+		panic(p.errorf("function %q already defined", name))
 	}
 	p.expect(NAME)
 	p.expect(LPAREN)
@@ -440,10 +440,10 @@ func (p *parser) function() Function {
 		first = false
 		param := p.val
 		if param == name {
-			panic(p.error("can't use function name as parameter name"))
+			panic(p.errorf("can't use function name as parameter name"))
 		}
 		if p.locals[param] {
-			panic(p.error("duplicate parameter name %q", param))
+			panic(p.errorf("duplicate parameter name %q", param))
 		}
 		p.expect(NAME)
 		params = append(params, param)
@@ -487,21 +487,17 @@ func (p *parser) exprList(parse func() Expr) []Expr {
 func (p *parser) expr() Expr      { return p.getLine() }
 func (p *parser) printExpr() Expr { return p._assign(p.printCond) }
 
-// Parse an "expr | getline [var]" expression:
+// Parse an "expr | getline [lvalue]" expression:
 //
-//     assign [PIPE GETLINE [NAME]]
+//     assign [PIPE GETLINE [lvalue]]
 //
 func (p *parser) getLine() Expr {
 	expr := p._assign(p.cond)
 	if p.tok == PIPE {
 		p.next()
 		p.expect(GETLINE)
-		var varExpr *VarExpr
-		if p.tok == NAME {
-			varExpr = p.varRef(p.val, p.pos)
-			p.next()
-		}
-		return &GetlineExpr{expr, varExpr, nil}
+		target := p.optionalLValue()
+		return &GetlineExpr{expr, target, nil}
 	}
 	return expr
 }
@@ -663,9 +659,10 @@ func (p *parser) preIncr() Expr {
 	if p.tok == INCR || p.tok == DECR {
 		op := p.tok
 		p.next()
+		exprPos := p.pos
 		expr := p.preIncr()
 		if !IsLValue(expr) {
-			panic(p.error("expected lvalue after ++ or --"))
+			panic(p.posErrorf(exprPos, "expected lvalue after ++ or --"))
 		}
 		return &IncrExpr{expr, op, true}
 	}
@@ -715,13 +712,13 @@ func (p *parser) primary() Expr {
 			p.next()
 			index := p.exprList(p.expr)
 			if len(index) == 0 {
-				panic(p.error("expected expression instead of ]"))
+				panic(p.errorf("expected expression instead of ]"))
 			}
 			p.expect(RBRACKET)
 			return &IndexExpr{p.arrayRef(name, namePos), index}
 		} else if p.tok == LPAREN && !p.lexer.HadSpace() {
 			if p.locals[name] {
-				panic(p.error("can't call local variable %q as function", name))
+				panic(p.errorf("can't call local variable %q as function", name))
 			}
 			// Grammar requires no space between function name and
 			// left paren for user function calls, hence the funky
@@ -735,7 +732,7 @@ func (p *parser) primary() Expr {
 		exprs := p.exprList(p.expr)
 		switch len(exprs) {
 		case 0:
-			panic(p.error("expected expression, not %s", p.tok))
+			panic(p.errorf("expected expression, not %s", p.tok))
 		case 1:
 			p.expect(RPAREN)
 			return exprs[0]
@@ -753,17 +750,13 @@ func (p *parser) primary() Expr {
 		}
 	case GETLINE:
 		p.next()
-		var varExpr *VarExpr
-		if p.tok == NAME {
-			varExpr = p.varRef(p.val, p.pos)
-			p.next()
-		}
+		target := p.optionalLValue()
 		var file Expr
 		if p.tok == LESS {
 			p.next()
-			file = p.expr()
+			file = p.primary()
 		}
-		return &GetlineExpr{nil, varExpr, file}
+		return &GetlineExpr{nil, target, file}
 	// Below is the parsing of all the builtin function calls. We
 	// could unify these but several of them have special handling
 	// (array/lvalue/regex params, optional arguments, and so on).
@@ -778,9 +771,10 @@ func (p *parser) primary() Expr {
 		args := []Expr{regex, repl}
 		if p.tok == COMMA {
 			p.commaNewlines()
+			inPos := p.pos
 			in := p.expr()
 			if !IsLValue(in) {
-				panic(p.error("3rd arg to sub/gsub must be lvalue"))
+				panic(p.posErrorf(inPos, "3rd arg to sub/gsub must be lvalue"))
 			}
 			args = append(args, in)
 		}
@@ -885,7 +879,37 @@ func (p *parser) primary() Expr {
 		p.expect(RPAREN)
 		return &CallExpr{op, []Expr{arg1, arg2}}
 	default:
-		panic(p.error("expected expression instead of %s", p.tok))
+		panic(p.errorf("expected expression instead of %s", p.tok))
+	}
+}
+
+// Parse an optional lvalue
+func (p *parser) optionalLValue() Expr {
+	switch p.tok {
+	case NAME:
+		if p.lexer.PeekByte() == '(' {
+			// User function call, e.g., foo() not lvalue.
+			return nil
+		}
+		name := p.val
+		namePos := p.pos
+		p.next()
+		if p.tok == LBRACKET {
+			// a[x] or a[x, y] array index expression
+			p.next()
+			index := p.exprList(p.expr)
+			if len(index) == 0 {
+				panic(p.errorf("expected expression instead of ]"))
+			}
+			p.expect(RBRACKET)
+			return &IndexExpr{p.arrayRef(name, namePos), index}
+		}
+		return p.varRef(name, namePos)
+	case DOLLAR:
+		p.next()
+		return &FieldExpr{p.primary()}
+	default:
+		return nil
 	}
 }
 
@@ -944,7 +968,7 @@ func (p *parser) next() {
 	p.prevTok = p.tok
 	p.pos, p.tok, p.val = p.lexer.Scan()
 	if p.tok == ILLEGAL {
-		panic(p.error("%s", p.val))
+		panic(p.errorf("%s", p.val))
 	}
 }
 
@@ -953,12 +977,12 @@ func (p *parser) next() {
 func (p *parser) nextRegex() string {
 	p.pos, p.tok, p.val = p.lexer.ScanRegex()
 	if p.tok == ILLEGAL {
-		panic(p.error("%s", p.val))
+		panic(p.errorf("%s", p.val))
 	}
 	regex := p.val
 	_, err := regexp.Compile(regex)
 	if err != nil {
-		panic(p.error("%v", err))
+		panic(p.errorf("%v", err))
 	}
 	p.next()
 	return regex
@@ -967,7 +991,7 @@ func (p *parser) nextRegex() string {
 // Ensure current token is tok, and parse next token into p.tok.
 func (p *parser) expect(tok Token) {
 	if p.tok != tok {
-		panic(p.error("expected %s instead of %s", tok, p.tok))
+		panic(p.errorf("expected %s instead of %s", tok, p.tok))
 	}
 	p.next()
 }
@@ -985,9 +1009,14 @@ func (p *parser) matches(operators ...Token) bool {
 
 // Format given string and args with Sprintf and return *ParseError
 // with that message and the current position.
-func (p *parser) error(format string, args ...interface{}) error {
+func (p *parser) errorf(format string, args ...interface{}) error {
+	return p.posErrorf(p.pos, format, args...)
+}
+
+// Like errorf, but with an explicit position.
+func (p *parser) posErrorf(pos Position, format string, args ...interface{}) error {
 	message := fmt.Sprintf(format, args...)
-	return &ParseError{p.pos, message}
+	return &ParseError{pos, message}
 }
 
 // Parse call to a user-defined function (and record call site for
diff --git a/parser/resolve.go b/parser/resolve.go
index 1e4a0a5..6543633 100644
--- a/parser/resolve.go
+++ b/parser/resolve.go
@@ -129,11 +129,11 @@ func (p *parser) resolveUserCalls(prog *Program) {
 		if !ok {
 			f, haveNative := p.nativeFuncs[c.call.Name]
 			if !haveNative {
-				panic(&ParseError{c.pos, fmt.Sprintf("undefined function %q", c.call.Name)})
+				panic(p.posErrorf(c.pos, "undefined function %q", c.call.Name))
 			}
 			typ := reflect.TypeOf(f)
 			if !typ.IsVariadic() && len(c.call.Args) > typ.NumIn() {
-				panic(&ParseError{c.pos, fmt.Sprintf("%q called with more arguments than declared", c.call.Name)})
+				panic(p.posErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
 			}
 			c.call.Native = true
 			c.call.Index = nativeIndexes[c.call.Name]
@@ -141,7 +141,7 @@ func (p *parser) resolveUserCalls(prog *Program) {
 		}
 		function := prog.Functions[index]
 		if len(c.call.Args) > len(function.Params) {
-			panic(&ParseError{c.pos, fmt.Sprintf("%q called with more arguments than declared", c.call.Name)})
+			panic(p.posErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
 		}
 		c.call.Index = index
 	}
@@ -195,7 +195,7 @@ func (p *parser) varRef(name string, pos Position) *VarExpr {
 func (p *parser) arrayRef(name string, pos Position) *ArrayExpr {
 	scope, funcName := p.getScope(name)
 	if scope == ScopeSpecial {
-		panic(p.error("can't use scalar %q as array", name))
+		panic(p.errorf("can't use scalar %q as array", name))
 	}
 	expr := &ArrayExpr{scope, 0, name}
 	p.arrayRefs = append(p.arrayRefs, arrayRef{funcName, expr, pos})
@@ -278,7 +278,7 @@ func (p *parser) resolveVars(prog *Program) {
 			break
 		}
 		if i >= maxResolveIterations {
-			panic(p.error("too many iterations trying to resolve variable types"))
+			panic(p.errorf("too many iterations trying to resolve variable types"))
 		}
 	}
 
@@ -290,7 +290,7 @@ func (p *parser) resolveVars(prog *Program) {
 		_, isFunc := p.functions[name]
 		if isFunc {
 			// Global var can't also be the name of a function
-			panic(p.error("global var %q can't also be a function", name))
+			panic(p.errorf("global var %q can't also be a function", name))
 		}
 		var index int
 		if info.scope == ScopeSpecial {
@@ -376,8 +376,7 @@ func (p *parser) resolveVars(prog *Program) {
 				funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
 				info := p.varTypes[funcName][varExpr.Name]
 				if info.typ == typeArray {
-					message := fmt.Sprintf("can't pass array %q to native function", varExpr.Name)
-					panic(&ParseError{c.pos, message})
+					panic(p.posErrorf(c.pos, "can't pass array %q to native function", varExpr.Name))
 				}
 			}
 			continue
@@ -389,20 +388,17 @@ func (p *parser) resolveVars(prog *Program) {
 			varExpr, ok := arg.(*VarExpr)
 			if !ok {
 				if function.Arrays[i] {
-					message := fmt.Sprintf("can't pass scalar %s as array param", arg)
-					panic(&ParseError{c.pos, message})
+					panic(p.posErrorf(c.pos, "can't pass scalar %s as array param", arg))
 				}
 				continue
 			}
 			funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
 			info := p.varTypes[funcName][varExpr.Name]
 			if info.typ == typeArray && !function.Arrays[i] {
-				message := fmt.Sprintf("can't pass array %q as scalar param", varExpr.Name)
-				panic(&ParseError{c.pos, message})
+				panic(p.posErrorf(c.pos, "can't pass array %q as scalar param", varExpr.Name))
 			}
 			if info.typ != typeArray && function.Arrays[i] {
-				message := fmt.Sprintf("can't pass scalar %q as array param", varExpr.Name)
-				panic(&ParseError{c.pos, message})
+				panic(p.posErrorf(c.pos, "can't pass scalar %q as array param", varExpr.Name))
 			}
 		}
 	}
@@ -416,16 +412,14 @@ func (p *parser) resolveVars(prog *Program) {
 	for _, varRef := range p.varRefs {
 		info := p.varTypes[varRef.funcName][varRef.ref.Name]
 		if info.typ == typeArray && !varRef.isArg {
-			message := fmt.Sprintf("can't use array %q as scalar", varRef.ref.Name)
-			panic(&ParseError{varRef.pos, message})
+			panic(p.posErrorf(varRef.pos, "can't use array %q as scalar", varRef.ref.Name))
 		}
 		varRef.ref.Index = info.index
 	}
 	for _, arrayRef := range p.arrayRefs {
 		info := p.varTypes[arrayRef.funcName][arrayRef.ref.Name]
 		if info.typ == typeScalar {
-			message := fmt.Sprintf("can't use scalar %q as array", arrayRef.ref.Name)
-			panic(&ParseError{arrayRef.pos, message})
+			panic(p.posErrorf(arrayRef.pos, "can't use scalar %q as array", arrayRef.ref.Name))
 		}
 		arrayRef.ref.Index = info.index
 	}
@@ -470,5 +464,5 @@ func (p *parser) checkMultiExprs() {
 			min = pos
 		}
 	}
-	panic(&ParseError{min, "unexpected comma-separated expression"})
+	panic(p.posErrorf(min, "unexpected comma-separated expression"))
 }


Test patch:
diff --git a/goawk_test.go b/goawk_test.go
index 192a94f..62e243f 100644
--- a/goawk_test.go
+++ b/goawk_test.go
@@ -218,11 +218,10 @@ func sortedLines(data []byte) []byte {
 
 func TestGAWK(t *testing.T) {
 	skip := map[string]bool{ // TODO: fix these
-		"inputred": true, // getInputScanner errors
-
 		"getline":  true, // getline syntax issues (may be okay, see grammar notes at http://pubs.opengroup.org/onlinepubs/007904975/utilities/awk.html#tag_04_06_13_14)
 		"getline3": true, // getline syntax issues (similar to above)
 		"getline5": true, // getline syntax issues (similar to above)
+		"inputred": true, // getline syntax issues (similar to above)
 
 		"gsubtst7":     true, // something wrong with gsub or field split/join
 		"splitwht":     true, // other awks handle split(s, a, " ") differently from split(s, a, / /)
diff --git a/interp/interp_test.go b/interp/interp_test.go
index 950dca1..6c971b7 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -596,18 +596,36 @@ BEGIN { foo(5); bar(10) }
 		"", "", `parse error at 1:46: global var "foo" can't also be a function`, "function"},
 	{`function f(x) { print x, x(); }  BEGIN { f() }`, "", "", `parse error at 1:27: can't call local variable "x" as function`, "function"},
 
-	// Redirected I/O (we give explicit errors, awk and gawk don't)
-	{`BEGIN { print >"out"; getline <"out" }  # !awk !gawk`, "", "", "can't read from writer stream", ""},
-	{`BEGIN { print |"out"; getline <"out" }  # !awk !gawk`, "", "", "can't read from writer stream", ""},
-	{`BEGIN { print >"out"; close("out"); getline <"out"; print >"out" }  # !awk !gawk`, "", "", "can't write to reader stream", ""},
-	{`BEGIN { print >"out"; close("out"); getline <"out"; print |"out" }  # !awk !gawk`, "", "", "can't write to reader stream", ""},
-	// TODO: currently we support "getline var" but not "getline lvalue"
-	// {`BEGIN { getline a[1]; print a[1] }`, "foo", "foo\n", "", ""},
-	// {`BEGIN { getline $1; print $1 }`, "foo", "foo\n", "", ""},
+	// Redirected I/O
+	{`BEGIN { getline a[1]; print a[1] }`, "foo", "foo\n", "", ""},
+	{`BEGIN { getline $1; print $1 }`, "foo", "foo\n", "", ""},
+	{`BEGIN { "echo foo" | getline a[1]; print a[1] }`, "", "foo\n", "", ""},
+	{`BEGIN { "echo foo" | getline $1; print $1 }`, "", "foo\n", "", ""},
 	{`BEGIN { print "foo" |"sort"; print "bar" |"sort" }  # !fuzz`, "", "bar\nfoo\n", "", ""},
 	{`BEGIN { print "foo" |">&2 echo error" }  # !gawk !fuzz`, "", "error\n", "", ""},
 	{`BEGIN { "cat" | getline; print }  # !fuzz`, "bar", "bar\n", "", ""},
 	{`BEGIN { print getline x < "/no/such/file" }  # !fuzz`, "", "-1\n", "", ""},
+	{`BEGIN { print getline "z"; print $0 }`, "foo", "1z\nfoo\n", "", ""},
+	{`BEGIN { print getline x+1; print x }`, "foo", "2\nfoo\n", "", ""},
+	{`BEGIN { print getline (x+1); print $0 }`, "foo", "11\nfoo\n", "", ""},
+	{`BEGIN { print getline foo(); print $0 } function foo() { print "z" }`, "foo", "z\n1\nfoo\n", "", ""},
+	// TODO: these forms don't yet work under GoAWK
+	//{`BEGIN { print("echo foo" | getline x+1); print x }`, "", "2\nfoo\n", "", ""},
+	//{`BEGIN { print("echo foo" | getline $0+1); print }`, "", "2\nfoo\n", "", ""},
+	//{`BEGIN { print("echo foo" | getline ($0+1)); print }`, "", "11\nfoo\n", "", ""},
+	//{`BEGIN { print("echo foo" | getline foo()); print } function foo() { print "z" }`, "", "z\n1\nfoo\n", "", ""},
+
+	// Ensure data returned by getline (in various forms) is treated as numeric string
+	{`BEGIN { getline; print($0==0) }`, "0.0", "1\n", "", ""},
+	{`BEGIN { getline x; print(x==0) }`, "0.0", "1\n", "", ""},
+	{`BEGIN { "echo 0.0" | getline; print($0==0) }`, "", "1\n", "", ""},
+	{`BEGIN { "echo 0.0" | getline x; print(x==0) }`, "", "1\n", "", ""},
+
+	// Redirected I/O errors (we give explicit errors, awk and gawk don't)
+	{`BEGIN { print >"out"; getline <"out" }  # !awk !gawk`, "", "", "can't read from writer stream", ""},
+	{`BEGIN { print |"out"; getline <"out" }  # !awk !gawk`, "", "", "can't read from writer stream", ""},
+	{`BEGIN { print >"out"; close("out"); getline <"out"; print >"out" }  # !awk !gawk`, "", "", "can't write to reader stream", ""},
+	{`BEGIN { print >"out"; close("out"); getline <"out"; print |"out" }  # !awk !gawk`, "", "", "can't write to reader stream", ""},
 
 	// Redirecting to or from a filename of "-" means write to stdout or read from stdin
 	{`BEGIN { print getline x < "-"; print x }`, "a\nb\n", "1\na\n", "", ""},
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 89b4837..579f2f7 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -151,6 +151,34 @@ func TestHadSpace(t *testing.T) {
 	}
 }
 
+func TestPeekByte(t *testing.T) {
+	l := NewLexer([]byte("foo()"))
+	b := l.PeekByte()
+	if b != 'f' {
+		t.Errorf("expected 'f', got %q", b)
+	}
+	_, tok, _ := l.Scan()
+	if tok != NAME {
+		t.Errorf("expected name, got %s", tok)
+	}
+	b = l.PeekByte()
+	if b != '(' {
+		t.Errorf("expected '(', got %q", b)
+	}
+	_, tok, _ = l.Scan()
+	if tok != LPAREN {
+		t.Errorf("expected (, got %s", tok)
+	}
+	_, tok, _ = l.Scan()
+	if tok != RPAREN {
+		t.Errorf("expected ), got %s", tok)
+	}
+	b = l.PeekByte()
+	if b != 0 {
+		t.Errorf("expected 0, got %q", b)
+	}
+}
+
 func TestKeywordToken(t *testing.T) {
 	tests := []struct {
 		name string
diff --git a/parser/parser_test.go b/parser/parser_test.go
index af1a5c0..75146c2 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -63,10 +63,18 @@ $0 {
     next
     "cmd" |getline
     "cmd" |getline x
+    "cmd" |getline a[1]
+    "cmd" |getline $1
     getline
     getline x
+    (getline x + 1)
+    getline $1
+    getline a[1]
     getline <"file"
     getline x <"file"
+    (getline x <"file" "x")
+    getline $1 <"file"
+    getline a[1] <"file"
     x = 0
     y = z = 0
     b += 1