go-vise

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

runner.go (12515B)


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