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