go-vise

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

runner.go (13468B)


      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 		logg.Errorf("load fail", "sym", sym, "error", err)
    317 		return b, err
    318 	}
    319 	err = vm.ca.Add(sym, r, uint16(sz))
    320 	if err != nil {
    321 		if err == cache.ErrDup {
    322 			logg.DebugCtxf(ctx, "Ignoring load request on frame that has symbol already loaded", "sym", sym)
    323 			err = nil
    324 		}
    325 	}
    326 	return b, err
    327 }
    328 
    329 // executes the RELOAD opcode
    330 func (vm *Vm) runReload(ctx context.Context, b []byte) ([]byte, error) {
    331 	sym, b, err := ParseReload(b)
    332 	if err != nil {
    333 		return b, err
    334 	}
    335 
    336 	r, err := vm.refresh(sym, vm.rs, ctx)
    337 	if err != nil {
    338 		logg.Errorf("reload fail", "sym", sym, "error", err)
    339 		return b, err
    340 	}
    341 	vm.ca.Update(sym, r)
    342 	if vm.pg != nil {
    343 		err := vm.pg.Map(sym)
    344 		if err != nil {
    345 			return b, err
    346 		}
    347 	}
    348 	return b, nil
    349 }
    350 
    351 // executes the MOVE opcode
    352 func (vm *Vm) runMove(ctx context.Context, b []byte) ([]byte, error) {
    353 	sym, b, err := ParseMove(b)
    354 	if err != nil {
    355 		return b, err
    356 	}
    357 	sym, _, err = applyTarget([]byte(sym), vm.st, vm.ca, ctx)
    358 	if err != nil {
    359 		return b, err
    360 	}
    361 	code, err := vm.rs.GetCode(ctx, sym)
    362 	if err != nil {
    363 		return b, err
    364 	}
    365 	logg.DebugCtxf(ctx, "loaded code", "sym", sym, "code", code)
    366 	b = append(b, code...)
    367 	vm.Reset()
    368 	return b, nil
    369 }
    370 
    371 // executes the INCMP opcode
    372 // TODO: document state transition table and simplify flow
    373 func (vm *Vm) runInCmp(ctx context.Context, b []byte) ([]byte, error) {
    374 	sym, target, b, err := ParseInCmp(b)
    375 	if err != nil {
    376 		return b, err
    377 	}
    378 
    379 	reading := vm.st.GetFlag(state.FLAG_READIN)
    380 	have := vm.st.GetFlag(state.FLAG_INMATCH)
    381 	if err != nil {
    382 		panic(err)
    383 	}
    384 	if have {
    385 		if reading {
    386 			logg.DebugCtxf(ctx, "ignoring input - already have match", "input", sym)
    387 			return b, nil
    388 		}
    389 	} else {
    390 		vm.st.SetFlag(state.FLAG_READIN)
    391 	}
    392 	input, err := vm.st.GetInput()
    393 	if err != nil {
    394 		return b, err
    395 	}
    396 	logg.TraceCtxf(ctx, "testing sym", "sym", sym, "input", input)
    397 
    398 	if !have && target == "*" {
    399 		logg.DebugCtxf(ctx, "input wildcard match", "input", input, "next", sym)
    400 	} else {
    401 		if target != string(input) {
    402 			return b, nil
    403 		}
    404 		logg.InfoCtxf(ctx, "input match", "input", input, "next", sym)
    405 	}
    406 	vm.st.SetFlag(state.FLAG_INMATCH)
    407 	vm.st.ResetFlag(state.FLAG_READIN)
    408 
    409 	newSym, _, err := applyTarget([]byte(sym), vm.st, vm.ca, ctx)
    410 
    411 	//_, ok := err.(*state.IndexError)
    412 	//if ok {
    413 	if errors.Is(err, state.IndexError) {
    414 		vm.st.SetFlag(state.FLAG_READIN)
    415 		return b, nil
    416 	} else if err != nil {
    417 		return b, err
    418 	}
    419 
    420 	sym = newSym
    421 
    422 	vm.Reset()
    423 
    424 	code, err := vm.rs.GetCode(ctx, sym)
    425 	if err != nil {
    426 		return b, err
    427 	}
    428 	logg.DebugCtxf(ctx, "loaded additional code", "next", sym, "code", code)
    429 	b = append(b, code...)
    430 	return b, err
    431 }
    432 
    433 // executes the HALT opcode
    434 func (vm *Vm) runHalt(ctx context.Context, b []byte) ([]byte, error) {
    435 	var err error
    436 	b, err = ParseHalt(b)
    437 	if err != nil {
    438 		return b, err
    439 	}
    440 	logg.DebugCtxf(ctx, "found HALT, stopping")
    441 
    442 	vm.st.SetFlag(state.FLAG_WAIT)
    443 	return b, nil
    444 }
    445 
    446 // executes the MSIZE opcode
    447 func (vm *Vm) runMSink(ctx context.Context, b []byte) ([]byte, error) {
    448 	b, err := ParseMSink(b)
    449 	mcfg := vm.mn.GetBrowseConfig()
    450 	vm.mn = vm.mn.WithSink().WithBrowseConfig(mcfg).WithPages()
    451 	//vm.pg.WithMenu(vm.mn)
    452 	return b, err
    453 }
    454 
    455 // executes the MOUT opcode
    456 func (vm *Vm) runMOut(ctx context.Context, b []byte) ([]byte, error) {
    457 	title, choice, b, err := ParseMOut(b)
    458 	if err != nil {
    459 		return b, err
    460 	}
    461 	err = vm.mn.Put(choice, title)
    462 	return b, err
    463 }
    464 
    465 // executes the MNEXT opcode
    466 func (vm *Vm) runMNext(ctx context.Context, b []byte) ([]byte, error) {
    467 	display, selector, b, err := ParseMNext(b)
    468 	if err != nil {
    469 		return b, err
    470 	}
    471 	cfg := vm.mn.GetBrowseConfig()
    472 	cfg.NextSelector = selector
    473 	cfg.NextTitle = display
    474 	cfg.NextAvailable = true
    475 	vm.mn = vm.mn.WithBrowseConfig(cfg)
    476 	return b, nil
    477 }
    478 
    479 // executes the MPREV opcode
    480 func (vm *Vm) runMPrev(ctx context.Context, b []byte) ([]byte, error) {
    481 	display, selector, b, err := ParseMPrev(b)
    482 	if err != nil {
    483 		return b, err
    484 	}
    485 	cfg := vm.mn.GetBrowseConfig()
    486 	cfg.PreviousSelector = selector
    487 	cfg.PreviousTitle = display
    488 	cfg.PreviousAvailable = true
    489 	vm.mn = vm.mn.WithBrowseConfig(cfg)
    490 	return b, nil
    491 }
    492 
    493 // Render wraps output rendering, and handles error when attempting to browse beyond the rendered page count.
    494 func (vm *Vm) Render(ctx context.Context) (string, error) {
    495 	changed := vm.st.ResetFlag(state.FLAG_DIRTY)
    496 	if !changed {
    497 		return "", nil
    498 	}
    499 	sym, idx := vm.st.Where()
    500 	if sym == "" {
    501 		return "", nil
    502 	}
    503 	r, err := vm.pg.Render(ctx, sym, idx)
    504 	var ok bool
    505 	_, ok = err.(*render.BrowseError)
    506 	if ok {
    507 		vm.Reset()
    508 		b := NewLine(nil, MOVE, []string{"_catch"}, nil, nil)
    509 		vm.Run(ctx, b)
    510 		sym, idx := vm.st.Where()
    511 		r, err = vm.pg.Render(ctx, sym, idx)
    512 	}
    513 	if err != nil {
    514 		return "", err
    515 	}
    516 	return r, nil
    517 }
    518 
    519 // retrieve and cache data for key
    520 func (vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (string, error) {
    521 	var err error
    522 	vm.last = key
    523 	fn, err := rs.FuncFor(ctx, key)
    524 	if err != nil {
    525 		return "", err
    526 	}
    527 	if fn == nil {
    528 		return "", fmt.Errorf("no retrieve function for external symbol %v", key)
    529 	}
    530 	input, _ := vm.st.GetInput()
    531 	r, err := fn(ctx, key, input)
    532 	if err != nil {
    533 		_ = vm.st.SetFlag(state.FLAG_LOADFAIL)
    534 		return "", NewExternalCodeError(key, err).WithCode(r.Status)
    535 	}
    536 	for _, flag := range r.FlagReset {
    537 		if !state.IsWriteableFlag(flag) {
    538 			continue
    539 		}
    540 		vm.st.ResetFlag(flag)
    541 	}
    542 	for _, flag := range r.FlagSet {
    543 		if !state.IsWriteableFlag(flag) {
    544 			continue
    545 		}
    546 		vm.st.SetFlag(flag)
    547 	}
    548 
    549 	haveLang := vm.st.MatchFlag(state.FLAG_LANG, true)
    550 	if haveLang {
    551 		vm.st.SetLanguage(r.Content)
    552 	}
    553 
    554 	return r.Content, err
    555 }