go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

runner.go (13148B)


      1 package vm
      2 
      3 import (
      4 	"context"
      5 	"errors"
      6 	"fmt"
      7 
      8 	"git.defalsify.org/vise.git/cache"
      9 	"git.defalsify.org/vise.git/render"
     10 	"git.defalsify.org/vise.git/resource"
     11 	"git.defalsify.org/vise.git/state"
     12 )
     13 
     14 // ExternalCodeError indicates an error that occurred when resolving an external code symbol (LOAD, RELOAD).
     15 type ExternalCodeError struct {
     16 	sym  string
     17 	code int
     18 	err  error
     19 }
     20 
     21 // NewExternalCodeError creates a new ExternalCodeError.
     22 func NewExternalCodeError(sym string, err error) *ExternalCodeError {
     23 	return &ExternalCodeError{
     24 		sym: sym,
     25 		err: err,
     26 	}
     27 }
     28 
     29 func (e *ExternalCodeError) WithCode(code int) *ExternalCodeError {
     30 	e.code = code
     31 	return e
     32 }
     33 
     34 // Error implements the Error interface.
     35 func (e ExternalCodeError) Error() string {
     36 	logg.Errorf("external code error: %v", e.err)
     37 	return fmt.Sprintf("error %v:%v", e.sym, e.code)
     38 }
     39 
     40 // Vm holds sub-components mutated by the vm execution.
     41 // TODO: Renderer should be passed to avoid proxy methods not strictly related to vm operation
     42 type Vm struct {
     43 	st            *state.State      // Navigation and error states.
     44 	rs            resource.Resource // Retrieves content, code, and templates for symbols.
     45 	ca            cache.Memory      // Loaded content.
     46 	mn            *render.Menu      // Menu component of page.
     47 	sizer         *render.Sizer     // Apply size constraints to output.
     48 	pg            *render.Page      // Render outputs with menues to size constraints
     49 	menuSeparator string            // Passed to Menu.WithSeparator if not empty
     50 }
     51 
     52 // NewVm creates a new Vm.
     53 func NewVm(st *state.State, rs resource.Resource, ca cache.Memory, sizer *render.Sizer) *Vm {
     54 	vmi := &Vm{
     55 		st:    st,
     56 		rs:    rs,
     57 		ca:    ca,
     58 		pg:    render.NewPage(ca, rs),
     59 		sizer: sizer,
     60 	}
     61 	vmi.Reset()
     62 	logg.Infof("vm created with state", "state", st, "renderer", vmi.pg)
     63 	return vmi
     64 }
     65 
     66 // WithMenuSeparator is a chainable function that sets the separator string to use
     67 // in the menu renderer.
     68 func (vmi *Vm) WithMenuSeparator(sep string) *Vm {
     69 	vmi.menuSeparator = sep
     70 	return vmi
     71 }
     72 
     73 // Reset re-initializes sub-components for output rendering.
     74 func (vmi *Vm) Reset() {
     75 	vmi.mn = render.NewMenu()
     76 	if vmi.menuSeparator != "" {
     77 		vmi.mn = vmi.mn.WithSeparator(vmi.menuSeparator)
     78 	}
     79 	vmi.pg.Reset()
     80 	vmi.pg = vmi.pg.WithMenu(vmi.mn)
     81 	if vmi.sizer != nil {
     82 		vmi.pg = vmi.pg.WithSizer(vmi.sizer)
     83 	}
     84 }
     85 
     86 func Rewind(sym string, st *state.State, ca cache.Memory) (string, error) {
     87 	var err error
     88 	for true {
     89 		notTop, err := st.Top()
     90 		if notTop {
     91 			break
     92 		}
     93 		sym, err = st.Up()
     94 		if err != nil {
     95 			break
     96 		}
     97 		err = ca.Pop()
     98 		if err != nil {
     99 			break
    100 		}
    101 	}
    102 	return sym, err
    103 }
    104 
    105 // Run extracts individual op codes and arguments and executes them.
    106 //
    107 // Each step may update the state.
    108 //
    109 // On error, the remaining instructions will be returned. State will not be rolled back.
    110 func (vm *Vm) Run(ctx context.Context, b []byte) ([]byte, error) {
    111 	logg.Tracef("new vm run")
    112 	running := true
    113 	for running {
    114 		r := vm.st.MatchFlag(state.FLAG_TERMINATE, true)
    115 		if r {
    116 			logg.InfoCtxf(ctx, "terminate set! bailing")
    117 			return []byte{}, nil
    118 		}
    119 
    120 		_ = vm.st.ResetFlag(state.FLAG_TERMINATE)
    121 
    122 		change := vm.st.ResetFlag(state.FLAG_LANG)
    123 		if change {
    124 			if vm.st.Language != nil {
    125 				ctx = context.WithValue(ctx, "Language", *vm.st.Language)
    126 			}
    127 		}
    128 
    129 		waitChange := vm.st.ResetFlag(state.FLAG_WAIT)
    130 		if waitChange {
    131 			vm.st.ResetFlag(state.FLAG_INMATCH)
    132 			vm.pg.Reset()
    133 			vm.mn.Reset()
    134 		}
    135 
    136 		_ = vm.st.SetFlag(state.FLAG_DIRTY)
    137 		op, bb, err := opSplit(b)
    138 		if err != nil {
    139 			return b, err
    140 		}
    141 		b = bb
    142 		logg.DebugCtxf(ctx, "execute code", "opcode", op, "op", OpcodeString[op], "code", b)
    143 		logg.DebugCtxf(ctx, "", "state", vm.st)
    144 		switch op {
    145 		case CATCH:
    146 			b, err = vm.runCatch(ctx, b)
    147 		case CROAK:
    148 			b, err = vm.runCroak(ctx, b)
    149 		case LOAD:
    150 			b, err = vm.runLoad(ctx, b)
    151 		case RELOAD:
    152 			b, err = vm.runReload(ctx, b)
    153 		case MAP:
    154 			b, err = vm.runMap(ctx, b)
    155 		case MOVE:
    156 			b, err = vm.runMove(ctx, b)
    157 		case INCMP:
    158 			b, err = vm.runInCmp(ctx, b)
    159 		case MSINK:
    160 			b, err = vm.runMSink(ctx, b)
    161 		case MOUT:
    162 			b, err = vm.runMOut(ctx, b)
    163 		case MNEXT:
    164 			b, err = vm.runMNext(ctx, b)
    165 		case MPREV:
    166 			b, err = vm.runMPrev(ctx, b)
    167 		case HALT:
    168 			b, err = vm.runHalt(ctx, b)
    169 			return b, err
    170 		default:
    171 			err = fmt.Errorf("Unhandled state: %v", op)
    172 		}
    173 		b, err = vm.runErrCheck(ctx, b, err)
    174 		if err != nil {
    175 			return b, err
    176 		}
    177 		if len(b) == 0 {
    178 			b, err = vm.runDeadCheck(ctx, b)
    179 			if err != nil {
    180 				return b, err
    181 			}
    182 		}
    183 		if len(b) == 0 {
    184 			return []byte{}, nil
    185 		}
    186 	}
    187 	return b, nil
    188 }
    189 
    190 // handles errors that should not be deferred to the client.
    191 func (vm *Vm) runErrCheck(ctx context.Context, b []byte, err error) ([]byte, error) {
    192 	if err == nil {
    193 		return b, err
    194 	}
    195 	vm.pg = vm.pg.WithError(err)
    196 
    197 	v := vm.st.MatchFlag(state.FLAG_LOADFAIL, true)
    198 	if !v {
    199 		return b, err
    200 	}
    201 
    202 	b = NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
    203 	return b, nil
    204 }
    205 
    206 // determines whether a state of empty bytecode should result in termination.
    207 //
    208 // If there is remaining bytecode, this method is a noop.
    209 //
    210 // If input has not been matched, a default invalid input page should be generated aswell as a possiblity of return to last screen (or exit).
    211 //
    212 // If the termination flag has been set but not yet handled, execution is allowed to terminate.
    213 func (vm *Vm) runDeadCheck(ctx context.Context, b []byte) ([]byte, error) {
    214 	if len(b) > 0 {
    215 		return b, nil
    216 	}
    217 	r := vm.st.MatchFlag(state.FLAG_READIN, false)
    218 	if r {
    219 		logg.DebugCtxf(ctx, "Not processing input. Setting terminate")
    220 		vm.st.SetFlag(state.FLAG_TERMINATE)
    221 		return b, nil
    222 	}
    223 	r = vm.st.MatchFlag(state.FLAG_TERMINATE, true)
    224 	if r {
    225 		logg.TraceCtxf(ctx, "Terminate found!!")
    226 		return b, nil
    227 	}
    228 
    229 	logg.TraceCtxf(ctx, "no code remaining but not terminating")
    230 	location, _ := vm.st.Where()
    231 	if location == "" {
    232 		return b, fmt.Errorf("dead runner with no current location")
    233 	} else if location == "_catch" {
    234 		return b, fmt.Errorf("unexpected catch endless loop detected for state: %s", vm.st)
    235 	}
    236 
    237 	input, err := vm.st.GetInput()
    238 	if err != nil {
    239 		input = []byte("(no input)")
    240 	}
    241 	cerr := NewInvalidInputError(string(input))
    242 	vm.pg.WithError(cerr)
    243 	b = NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
    244 	return b, nil
    245 }
    246 
    247 // executes the MAP opcode
    248 func (vm *Vm) runMap(ctx context.Context, b []byte) ([]byte, error) {
    249 	sym, b, err := ParseMap(b)
    250 	err = vm.pg.Map(sym)
    251 	return b, err
    252 }
    253 
    254 // executes the CATCH opcode
    255 func (vm *Vm) runCatch(ctx context.Context, b []byte) ([]byte, error) {
    256 	sym, sig, mode, b, err := ParseCatch(b)
    257 	if err != nil {
    258 		return b, err
    259 	}
    260 	r := vm.st.MatchFlag(sig, mode)
    261 	if r {
    262 		actualSym, _, err := applyTarget([]byte(sym), vm.st, vm.ca, ctx)
    263 		if err != nil {
    264 			return b, err
    265 		}
    266 		logg.InfoCtxf(ctx, "catch!", "flag", sig, "sym", sym, "target", actualSym, "mode", mode)
    267 		sym = actualSym
    268 		bh, err := vm.rs.GetCode(ctx, sym)
    269 		if err != nil {
    270 			return b, err
    271 		}
    272 		b = bh
    273 	}
    274 	return b, nil
    275 }
    276 
    277 // executes the CROAK opcode
    278 func (vm *Vm) runCroak(ctx context.Context, b []byte) ([]byte, error) {
    279 	sig, mode, b, err := ParseCroak(b)
    280 	if err != nil {
    281 		return b, err
    282 	}
    283 	r := vm.st.MatchFlag(sig, mode)
    284 	if r {
    285 		logg.InfoCtxf(ctx, "croak! purging and moving to top", "signal", sig)
    286 		vm.Reset()
    287 		vm.ca.Reset()
    288 		b = []byte{}
    289 	}
    290 	return b, nil
    291 }
    292 
    293 // executes the LOAD opcode
    294 func (vm *Vm) runLoad(ctx context.Context, b []byte) ([]byte, error) {
    295 	sym, sz, b, err := ParseLoad(b)
    296 	if err != nil {
    297 		return b, err
    298 	}
    299 	_, err = vm.ca.Get(sym)
    300 	if err == nil {
    301 		logg.DebugCtxf(ctx, "skip already loaded symbol", "symbol", sym)
    302 		return b, nil
    303 	}
    304 	r, err := vm.refresh(sym, vm.rs, ctx)
    305 	if err != nil {
    306 		return b, err
    307 	}
    308 	err = vm.ca.Add(sym, r, uint16(sz))
    309 	if err != nil {
    310 		if err == cache.ErrDup {
    311 			logg.DebugCtxf(ctx, "Ignoring load request on frame that has symbol already loaded", "sym", sym)
    312 			err = nil
    313 		}
    314 	}
    315 	return b, err
    316 }
    317 
    318 // executes the RELOAD opcode
    319 func (vm *Vm) runReload(ctx context.Context, b []byte) ([]byte, error) {
    320 	sym, b, err := ParseReload(b)
    321 	if err != nil {
    322 		return b, err
    323 	}
    324 
    325 	r, err := vm.refresh(sym, vm.rs, ctx)
    326 	if err != nil {
    327 		return b, err
    328 	}
    329 	vm.ca.Update(sym, r)
    330 	if vm.pg != nil {
    331 		err := vm.pg.Map(sym)
    332 		if err != nil {
    333 			return b, err
    334 		}
    335 	}
    336 	return b, nil
    337 }
    338 
    339 // executes the MOVE opcode
    340 func (vm *Vm) runMove(ctx context.Context, b []byte) ([]byte, error) {
    341 	sym, b, err := ParseMove(b)
    342 	if err != nil {
    343 		return b, err
    344 	}
    345 	sym, _, err = applyTarget([]byte(sym), vm.st, vm.ca, ctx)
    346 	if err != nil {
    347 		return b, err
    348 	}
    349 	code, err := vm.rs.GetCode(ctx, sym)
    350 	if err != nil {
    351 		return b, err
    352 	}
    353 	logg.DebugCtxf(ctx, "loaded code", "sym", sym, "code", code)
    354 	b = append(b, code...)
    355 	vm.Reset()
    356 	return b, nil
    357 }
    358 
    359 // executes the INCMP opcode
    360 // TODO: document state transition table and simplify flow
    361 func (vm *Vm) runInCmp(ctx context.Context, b []byte) ([]byte, error) {
    362 	sym, target, b, err := ParseInCmp(b)
    363 	if err != nil {
    364 		return b, err
    365 	}
    366 
    367 	reading := vm.st.GetFlag(state.FLAG_READIN)
    368 	have := vm.st.GetFlag(state.FLAG_INMATCH)
    369 	if err != nil {
    370 		panic(err)
    371 	}
    372 	if have {
    373 		if reading {
    374 			logg.DebugCtxf(ctx, "ignoring input - already have match", "input", sym)
    375 			return b, nil
    376 		}
    377 	} else {
    378 		vm.st.SetFlag(state.FLAG_READIN)
    379 	}
    380 	input, err := vm.st.GetInput()
    381 	if err != nil {
    382 		return b, err
    383 	}
    384 	logg.TraceCtxf(ctx, "testing sym", "sym", sym, "input", input)
    385 
    386 	if !have && target == "*" {
    387 		logg.DebugCtxf(ctx, "input wildcard match", "input", input, "next", sym)
    388 	} else {
    389 		if target != string(input) {
    390 			return b, nil
    391 		}
    392 		logg.InfoCtxf(ctx, "input match", "input", input, "next", sym)
    393 	}
    394 	vm.st.SetFlag(state.FLAG_INMATCH)
    395 	vm.st.ResetFlag(state.FLAG_READIN)
    396 
    397 	newSym, _, err := applyTarget([]byte(sym), vm.st, vm.ca, ctx)
    398 
    399 	//_, ok := err.(*state.IndexError)
    400 	//if ok {
    401 	if errors.Is(err, state.IndexError) {
    402 		vm.st.SetFlag(state.FLAG_READIN)
    403 		return b, nil
    404 	} else if err != nil {
    405 		return b, err
    406 	}
    407 
    408 	sym = newSym
    409 
    410 	vm.Reset()
    411 
    412 	code, err := vm.rs.GetCode(ctx, sym)
    413 	if err != nil {
    414 		return b, err
    415 	}
    416 	logg.DebugCtxf(ctx, "loaded additional code", "next", sym, "code", code)
    417 	b = append(b, code...)
    418 	return b, err
    419 }
    420 
    421 // executes the HALT opcode
    422 func (vm *Vm) runHalt(ctx context.Context, b []byte) ([]byte, error) {
    423 	var err error
    424 	b, err = ParseHalt(b)
    425 	if err != nil {
    426 		return b, err
    427 	}
    428 	logg.DebugCtxf(ctx, "found HALT, stopping")
    429 
    430 	vm.st.SetFlag(state.FLAG_WAIT)
    431 	return b, nil
    432 }
    433 
    434 // executes the MSIZE opcode
    435 func (vm *Vm) runMSink(ctx context.Context, b []byte) ([]byte, error) {
    436 	b, err := ParseMSink(b)
    437 	mcfg := vm.mn.GetBrowseConfig()
    438 	vm.mn = vm.mn.WithSink().WithBrowseConfig(mcfg).WithPages()
    439 	//vm.pg.WithMenu(vm.mn)
    440 	return b, err
    441 }
    442 
    443 // executes the MOUT opcode
    444 func (vm *Vm) runMOut(ctx context.Context, b []byte) ([]byte, error) {
    445 	title, choice, b, err := ParseMOut(b)
    446 	if err != nil {
    447 		return b, err
    448 	}
    449 	err = vm.mn.Put(choice, title)
    450 	return b, err
    451 }
    452 
    453 // executes the MNEXT opcode
    454 func (vm *Vm) runMNext(ctx context.Context, b []byte) ([]byte, error) {
    455 	display, selector, b, err := ParseMNext(b)
    456 	if err != nil {
    457 		return b, err
    458 	}
    459 	cfg := vm.mn.GetBrowseConfig()
    460 	cfg.NextSelector = selector
    461 	cfg.NextTitle = display
    462 	cfg.NextAvailable = true
    463 	vm.mn = vm.mn.WithBrowseConfig(cfg)
    464 	return b, nil
    465 }
    466 
    467 // executes the MPREV opcode
    468 func (vm *Vm) runMPrev(ctx context.Context, b []byte) ([]byte, error) {
    469 	display, selector, b, err := ParseMPrev(b)
    470 	if err != nil {
    471 		return b, err
    472 	}
    473 	cfg := vm.mn.GetBrowseConfig()
    474 	cfg.PreviousSelector = selector
    475 	cfg.PreviousTitle = display
    476 	cfg.PreviousAvailable = true
    477 	vm.mn = vm.mn.WithBrowseConfig(cfg)
    478 	return b, nil
    479 }
    480 
    481 // Render wraps output rendering, and handles error when attempting to browse beyond the rendered page count.
    482 func (vm *Vm) Render(ctx context.Context) (string, error) {
    483 	changed := vm.st.ResetFlag(state.FLAG_DIRTY)
    484 	if !changed {
    485 		return "", nil
    486 	}
    487 	sym, idx := vm.st.Where()
    488 	if sym == "" {
    489 		return "", nil
    490 	}
    491 	r, err := vm.pg.Render(ctx, sym, idx)
    492 	var ok bool
    493 	_, ok = err.(*render.BrowseError)
    494 	if ok {
    495 		vm.Reset()
    496 		b := NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
    497 		vm.Run(ctx, b)
    498 		sym, idx := vm.st.Where()
    499 		r, err = vm.pg.Render(ctx, sym, idx)
    500 	}
    501 	if err != nil {
    502 		return "", err
    503 	}
    504 	return r, nil
    505 }
    506 
    507 // retrieve and cache data for key
    508 func (vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (string, error) {
    509 	var err error
    510 
    511 	fn, err := rs.FuncFor(ctx, key)
    512 	if err != nil {
    513 		return "", err
    514 	}
    515 	if fn == nil {
    516 		return "", fmt.Errorf("no retrieve function for external symbol %v", key)
    517 	}
    518 	input, _ := vm.st.GetInput()
    519 	r, err := fn(ctx, key, input)
    520 	if err != nil {
    521 		logg.Errorf("external function load fail", "key", key, "error", err)
    522 		_ = vm.st.SetFlag(state.FLAG_LOADFAIL)
    523 		return "", NewExternalCodeError(key, err).WithCode(r.Status)
    524 	}
    525 	for _, flag := range r.FlagReset {
    526 		if !state.IsWriteableFlag(flag) {
    527 			continue
    528 		}
    529 		vm.st.ResetFlag(flag)
    530 	}
    531 	for _, flag := range r.FlagSet {
    532 		if !state.IsWriteableFlag(flag) {
    533 			continue
    534 		}
    535 		vm.st.SetFlag(flag)
    536 	}
    537 
    538 	haveLang := vm.st.MatchFlag(state.FLAG_LANG, true)
    539 	if haveLang {
    540 		vm.st.SetLanguage(r.Content)
    541 	}
    542 
    543 	return r.Content, err
    544 }