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