go-vise

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

runner.go (13431B)


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