// Virtual machine: interpret GoAWK compiled opcodes

package interp

import (
	"io"
	"math"
	"os"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/benhoyt/goawk/internal/compiler"
	"github.com/benhoyt/goawk/internal/resolver"
	"github.com/benhoyt/goawk/lexer"
)

// Execute a block of virtual machine instructions.
//
// A big switch seems to be the best way of doing this for now. I also tried
// an array of functions (https://github.com/benhoyt/goawk/commit/8e04b069b621ff9b9456de57a35ff2fe335cf201)
// and it was ever so slightly faster, but the code was harder to work with
// and it won't be improved when Go gets faster switches via jump tables
// (https://go-review.googlesource.com/c/go/+/357330/).
//
// Additionally, I've made this version faster since the above test by
// reducing the number of opcodes (replacing a couple dozen Call* opcodes with
// a single CallBuiltin -- that probably pushed it below a switch binary tree
// branch threshold).
func (p *interp) execute(code []compiler.Opcode) error {
	for ip := 0; ip < len(code); {
		op := code[ip]
		ip++

		if p.checkCtx {
			err := p.checkContext()
			if err != nil {
				return err
			}
		}

		switch op {
		case compiler.Num:
			index := code[ip]
			ip++
			p.push(num(p.nums[index]))

		case compiler.Str:
			index := code[ip]
			ip++
			p.push(str(p.strs[index]))

		case compiler.Dupe:
			v := p.peekTop()
			p.push(v)

		case compiler.Drop:
			p.pop()

		case compiler.Swap:
			l, r := p.peekTwo()
			p.replaceTwo(r, l)

		case compiler.Rote:
			s := p.peekSlice(3)
			v0, v1, v2 := s[0], s[1], s[2]
			s[0], s[1], s[2] = v1, v2, v0

		case compiler.Field:
			index := p.peekTop()
			v := p.getField(int(index.num()))
			p.replaceTop(v)

		case compiler.FieldInt:
			index := code[ip]
			ip++
			v := p.getField(int(index))
			p.push(v)

		case compiler.FieldByName:
			fieldName := p.peekTop()
			field, err := p.getFieldByName(p.toString(fieldName))
			if err != nil {
				return err
			}
			p.replaceTop(field)

		case compiler.FieldByNameStr:
			index := code[ip]
			fieldName := p.strs[index]
			ip++
			field, err := p.getFieldByName(fieldName)
			if err != nil {
				return err
			}
			p.push(field)

		case compiler.Global:
			index := code[ip]
			ip++
			p.push(p.globals[index])

		case compiler.Local:
			index := code[ip]
			ip++
			p.push(p.frame[index])

		case compiler.Special:
			index := code[ip]
			ip++
			p.push(p.getSpecial(int(index)))

		case compiler.ArrayGlobal:
			arrayIndex := code[ip]
			ip++
			array := p.arrays[arrayIndex]
			index := p.toString(p.peekTop())
			v := arrayGet(array, index)
			p.replaceTop(v)

		case compiler.ArrayLocal:
			arrayIndex := code[ip]
			ip++
			array := p.localArray(int(arrayIndex))
			index := p.toString(p.peekTop())
			v := arrayGet(array, index)
			p.replaceTop(v)

		case compiler.InGlobal:
			arrayIndex := code[ip]
			ip++
			array := p.arrays[arrayIndex]
			index := p.toString(p.peekTop())
			_, ok := array[index]
			p.replaceTop(boolean(ok))

		case compiler.InLocal:
			arrayIndex := code[ip]
			ip++
			array := p.localArray(int(arrayIndex))
			index := p.toString(p.peekTop())
			_, ok := array[index]
			p.replaceTop(boolean(ok))

		case compiler.AssignField:
			right, index := p.popTwo()
			err := p.setField(int(index.num()), p.toString(right))
			if err != nil {
				return err
			}

		case compiler.AssignGlobal:
			index := code[ip]
			ip++
			p.globals[index] = p.pop()

		case compiler.AssignLocal:
			index := code[ip]
			ip++
			p.frame[index] = p.pop()

		case compiler.AssignSpecial:
			index := code[ip]
			ip++
			err := p.setSpecial(int(index), p.pop())
			if err != nil {
				return err
			}

		case compiler.AssignArrayGlobal:
			arrayIndex := code[ip]
			ip++
			array := p.arrays[arrayIndex]
			v, index := p.popTwo()
			array[p.toString(index)] = v

		case compiler.AssignArrayLocal:
			arrayIndex := code[ip]
			ip++
			array := p.localArray(int(arrayIndex))
			v, index := p.popTwo()
			array[p.toString(index)] = v

		case compiler.Delete:
			arrayScope := code[ip]
			arrayIndex := code[ip+1]
			ip += 2
			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
			index := p.toString(p.pop())
			delete(array, index)

		case compiler.DeleteAll:
			arrayScope := code[ip]
			arrayIndex := code[ip+1]
			ip += 2
			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
			for k := range array {
				delete(array, k)
			}

		case compiler.IncrField:
			amount := code[ip]
			ip++
			index := int(p.pop().num())
			v := p.getField(index)
			err := p.setField(index, p.toString(num(v.num()+float64(amount))))
			if err != nil {
				return err
			}

		case compiler.IncrGlobal:
			amount := code[ip]
			index := code[ip+1]
			ip += 2
			p.globals[index] = num(p.globals[index].num() + float64(amount))

		case compiler.IncrLocal:
			amount := code[ip]
			index := code[ip+1]
			ip += 2
			p.frame[index] = num(p.frame[index].num() + float64(amount))

		case compiler.IncrSpecial:
			amount := code[ip]
			index := int(code[ip+1])
			ip += 2
			v := p.getSpecial(index)
			err := p.setSpecial(index, num(v.num()+float64(amount)))
			if err != nil {
				return err
			}

		case compiler.IncrArrayGlobal:
			amount := code[ip]
			arrayIndex := code[ip+1]
			ip += 2
			array := p.arrays[arrayIndex]
			index := p.toString(p.pop())
			array[index] = num(array[index].num() + float64(amount))

		case compiler.IncrArrayLocal:
			amount := code[ip]
			arrayIndex := code[ip+1]
			ip += 2
			array := p.localArray(int(arrayIndex))
			index := p.toString(p.pop())
			array[index] = num(array[index].num() + float64(amount))

		case compiler.AugAssignField:
			operation := compiler.AugOp(code[ip])
			ip++
			right, indexVal := p.popTwo()
			index := int(indexVal.num())
			field := p.getField(index)
			v, err := p.augAssignOp(operation, field, right)
			if err != nil {
				return err
			}
			err = p.setField(index, p.toString(v))
			if err != nil {
				return err
			}

		case compiler.AugAssignGlobal:
			operation := compiler.AugOp(code[ip])
			index := code[ip+1]
			ip += 2
			v, err := p.augAssignOp(operation, p.globals[index], p.pop())
			if err != nil {
				return err
			}
			p.globals[index] = v

		case compiler.AugAssignLocal:
			operation := compiler.AugOp(code[ip])
			index := code[ip+1]
			ip += 2
			v, err := p.augAssignOp(operation, p.frame[index], p.pop())
			if err != nil {
				return err
			}
			p.frame[index] = v

		case compiler.AugAssignSpecial:
			operation := compiler.AugOp(code[ip])
			index := int(code[ip+1])
			ip += 2
			v, err := p.augAssignOp(operation, p.getSpecial(index), p.pop())
			if err != nil {
				return err
			}
			err = p.setSpecial(index, v)
			if err != nil {
				return err
			}

		case compiler.AugAssignArrayGlobal:
			operation := compiler.AugOp(code[ip])
			arrayIndex := code[ip+1]
			ip += 2
			array := p.arrays[arrayIndex]
			index := p.toString(p.pop())
			v, err := p.augAssignOp(operation, array[index], p.pop())
			if err != nil {
				return err
			}
			array[index] = v

		case compiler.AugAssignArrayLocal:
			operation := compiler.AugOp(code[ip])
			arrayIndex := code[ip+1]
			ip += 2
			array := p.localArray(int(arrayIndex))
			right, indexVal := p.popTwo()
			index := p.toString(indexVal)
			v, err := p.augAssignOp(operation, array[index], right)
			if err != nil {
				return err
			}
			array[index] = v

		case compiler.Regex:
			// Stand-alone /regex/ is equivalent to: $0 ~ /regex/
			index := code[ip]
			ip++
			re := p.regexes[index]
			p.push(boolean(re.MatchString(p.line)))

		case compiler.IndexMulti:
			numValues := int(code[ip])
			ip++
			values := p.popSlice(numValues)
			indices := make([]string, 0, 3) // up to 3-dimensional indices won't require heap allocation
			for _, v := range values {
				indices = append(indices, p.toString(v))
			}
			p.push(str(strings.Join(indices, p.subscriptSep)))

		case compiler.Add:
			l, r := p.peekPop()
			p.replaceTop(num(l.num() + r.num()))

		case compiler.Subtract:
			l, r := p.peekPop()
			p.replaceTop(num(l.num() - r.num()))

		case compiler.Multiply:
			l, r := p.peekPop()
			p.replaceTop(num(l.num() * r.num()))

		case compiler.Divide:
			l, r := p.peekPop()
			rf := r.num()
			if rf == 0.0 {
				return newError("division by zero")
			}
			p.replaceTop(num(l.num() / rf))

		case compiler.Power:
			l, r := p.peekPop()
			p.replaceTop(num(math.Pow(l.num(), r.num())))

		case compiler.Modulo:
			l, r := p.peekPop()
			rf := r.num()
			if rf == 0.0 {
				return newError("division by zero in mod")
			}
			p.replaceTop(num(math.Mod(l.num(), rf)))

		case compiler.Equals:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) == p.toString(r)))
			} else {
				p.replaceTop(boolean(ln == rn))
			}

		case compiler.NotEquals:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) != p.toString(r)))
			} else {
				p.replaceTop(boolean(ln != rn))
			}

		case compiler.Less:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) < p.toString(r)))
			} else {
				p.replaceTop(boolean(ln < rn))
			}

		case compiler.Greater:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) > p.toString(r)))
			} else {
				p.replaceTop(boolean(ln > rn))
			}

		case compiler.LessOrEqual:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) <= p.toString(r)))
			} else {
				p.replaceTop(boolean(ln <= rn))
			}

		case compiler.GreaterOrEqual:
			l, r := p.peekPop()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			if lIsStr || rIsStr {
				p.replaceTop(boolean(p.toString(l) >= p.toString(r)))
			} else {
				p.replaceTop(boolean(ln >= rn))
			}

		case compiler.Concat:
			l, r := p.peekPop()
			p.replaceTop(str(p.toString(l) + p.toString(r)))

		case compiler.ConcatMulti:
			numValues := int(code[ip])
			ip++
			values := p.popSlice(numValues)
			var sb strings.Builder

			for _, v := range values {
				sb.WriteString(p.toString(v))
			}
			p.push(str(sb.String()))

		case compiler.Match:
			l, r := p.peekPop()
			re, err := p.compileRegex(p.toString(r))
			if err != nil {
				return err
			}
			matched := re.MatchString(p.toString(l))
			p.replaceTop(boolean(matched))

		case compiler.NotMatch:
			l, r := p.peekPop()
			re, err := p.compileRegex(p.toString(r))
			if err != nil {
				return err
			}
			matched := re.MatchString(p.toString(l))
			p.replaceTop(boolean(!matched))

		case compiler.Not:
			p.replaceTop(boolean(!p.peekTop().boolean()))

		case compiler.UnaryMinus:
			p.replaceTop(num(-p.peekTop().num()))

		case compiler.UnaryPlus:
			p.replaceTop(num(p.peekTop().num()))

		case compiler.Boolean:
			p.replaceTop(boolean(p.peekTop().boolean()))

		case compiler.Jump:
			offset := code[ip]
			ip += 1 + int(offset)

		case compiler.JumpFalse:
			offset := code[ip]
			ip++
			v := p.pop()
			if !v.boolean() {
				ip += int(offset)
			}

		case compiler.JumpTrue:
			offset := code[ip]
			ip++
			v := p.pop()
			if v.boolean() {
				ip += int(offset)
			}

		case compiler.JumpEquals:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) == p.toString(r)
			} else {
				b = ln == rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.JumpNotEquals:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) != p.toString(r)
			} else {
				b = ln != rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.JumpLess:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) < p.toString(r)
			} else {
				b = ln < rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.JumpGreater:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) > p.toString(r)
			} else {
				b = ln > rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.JumpLessOrEqual:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) <= p.toString(r)
			} else {
				b = ln <= rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.JumpGreaterOrEqual:
			offset := code[ip]
			ip++
			l, r := p.popTwo()
			ln, lIsStr := l.isTrueStr()
			rn, rIsStr := r.isTrueStr()
			var b bool
			if lIsStr || rIsStr {
				b = p.toString(l) >= p.toString(r)
			} else {
				b = ln >= rn
			}
			if b {
				ip += int(offset)
			}

		case compiler.Next:
			return errNext

		case compiler.Nextfile:
			return errNextfile

		case compiler.Exit:
			// Return special errExit value "caught" by top-level executor
			return errExit

		case compiler.ExitStatus:
			p.exitStatus = int(p.pop().num())
			return errExit

		case compiler.ForIn:
			varScope := code[ip]
			varIndex := code[ip+1]
			arrayScope := code[ip+2]
			arrayIndex := code[ip+3]
			offset := code[ip+4]
			ip += 5
			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
			loopCode := code[ip : ip+int(offset)]
			for index := range array {
				switch resolver.Scope(varScope) {
				case resolver.Global:
					p.globals[varIndex] = str(index)
				case resolver.Local:
					p.frame[varIndex] = str(index)
				default: // resolver.Special
					err := p.setSpecial(int(varIndex), str(index))
					if err != nil {
						return err
					}
				}
				err := p.execute(loopCode)
				if err == errBreak {
					break
				}
				if err != nil {
					return err
				}
			}
			ip += int(offset)

		case compiler.BreakForIn:
			return errBreak

		case compiler.CallBuiltin:
			builtinOp := compiler.BuiltinOp(code[ip])
			ip++
			err := p.callBuiltin(builtinOp)
			if err != nil {
				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]
			ip += 2
			s := p.toString(p.peekTop())
			n, err := p.split(s, resolver.Scope(arrayScope), int(arrayIndex), p.fieldSep, p.inputMode)
			if err != nil {
				return err
			}
			p.replaceTop(num(float64(n)))

		case compiler.CallSplitSep:
			arrayScope := code[ip]
			arrayIndex := code[ip+1]
			ip += 2
			s, fieldSep := p.peekPop()
			// 3-argument form of split() ignores input mode
			n, err := p.split(p.toString(s), resolver.Scope(arrayScope), int(arrayIndex), p.toString(fieldSep), DefaultMode)
			if err != nil {
				return err
			}
			p.replaceTop(num(float64(n)))

		case compiler.CallSprintf:
			numArgs := code[ip]
			ip++
			args := p.popSlice(int(numArgs))
			s, err := p.sprintf(p.toString(args[0]), args[1:])
			if err != nil {
				return err
			}
			p.push(str(s))

		case compiler.CallUser:
			funcIndex := code[ip]
			numArrayArgs := int(code[ip+1])
			ip += 2

			f := p.program.Compiled.Functions[funcIndex]
			if p.callDepth >= maxCallDepth {
				return newError("calling %q exceeded maximum call depth of %d", f.Name, maxCallDepth)
			}

			// Set up frame for scalar arguments
			oldFrame := p.frame
			p.frame = p.peekSlice(f.NumScalars)

			// Handle array arguments
			var arrays []int
			for j := 0; j < numArrayArgs; j++ {
				arrayScope := resolver.Scope(code[ip])
				arrayIndex := int(code[ip+1])
				ip += 2
				arrays = append(arrays, p.arrayIndex(arrayScope, arrayIndex))
			}
			oldArraysLen := len(p.arrays)
			for j := numArrayArgs; j < f.NumArrays; j++ {
				arrays = append(arrays, len(p.arrays))
				p.arrays = append(p.arrays, make(map[string]value))
			}
			p.localArrays = append(p.localArrays, arrays)

			// Execute the function!
			p.callDepth++
			err := p.execute(f.Body)
			p.callDepth--

			// Pop the locals off the stack
			p.popSlice(f.NumScalars)
			p.frame = oldFrame
			p.localArrays = p.localArrays[:len(p.localArrays)-1]
			p.arrays = p.arrays[:oldArraysLen]

			if r, ok := err.(returnValue); ok {
				p.push(r.Value)
			} else if err != nil {
				return err
			} else {
				p.push(null())
			}

		case compiler.CallNative:
			funcIndex := int(code[ip])
			numArgs := int(code[ip+1])
			ip += 2

			args := p.popSlice(numArgs)
			r, err := p.callNative(funcIndex, args)
			if err != nil {
				return err
			}
			p.push(r)

		case compiler.Return:
			v := p.pop()
			return returnValue{v}

		case compiler.ReturnNull:
			return returnValue{null()}

		case compiler.Nulls:
			numNulls := int(code[ip])
			ip++
			p.pushNulls(numNulls)

		case compiler.Print:
			numArgs := code[ip]
			redirect := lexer.Token(code[ip+1])
			ip += 2

			args := p.popSlice(int(numArgs))

			// Determine what output stream to write to.
			output := p.output
			if redirect != lexer.ILLEGAL {
				var err error
				dest := p.pop()
				output, err = p.getOutputStream(redirect, dest)
				if err != nil {
					return err
				}
			}

			if numArgs > 0 {
				err := p.printArgs(output, args)
				if err != nil {
					return err
				}
			} else {
				// "print" with no arguments prints the raw value of $0,
				// regardless of output mode.
				err := p.printLine(output, p.line)
				if err != nil {
					return err
				}
			}

		case compiler.Printf:
			numArgs := code[ip]
			redirect := lexer.Token(code[ip+1])
			ip += 2

			args := p.popSlice(int(numArgs))
			s, err := p.sprintf(p.toString(args[0]), args[1:])
			if err != nil {
				return err
			}

			output := p.output
			if redirect != lexer.ILLEGAL {
				dest := p.pop()
				output, err = p.getOutputStream(redirect, dest)
				if err != nil {
					return err
				}
			}
			err = writeOutput(output, s)
			if err != nil {
				return err
			}

		case compiler.Getline:
			redirect := lexer.Token(code[ip])
			ip++

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			if ret == 1 {
				p.setLine(line, false)
			}
			p.push(num(ret))

		case compiler.GetlineField:
			redirect := lexer.Token(code[ip])
			ip++

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			if ret == 1 {
				err := p.setField(0, line)
				if err != nil {
					return err
				}
			}
			p.push(num(ret))

		case compiler.GetlineGlobal:
			redirect := lexer.Token(code[ip])
			index := code[ip+1]
			ip += 2

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			if ret == 1 {
				p.globals[index] = numStr(line)
			}
			p.push(num(ret))

		case compiler.GetlineLocal:
			redirect := lexer.Token(code[ip])
			index := code[ip+1]
			ip += 2

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			if ret == 1 {
				p.frame[index] = numStr(line)
			}
			p.push(num(ret))

		case compiler.GetlineSpecial:
			redirect := lexer.Token(code[ip])
			index := code[ip+1]
			ip += 2

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			if ret == 1 {
				err := p.setSpecial(int(index), numStr(line))
				if err != nil {
					return err
				}
			}
			p.push(num(ret))

		case compiler.GetlineArray:
			redirect := lexer.Token(code[ip])
			arrayScope := code[ip+1]
			arrayIndex := code[ip+2]
			ip += 3

			ret, line, err := p.getline(redirect)
			if err != nil {
				return err
			}
			index := p.toString(p.peekTop())
			if ret == 1 {
				array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
				array[index] = numStr(line)
			}
			p.replaceTop(num(ret))
		}
	}

	return nil
}

func (p *interp) callBuiltin(builtinOp compiler.BuiltinOp) error {
	switch builtinOp {
	case compiler.BuiltinAtan2:
		y, x := p.peekPop()
		p.replaceTop(num(math.Atan2(y.num(), x.num())))

	case compiler.BuiltinClose:
		var err error
		code := -1
		name := p.toString(p.peekTop())
		if stream := p.inputStreams[name]; stream != nil {
			// Close input stream
			delete(p.inputStreams, name)
			delete(p.scanners, name)
			err = stream.Close()
			code = stream.ExitCode()
		} else if stream := p.outputStreams[name]; stream != nil {
			// Close output stream
			delete(p.outputStreams, name)
			err = stream.Close()
			code = stream.ExitCode()
		}
		if err != nil {
			p.printErrorf("error closing %q: %v\n", name, err)
		}
		p.replaceTop(num(float64(code)))

	case compiler.BuiltinCos:
		p.replaceTop(num(math.Cos(p.peekTop().num())))

	case compiler.BuiltinExp:
		p.replaceTop(num(math.Exp(p.peekTop().num())))

	case compiler.BuiltinFflush:
		name := p.toString(p.peekTop())
		var ok bool
		if name != "" {
			// Flush a single, named output stream
			ok = p.flushStream(name)
		} else {
			// fflush() or fflush("") flushes all output streams
			ok = p.flushAll()
		}
		if !ok {
			p.replaceTop(num(-1))
		} else {
			p.replaceTop(num(0))
		}

	case compiler.BuiltinFflushAll:
		ok := p.flushAll()
		if !ok {
			p.push(num(-1))
		} else {
			p.push(num(0))
		}

	case compiler.BuiltinGsub:
		regex, repl, in := p.peekPeekPop()
		out, n, err := p.sub(p.toString(regex), p.toString(repl), p.toString(in), true)
		if err != nil {
			return err
		}
		p.replaceTwo(num(float64(n)), str(out))

	case compiler.BuiltinIndex:
		sValue, substr := p.peekPop()
		s := p.toString(sValue)
		index := strings.Index(s, p.toString(substr))
		var awkIndex int
		if index < 0 {
			awkIndex = 0
		} else if p.chars {
			awkIndex = utf8.RuneCountInString(s[:index]) + 1
		} else {
			awkIndex = index + 1
		}
		p.replaceTop(num(float64(awkIndex)))

	case compiler.BuiltinInt:
		p.replaceTop(num(float64(int64(p.peekTop().num()))))

	case compiler.BuiltinLength:
		var length int
		if p.chars {
			length = utf8.RuneCountInString(p.line)
		} else {
			length = len(p.line)
		}
		p.push(num(float64(length)))

	case compiler.BuiltinLengthArg:
		s := p.toString(p.peekTop())
		var length int
		if p.chars {
			length = utf8.RuneCountInString(s)
		} else {
			length = len(s)
		}
		p.replaceTop(num(float64(length)))

	case compiler.BuiltinLog:
		p.replaceTop(num(math.Log(p.peekTop().num())))

	case compiler.BuiltinMatch:
		sValue, regex := p.peekPop()
		s := p.toString(sValue)
		re, err := p.compileRegex(p.toString(regex))
		if err != nil {
			return err
		}
		loc := re.FindStringIndex(s)
		if loc == nil {
			p.matchStart = 0
			p.matchLength = -1
		} else if p.chars {
			p.matchStart = utf8.RuneCountInString(s[:loc[0]]) + 1
			p.matchLength = utf8.RuneCountInString(s[loc[0]:loc[1]])
		} else {
			p.matchStart = loc[0] + 1
			p.matchLength = loc[1] - loc[0]
		}
		p.replaceTop(num(float64(p.matchStart)))

	case compiler.BuiltinRand:
		p.push(num(p.random.Float64()))

	case compiler.BuiltinSin:
		p.replaceTop(num(math.Sin(p.peekTop().num())))

	case compiler.BuiltinSqrt:
		p.replaceTop(num(math.Sqrt(p.peekTop().num())))

	case compiler.BuiltinSrand:
		prevSeed := p.randSeed
		p.randSeed = float64(time.Now().Unix())
		p.random.Seed(int64(math.Float64bits(p.randSeed)))
		p.push(num(prevSeed))

	case compiler.BuiltinSrandSeed:
		prevSeed := p.randSeed
		p.randSeed = p.peekTop().num()
		p.random.Seed(int64(math.Float64bits(p.randSeed)))
		p.replaceTop(num(prevSeed))

	case compiler.BuiltinSub:
		regex, repl, in := p.peekPeekPop()
		out, n, err := p.sub(p.toString(regex), p.toString(repl), p.toString(in), false)
		if err != nil {
			return err
		}
		p.replaceTwo(num(float64(n)), str(out))

	case compiler.BuiltinSubstr:
		sValue, posValue := p.peekPop()
		pos := int(posValue.num())
		s := p.toString(sValue)
		var substr string
		if p.chars {
			substr = substrChars(s, pos)
		} else {
			if pos > len(s) {
				pos = len(s) + 1
			}
			if pos < 1 {
				pos = 1
			}
			length := len(s) - pos + 1
			substr = s[pos-1 : pos-1+length]
		}
		p.replaceTop(str(substr))

	case compiler.BuiltinSubstrLength:
		posValue, lengthValue := p.popTwo()
		length := int(lengthValue.num())
		pos := int(posValue.num())
		s := p.toString(p.peekTop())
		var substr string
		if p.chars {
			substr = substrLengthChars(s, pos, length)
		} else {
			if pos > len(s) {
				pos = len(s) + 1
			}
			if pos < 1 {
				pos = 1
			}
			maxLength := len(s) - pos + 1
			if length < 0 {
				length = 0
			}
			if length > maxLength {
				length = maxLength
			}
			substr = s[pos-1 : pos-1+length]
		}
		p.replaceTop(str(substr))

	case compiler.BuiltinSystem:
		if p.noExec {
			return newError("can't call system() due to NoExec")
		}
		cmdline := p.toString(p.peekTop())
		cmd := p.execShell(cmdline)
		cmd.Stdin = p.stdin
		cmd.Stdout = p.output
		cmd.Stderr = p.errorOutput
		_ = p.flushAll() // ensure synchronization
		err := cmd.Start()
		if err != nil {
			// Could not start the shell so skip waiting on it.
			p.printErrorf("%v\n", err)
			p.replaceTop(num(-1.0))
			return nil
		}
		exitCode, err := waitExitCode(cmd)
		if err != nil {
			if p.checkCtx && p.ctx.Err() != nil {
				return p.ctx.Err()
			}
			p.printErrorf("%v\n", err)
		}
		p.replaceTop(num(float64(exitCode)))

	case compiler.BuiltinTolower:
		p.replaceTop(str(strings.ToLower(p.toString(p.peekTop()))))

	case compiler.BuiltinToupper:
		p.replaceTop(str(strings.ToUpper(p.toString(p.peekTop()))))
	}

	return nil
}

// Fetch the value at the given index from array. This handles the strange
// POSIX behavior of creating a null entry for non-existent array elements.
// Per the POSIX spec, "Any other reference to a nonexistent array element
// [apart from "in" expressions] shall automatically create it."
func arrayGet(array map[string]value, index string) value {
	v, ok := array[index]
	if !ok {
		array[index] = v
	}
	return v
}

// Stack operations follow. These should be inlined. Instead of just push and
// pop, for efficiency we have custom operations for when we're replacing the
// top of stack without changing the stack pointer. Primarily this avoids the
// check for append in push.
func (p *interp) push(v value) {
	sp := p.sp
	if sp >= len(p.stack) {
		p.stack = append(p.stack, null())
	}
	p.stack[sp] = v
	sp++
	p.sp = sp
}

func (p *interp) pushNulls(num int) {
	sp := p.sp
	for p.sp+num-1 >= len(p.stack) {
		p.stack = append(p.stack, null())
	}
	for i := 0; i < num; i++ {
		p.stack[sp] = null()
		sp++
	}
	p.sp = sp
}

func (p *interp) pop() value {
	p.sp--
	return p.stack[p.sp]
}

func (p *interp) popTwo() (value, value) {
	p.sp -= 2
	return p.stack[p.sp], p.stack[p.sp+1]
}

func (p *interp) peekTop() value {
	return p.stack[p.sp-1]
}

func (p *interp) peekTwo() (value, value) {
	return p.stack[p.sp-2], p.stack[p.sp-1]
}

func (p *interp) peekPop() (value, value) {
	p.sp--
	return p.stack[p.sp-1], p.stack[p.sp]
}

func (p *interp) peekPeekPop() (value, value, value) {
	p.sp--
	return p.stack[p.sp-2], p.stack[p.sp-1], p.stack[p.sp]
}

func (p *interp) replaceTop(v value) {
	p.stack[p.sp-1] = v
}

func (p *interp) replaceTwo(l, r value) {
	p.stack[p.sp-2] = l
	p.stack[p.sp-1] = r
}

func (p *interp) popSlice(n int) []value {
	p.sp -= n
	return p.stack[p.sp : p.sp+n]
}

func (p *interp) peekSlice(n int) []value {
	return p.stack[p.sp-n:]
}

// Helper for getline operations. This performs the (possibly redirected) read
// of a line, and returns the result. If the result is 1 (success in AWK), the
// caller will set the target to the returned string.
func (p *interp) getline(redirect lexer.Token) (float64, string, error) {
	switch redirect {
	case lexer.PIPE: // redirect from command
		name := p.toString(p.pop())
		scanner, err := p.getInputScannerPipe(name)
		if err != nil {
			return 0, "", err
		}
		if !scanner.Scan() {
			if err := scanner.Err(); err != nil {
				return -1, "", nil
			}
			return 0, "", nil
		}
		return 1, scanner.Text(), nil

	case lexer.LESS: // redirect from file
		name := p.toString(p.pop())
		scanner, err := p.getInputScannerFile(name)
		if err != nil {
			if _, ok := err.(*os.PathError); ok {
				// File not found is not a hard error, getline just returns -1.
				// See: https://github.com/benhoyt/goawk/issues/41
				return -1, "", nil
			}
			return 0, "", err
		}
		if !scanner.Scan() {
			if err := scanner.Err(); err != nil {
				return -1, "", nil
			}
			return 0, "", nil
		}
		return 1, scanner.Text(), nil

	default: // no redirect
		p.flushOutputAndError() // Flush output in case they've written a prompt
		var err error
		line, err := p.nextLine()
		if err == io.EOF {
			return 0, "", nil
		}
		if err != nil {
			return -1, "", nil
		}
		return 1, line, nil
	}
}

// Perform augmented assignment operation.
func (p *interp) augAssignOp(op compiler.AugOp, l, r value) (value, error) {
	switch op {
	case compiler.AugOpAdd:
		return num(l.num() + r.num()), nil
	case compiler.AugOpSub:
		return num(l.num() - r.num()), nil
	case compiler.AugOpMul:
		return num(l.num() * r.num()), nil
	case compiler.AugOpDiv:
		rf := r.num()
		if rf == 0.0 {
			return null(), newError("division by zero")
		}
		return num(l.num() / rf), nil
	case compiler.AugOpPow:
		return num(math.Pow(l.num(), r.num())), nil
	default: // AugOpMod
		rf := r.num()
		if rf == 0.0 {
			return null(), newError("division by zero in mod")
		}
		return num(math.Mod(l.num(), rf)), nil
	}
}
