// The New...Execute API (allows you to efficiently execute the same program repeatedly). package interp import ( "context" "math" "github.com/benhoyt/goawk/internal/resolver" "github.com/benhoyt/goawk/parser" ) const checkContextOps = 1000 // for efficiency, only check context every N instructions // Interpreter is an interpreter for a specific program, allowing you to // efficiently execute the same program over and over with different inputs. // Use New to create an Interpreter. // // Most programs won't need reusable execution, and should use the simpler // Exec or ExecProgram functions instead. type Interpreter struct { interp *interp } // New creates a reusable interpreter for the given program. // // Most programs won't need reusable execution, and should use the simpler // Exec or ExecProgram functions instead. func New(program *parser.Program) (*Interpreter, error) { p := newInterp(program) return &Interpreter{interp: p}, nil } // Execute runs this program with the given execution configuration (input, // output, and variables) and returns the exit status code of the program. A // nil config is valid and will use the defaults (zero values). // // Internal memory allocations are reused, so calling Execute on the same // Interpreter instance is significantly more efficient than calling // ExecProgram multiple times. // // I/O state is reset between each run, but variables and the random number // generator seed are not; use ResetVars and ResetRand to reset those. // // It's best to set config.Environ to a non-nil slice, otherwise Execute will // call the relatively inefficient os.Environ each time. Set config.Environ to // []string{} if the script doesn't need environment variables, or call // os.Environ once and set config.Environ to that value each execution. // // Note that config.Funcs must be the same value provided to // parser.ParseProgram, and must not change between calls to Execute. func (p *Interpreter) Execute(config *Config) (int, error) { p.interp.resetCore() p.interp.checkCtx = false err := p.interp.setExecuteConfig(config) if err != nil { return 0, err } return p.interp.executeAll() } // Array returns a map representing the items in the named AWK array. AWK // numbers are included as type float64, strings (including "numeric strings") // are included as type string. If the named array does not exist, return nil. func (p *Interpreter) Array(name string) map[string]any { index, exists := p.interp.arrayIndexes[name] if !exists { return nil } array := p.interp.array(resolver.Global, index) result := make(map[string]any, len(array)) for k, v := range array { switch v.typ { case typeNum: result[k] = v.n case typeStr, typeNumStr: result[k] = v.s default: result[k] = "" } } return result } func (p *interp) resetCore() { p.scanner = nil for k := range p.scanners { delete(p.scanners, k) } p.input = nil for k := range p.inputStreams { delete(p.inputStreams, k) } for k := range p.outputStreams { delete(p.outputStreams, k) } p.sp = 0 p.localArrays = p.localArrays[:0] p.callDepth = 0 p.filename = null() p.line = "" p.lineIsTrueStr = false p.lineNum = 0 p.fileLineNum = 0 p.fields = nil p.fieldsIsTrueStr = nil p.numFields = 0 p.haveFields = false p.exitStatus = 0 } func (p *interp) resetVars() { // Reset global scalars for i := range p.globals { p.globals[i] = null() } // Reset global arrays for _, array := range p.arrays { for k := range array { delete(array, k) } } // Reset special variables p.convertFormat = "%.6g" p.outputFormat = "%.6g" p.fieldSep = " " p.fieldSepRegex = nil p.savedFieldSep = " " p.savedFieldSepRegex = nil p.recordSep = "\n" p.recordSepRegex = nil p.recordTerminator = "" p.outputFieldSep = " " p.outputRecordSep = "\n" p.subscriptSep = "\x1c" p.matchLength = 0 p.matchStart = 0 } // ResetVars resets this interpreter's variables, setting scalar variables to // null, clearing arrays, and resetting special variables such as FS and RS to // their defaults. func (p *Interpreter) ResetVars() { p.interp.resetVars() } // ResetRand resets this interpreter's random number generator seed, so that // rand() produces the same sequence it would have after calling New. This is // a relatively CPU-intensive operation. func (p *Interpreter) ResetRand() { p.interp.randSeed = 1.0 p.interp.random.Seed(int64(math.Float64bits(p.interp.randSeed))) } // ExecuteContext is like Execute, but takes a context to allow the caller to // set an execution timeout or cancel the execution. For efficiency, the // context is only tested every 1000 virtual machine instructions. // // Context handling is not preemptive: some long-running operations may not // be interrupted when the context is cancelled. func (p *Interpreter) ExecuteContext(ctx context.Context, config *Config) (int, error) { p.interp.resetCore() p.interp.checkCtx = ctx != context.Background() && ctx != context.TODO() p.interp.ctx = ctx p.interp.ctxDone = ctx.Done() p.interp.ctxOps = 0 err := p.interp.setExecuteConfig(config) if err != nil { return 0, err } return p.interp.executeAll() } func (p *interp) checkContext() error { p.ctxOps++ if p.ctxOps < checkContextOps { return nil } p.ctxOps = 0 return p.checkContextNow() } func (p *interp) checkContextNow() error { select { case <-p.ctxDone: return p.ctx.Err() default: return nil } }