go-vise

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

engine.go (5838B)


      1 package engine
      2 
      3 import (
      4 	"context"
      5 	"fmt"
      6 	"io"
      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 	"git.defalsify.org/vise.git/vm"
     13 )
     14 
     15 // EngineIsh defines the interface for execution engines that handle vm initialization and execution, and rendering outputs.
     16 type EngineIsh interface {
     17 	Init(ctx context.Context) (bool, error)
     18 	Exec(ctx context.Context, input []byte) (bool, error)
     19 	WriteResult(ctx context.Context, w io.Writer) (int, error)
     20 	Finish() error
     21 }
     22 
     23 // Config globally defines behavior of all components driven by the engine.
     24 type Config struct {
     25 	OutputSize uint32 // Maximum size of output from a single rendered page
     26 	SessionId string
     27 	Root string
     28 	FlagCount uint32
     29 	CacheSize uint32
     30 	Language string
     31 }
     32 
     33 // Engine is an execution engine that handles top-level errors when running client inputs against code in the bytecode buffer.
     34 type Engine struct {
     35 	st *state.State
     36 	rs resource.Resource
     37 	ca cache.Memory
     38 	vm *vm.Vm
     39 	root string
     40 	session string
     41 	initd bool
     42 }
     43 
     44 // NewEngine creates a new Engine
     45 func NewEngine(ctx context.Context, cfg Config, st *state.State, rs resource.Resource, ca cache.Memory) Engine {
     46 	var szr *render.Sizer
     47 	if cfg.OutputSize > 0 {
     48 		szr = render.NewSizer(cfg.OutputSize)
     49 	}
     50 	ctx = context.WithValue(ctx, "sessionId", cfg.SessionId)
     51 	engine := Engine{
     52 		st: st,
     53 		rs: rs,
     54 		ca: ca,
     55 		vm: vm.NewVm(st, rs, ca, szr),
     56 	}
     57 	engine.root = cfg.Root	
     58 	engine.session = cfg.SessionId
     59 
     60 	var err error
     61 	if st.Language == nil {
     62 		if cfg.Language != "" {
     63 			err = st.SetLanguage(cfg.Language)
     64 			if err != nil {
     65 				panic(err)
     66 			}
     67 			Logg.InfoCtxf(ctx, "set language from config", "language", cfg.Language)
     68 		}
     69 	}
     70 
     71 	if st.Language != nil {
     72 		st.SetFlag(state.FLAG_LANG)
     73 	}
     74 	return engine
     75 }
     76 
     77 // Finish implements EngineIsh interface
     78 func(en *Engine) Finish() error {
     79 	Logg.Tracef("that's a wrap", "engine", en)
     80 	return nil
     81 }
     82 
     83 // change root to current state location if non-empty.
     84 func(en *Engine) restore() {
     85 	location, _ := en.st.Where()
     86 	if len(location) == 0 {
     87 		return
     88 	}
     89 	if en.root != location {
     90 		Logg.Infof("restoring state: %s", location)
     91 		en.root = "."
     92 	}
     93 }
     94 
     95 // Init must be explicitly called before using the Engine instance.
     96 //
     97 // It loads and executes code for the start node.
     98 func(en *Engine) Init(ctx context.Context) (bool, error) {
     99 	en.restore()
    100 	if en.initd {
    101 		Logg.DebugCtxf(ctx, "already initialized")
    102 		return true, nil
    103 	}
    104 	
    105 	sym := en.root
    106 	if sym == "" {
    107 		return false, fmt.Errorf("start sym empty")
    108 	}
    109 	inSave, _ := en.st.GetInput()
    110 	err := en.st.SetInput([]byte{})
    111 	if err != nil {
    112 		return false, err
    113 	}
    114 	b := vm.NewLine(nil, vm.MOVE, []string{sym}, nil, nil)
    115 	Logg.DebugCtxf(ctx, "start new init VM run", "code", b)
    116 	b, err = en.vm.Run(ctx, b)
    117 	if err != nil {
    118 		return false, err
    119 	}
    120 	
    121 	Logg.DebugCtxf(ctx, "end new init VM run", "code", b)
    122 	en.st.SetCode(b)
    123 	err = en.st.SetInput(inSave)
    124 	if err != nil {
    125 		return false, err
    126 	}
    127 	return len(b) > 0, nil
    128 }
    129 
    130 // Exec processes user input against the current state of the virtual machine environment.
    131 //
    132 // If successfully executed, output of the last execution is available using the WriteResult call.
    133 // 
    134 // A bool return valus of false indicates that execution should be terminated. Calling Exec again has undefined effects.
    135 //
    136 // Fails if:
    137 // - input is formally invalid (too long etc)
    138 // - no current bytecode is available
    139 // - input processing against bytcode failed
    140 func (en *Engine) Exec(ctx context.Context, input []byte) (bool, error) {
    141 	var err error
    142 	if en.st.Language != nil {
    143 		ctx = context.WithValue(ctx, "Language", *en.st.Language)
    144 	}
    145 	if en.st.Moves == 0 {
    146 		cont, err := en.Init(ctx)
    147 		if err != nil {
    148 			return false, err
    149 		}
    150 		return cont, nil
    151 	}
    152 	err = vm.ValidInput(input)
    153 	if err != nil {
    154 		return true, err
    155 	}
    156 	err = en.st.SetInput(input)
    157 	if err != nil {
    158 		return false, err
    159 	}
    160 	return en.exec(ctx, input)
    161 }
    162 
    163 // backend for Exec, after the input validity check
    164 func(en *Engine) exec(ctx context.Context, input []byte) (bool, error) {
    165 	Logg.InfoCtxf(ctx, "new VM execution with input", "input", string(input))
    166 	code, err := en.st.GetCode()
    167 	if err != nil {
    168 		return false, err
    169 	}
    170 	if len(code) == 0 {
    171 		return false, fmt.Errorf("no code to execute")
    172 	}
    173 
    174 	Logg.Debugf("start new VM run", "code", code)
    175 	code, err = en.vm.Run(ctx, code)
    176 	if err != nil {
    177 		return false, err
    178 	}
    179 	Logg.Debugf("end new VM run", "code", code)
    180 
    181 	v := en.st.MatchFlag(state.FLAG_TERMINATE, true)
    182 	if v {
    183 		if len(code) > 0 {
    184 			Logg.Debugf("terminated with code remaining", "code", code)
    185 		}
    186 		return false, err
    187 	}
    188 
    189 	en.st.SetCode(code)
    190 	if len(code) == 0 {
    191 		Logg.Infof("runner finished with no remaining code")
    192 		_, err = en.reset(ctx)
    193 		return false, err
    194 	}
    195 	return true, nil
    196 }
    197 
    198 // WriteResult writes the output of the last vm execution to the given writer.
    199 //
    200 // Fails if
    201 // - required data inputs to the template are not available.
    202 // - the template for the given node point is note available for retrieval using the resource.Resource implementer.
    203 // - the supplied writer fails to process the writes.
    204 func(en *Engine) WriteResult(ctx context.Context, w io.Writer) (int, error) {
    205 	if en.st.Language != nil {
    206 		ctx = context.WithValue(ctx, "Language", *en.st.Language)
    207 	}
    208 	Logg.TraceCtxf(ctx, "render with state", "state", en.st)
    209 	r, err := en.vm.Render(ctx)
    210 	if err != nil {
    211 		return 0, err
    212 	}
    213 	return io.WriteString(w, r)
    214 }
    215 
    216 // start execution over at top node while keeping current state of client error flags.
    217 func(en *Engine) reset(ctx context.Context) (bool, error) {
    218 	var err error
    219 	var isTop bool
    220 	for !isTop {
    221 		isTop, err = en.st.Top()
    222 		if err != nil {
    223 			return false, err
    224 		}
    225 		_, err = en.st.Up()
    226 		if err != nil {
    227 			return false, err
    228 		}
    229 		en.ca.Pop()
    230 	}
    231 	en.st.Restart()
    232 	en.initd = false
    233 	return en.Init(ctx)
    234 }