go-vise

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

runner.go (12495B)


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