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