Files
aego/script/vm/interp.go
T
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00

463 lines
13 KiB
Go

package vm
import (
"math"
"strconv"
"aego/script/bytecode"
)
func (m *Machine) at(top int) (*frame, *Func, int, []bytecode.Instr, []Value) {
fr := &m.frames[top]
return fr, fr.f, fr.pc, fr.f.code, m.stack[fr.base : fr.base+fr.f.Regs]
}
func (m *Machine) enter(top, pc int, callee *Func, nb, ret int, self []Value, owner InstanceID) bool {
if len(m.frames) >= m.opt.MaxCallDepth {
m.trap(top, pc, CodeCallDepth, "call depth exceeds "+strconv.Itoa(m.opt.MaxCallDepth))
return false
}
if need := nb + callee.Regs; need > len(m.stack) {
m.grow(need)
}
clear(m.stack[nb+callee.Params : nb+callee.Regs])
m.frames[top].pc = pc
m.frames = append(m.frames, frame{f: callee, base: nb, ret: ret, self: self, owner: owner})
m.stats.Calls++
return true
}
func (m *Machine) run(floor int) {
top := len(m.frames) - 1
fr, f, pc, code, regs := m.at(top)
for {
if m.left == 0 {
m.stats.BudgetAborts++
m.trap(top, pc+1, CodeBudget, "instruction budget exceeded")
return
}
m.left--
in := code[pc]
pc++
a, b, c := int(in.A()), int(in.B()), int(in.C())
switch in.Op() {
case bytecode.NOP:
case bytecode.MOVE:
regs[a] = regs[b]
case bytecode.MOVEN:
copy(regs[a:a+c], regs[b:b+c])
case bytecode.LOADK:
regs[a] = f.consts[in.Bx()]
case bytecode.LOADI:
regs[a] = Int(int64(in.SBx()))
case bytecode.LOADNIL:
regs[a] = Value{}
case bytecode.LOADBOOL:
regs[a] = Bool(b != 0)
case bytecode.ADDI:
regs[a] = Int(regs[b].Int() + regs[c].Int())
case bytecode.SUBI:
regs[a] = Int(regs[b].Int() - regs[c].Int())
case bytecode.MULI:
regs[a] = Int(regs[b].Int() * regs[c].Int())
case bytecode.DIVI, bytecode.MODI:
x, y := regs[b].Int(), regs[c].Int()
switch {
case y == 0:
m.trap(top, pc, CodeDivZero, "integer division by zero")
return
case y == -1 && in.Op() == bytecode.DIVI:
regs[a] = Int(-x)
case y == -1:
regs[a] = Value{}
case in.Op() == bytecode.DIVI:
regs[a] = Int(x / y)
default:
regs[a] = Int(x % y)
}
case bytecode.NEGI:
regs[a] = Int(-regs[b].Int())
case bytecode.ADDF:
regs[a] = Float(regs[b].Float() + regs[c].Float())
case bytecode.SUBF:
regs[a] = Float(regs[b].Float() - regs[c].Float())
case bytecode.MULF:
regs[a] = Float(regs[b].Float() * regs[c].Float())
case bytecode.DIVF:
regs[a] = Float(regs[b].Float() / regs[c].Float())
case bytecode.NEGF:
regs[a] = Float(-regs[b].Float())
case bytecode.ITOF:
regs[a] = Float(float32(regs[b].Int()))
case bytecode.FTOI:
regs[a] = Int(ftoi(regs[b].Float()))
case bytecode.ITOS:
regs[a] = m.NewString(strconv.FormatInt(regs[b].Int(), 10))
case bytecode.FTOS:
regs[a] = m.NewString(strconv.FormatFloat(float64(regs[b].Float()), 'g', -1, 32))
case bytecode.BTOS:
if regs[b].Bool() {
regs[a] = m.Intern("true")
} else {
regs[a] = m.Intern("false")
}
case bytecode.ADDS:
x, ok1 := m.Str(regs[b])
y, ok2 := m.Str(regs[c])
switch {
case !ok1 || !ok2:
m.trap(top, pc, CodeBadHandle, "string handle is not valid")
return
case x == "":
regs[a] = regs[c]
case y == "":
regs[a] = regs[b]
default:
regs[a] = m.NewString(x + y)
}
case bytecode.LENS:
s, ok := m.Str(regs[b])
if !ok {
m.trap(top, pc, CodeBadHandle, "string handle is not valid")
return
}
regs[a] = Int(int64(len(s)))
case bytecode.EQ:
regs[a] = Bool(regs[b] == regs[c])
case bytecode.EQF:
regs[a] = Bool(regs[b].Float() == regs[c].Float())
case bytecode.EQS, bytecode.LTS, bytecode.LES:
x, ok1 := m.Str(regs[b])
y, ok2 := m.Str(regs[c])
if !ok1 || !ok2 {
m.trap(top, pc, CodeBadHandle, "string handle is not valid")
return
}
switch in.Op() {
case bytecode.EQS:
regs[a] = Bool(x == y)
case bytecode.LTS:
regs[a] = Bool(x < y)
default:
regs[a] = Bool(x <= y)
}
case bytecode.EQV:
x, y := regs[b], regs[c]
regs[a] = Bool(x.Lane(0) == y.Lane(0) && x.Lane(1) == y.Lane(1) && x.Lane(2) == y.Lane(2) && x.Lane(3) == y.Lane(3))
case bytecode.LTI:
regs[a] = Bool(regs[b].Int() < regs[c].Int())
case bytecode.LEI:
regs[a] = Bool(regs[b].Int() <= regs[c].Int())
case bytecode.LTF:
regs[a] = Bool(regs[b].Float() < regs[c].Float())
case bytecode.LEF:
regs[a] = Bool(regs[b].Float() <= regs[c].Float())
case bytecode.NOT:
regs[a] = Bool(!regs[b].Bool())
case bytecode.JMP:
pc += int(in.SBx())
case bytecode.JMPIF:
if regs[a].A != 0 {
pc += int(in.SBx())
}
case bytecode.JMPNOT:
if regs[a].A == 0 {
pc += int(in.SBx())
}
case bytecode.CALL, bytecode.CALLX:
var callee *Func
if in.Op() == bytecode.CALL {
callee = f.mod.Funcs[in.Bx()]
} else if callee = f.mod.externs[in.Bx()]; callee == nil {
m.trap(top, pc, CodeLink, "extern function is not linked")
return
}
if !m.enter(top, pc, callee, fr.base+a, fr.base+a, fr.self, fr.owner) {
return
}
top++
fr, f, pc, code, regs = m.at(top)
case bytecode.CALLR:
x := &f.mod.xmethods[in.Bx()]
if m.Scripts == nil {
m.trap(top, pc, CodeLink, "script references are not bound")
return
}
tgt, id, ok := m.Scripts.Instance(regs[a], x.script)
if !ok {
m.trap(top, pc, CodeNil, "script reference is nil or its instance is gone")
return
}
if !m.enter(top, pc, x.fn, fr.base+a+1, fr.base+a, tgt, id) {
return
}
top++
fr, f, pc, code, regs = m.at(top)
case bytecode.START:
callee := f.mod.Funcs[in.Bx()]
if !m.enter(top, pc, callee, fr.base+a, fr.base+a, fr.self, fr.owner) {
return
}
h := m.newCo(callee, fr.owner)
top++
m.frames[top].co = h.Index + 1
fr, f, pc, code, regs = m.at(top)
case bytecode.CALLH:
bx := int(in.Bx())
m.frames[top].pc = pc
m.stats.HostCalls++
if err := f.mod.hosts[bx](m, regs[a:a+f.mod.windows[bx]]); err != nil {
if m.fault == nil {
m.trapErr(top, pc, err)
}
return
}
case bytecode.RET, bytecode.YIELD:
switch {
case in.Op() == bytecode.YIELD && fr.co == 0:
m.trap(top, pc, CodeLink, "yield outside a coroutine frame")
return
case in.Op() == bytecode.YIELD:
arg := Value{}
if c != int(WaitNext) {
arg = regs[a]
}
m.yieldCo(fr.co-1, fr.ret, pc, regs, WaitKind(c), arg)
case fr.co != 0:
m.finishCo(fr.co-1, fr.ret)
default:
copy(m.stack[fr.ret:fr.ret+b], regs[a:a+b])
}
m.frames = m.frames[:top]
if top == floor {
return
}
top--
fr, f, pc, code, regs = m.at(top)
case bytecode.GETSF, bytecode.SETSF:
bx := int(in.Bx())
self := fr.self
if bx >= len(self) {
m.trap(top, pc, CodeLink, "script field is outside the instance")
return
}
if in.Op() == bytecode.GETSF {
regs[a] = self[bx]
} else {
self[bx] = regs[a]
}
case bytecode.GETCF, bytecode.SETCF:
ref := regs[b]
if in.Op() == bytecode.SETCF {
ref = regs[a]
}
switch {
case m.Comps == nil:
m.trap(top, pc, CodeLink, "component access is not bound")
return
case ref.IsZero():
m.trap(top, pc, CodeNil, "component reference is nil")
return
}
var err error
if in.Op() == bytecode.GETCF {
err = m.Comps.GetField(ref, c, &regs[a])
} else {
err = m.Comps.SetField(ref, c, regs[b])
}
if err != nil {
m.trap(top, pc, CodeNil, err.Error())
return
}
case bytecode.GETXF, bytecode.SETXF:
x := &f.mod.xfields[c]
ref, val := regs[b], a
if in.Op() == bytecode.SETXF {
ref, val = regs[a], b
}
if m.Scripts == nil {
m.trap(top, pc, CodeLink, "script references are not bound")
return
}
tgt, _, ok := m.Scripts.Instance(ref, x.script)
if !ok || x.slot+x.width > len(tgt) {
m.trap(top, pc, CodeNil, "script reference is nil or its instance is gone")
return
}
if in.Op() == bytecode.GETXF {
copy(regs[val:val+x.width], tgt[x.slot:x.slot+x.width])
} else {
copy(tgt[x.slot:x.slot+x.width], regs[val:val+x.width])
}
case bytecode.GETSCRIPT:
if m.Scripts == nil {
m.trap(top, pc, CodeLink, "script references are not bound")
return
}
if regs[a].IsZero() {
m.trap(top, pc, CodeNil, "node reference is nil")
return
}
ref, err := m.Scripts.Find(regs[a], f.mod.src.ScriptRefs[in.Bx()])
if err != nil {
m.trap(top, pc, CodeNil, err.Error())
return
}
regs[a] = ref
case bytecode.SELFREF:
if len(fr.self) == 0 {
m.trap(top, pc, CodeLink, "self reference outside a script instance")
return
}
regs[a] = Value{A: fr.self[0].A, B: uint64(fr.owner)}
case bytecode.COSTOP:
if !regs[a].IsZero() {
m.Cancel(CoOf(regs[a]))
}
case bytecode.CORUN:
regs[a] = Bool(m.CoRunning(regs[b]))
case bytecode.EMIT:
if m.Signals == nil {
m.trap(top, pc, CodeLink, "signals are not bound")
return
}
n := f.mod.sigSlots[f.Script][in.Bx()]
m.frames[top].pc = pc
if err := m.Signals.Emit(fr.owner, int(in.Bx()), regs[a:a+n]); err != nil {
m.trapErr(top, pc, err)
return
}
if m.fault != nil {
return
}
fr, f, pc, code, regs = m.at(top)
case bytecode.CONNECT:
if m.Signals == nil {
m.trap(top, pc, CodeLink, "signals are not bound")
return
}
v, err := m.Signals.Connect(regs[a+1], regs[a+2], &f.mod.connects[in.Bx()])
if err != nil {
m.trap(top, pc, CodeNil, err.Error())
return
}
regs[a] = v
case bytecode.DISCONNECT:
if !regs[a].IsZero() && m.Signals != nil {
m.Signals.Disconnect(regs[a])
}
case bytecode.CONNECTED:
regs[a] = Bool(!regs[b].IsZero() && m.Signals != nil && m.Signals.Connected(regs[b]))
case bytecode.NEWSLICE:
t := f.mod.types[in.Bx()]
regs[a] = m.heap.newSlice(t.width, t.mask, 4)
case bytecode.ENSURE:
if regs[a].A == 0 {
t := f.mod.types[in.Bx()]
regs[a] = m.heap.newSlice(t.width, t.mask, 4)
}
case bytecode.LEN:
regs[a] = Int(int64(regs[b].B))
case bytecode.INDEX, bytecode.SETINDEX:
s, i, at := regs[b], regs[c].Int(), a
if in.Op() == bytecode.SETINDEX {
s, i, at = regs[a], regs[b].Int(), c
}
if i < 0 || uint64(i) >= s.B {
m.trap(top, pc, CodeIndex, "index "+strconv.FormatInt(i, 10)+" out of range for length "+strconv.FormatUint(s.B, 10))
return
}
o := m.heap.get(s.A)
if o == nil || o.str {
m.trap(top, pc, CodeBadHandle, "slice handle is not valid")
return
}
w := o.width
off := int(i) * w
if at+w > len(regs) || off+w > len(o.elems) {
m.trap(top, pc, CodeLink, "slice element does not fit the registers")
return
}
if in.Op() == bytecode.INDEX {
copy(regs[at:at+w], o.elems[off:off+w])
} else {
copy(o.elems[off:off+w], regs[at:at+w])
}
case bytecode.APPEND:
s := regs[b]
o := m.heap.get(s.A)
if o == nil || o.str {
m.trap(top, pc, CodeNil, "append to a nil slice")
return
}
w := o.width
n := int(s.B) * w
if c+w > len(regs) || n > len(o.elems) {
m.trap(top, pc, CodeLink, "slice element does not fit the registers")
return
}
if n == len(o.elems) {
before := cap(o.elems)
o.elems = append(o.elems, regs[c:c+w]...)
if d := cap(o.elems) - before; d > 0 {
m.heap.grow(o, d*16)
}
regs[a] = Value{A: s.A, B: s.B + 1}
break
}
width, mask := o.width, o.mask
nv := m.heap.newSlice(width, mask, int(s.B)+1)
old, no := m.heap.get(s.A), m.heap.get(nv.A)
no.elems = append(no.elems, old.elems[:n]...)
no.elems = append(no.elems, regs[c:c+w]...)
regs[a] = Value{A: nv.A, B: s.B + 1}
case bytecode.VEC2:
regs[a] = Vec(regs[b].Float(), regs[b+1].Float(), 0, 0)
case bytecode.VEC3:
regs[a] = Vec(regs[b].Float(), regs[b+1].Float(), regs[b+2].Float(), 0)
case bytecode.VEC4:
regs[a] = Vec(regs[b].Float(), regs[b+1].Float(), regs[b+2].Float(), regs[b+3].Float())
case bytecode.GETL:
regs[a] = Float(regs[b].Lane(c))
case bytecode.SETL:
regs[a] = regs[a].WithLane(c, regs[b].Float())
case bytecode.VADD:
x, y := regs[b], regs[c]
regs[a] = Vec(x.Lane(0)+y.Lane(0), x.Lane(1)+y.Lane(1), x.Lane(2)+y.Lane(2), x.Lane(3)+y.Lane(3))
case bytecode.VSUB:
x, y := regs[b], regs[c]
regs[a] = Vec(x.Lane(0)-y.Lane(0), x.Lane(1)-y.Lane(1), x.Lane(2)-y.Lane(2), x.Lane(3)-y.Lane(3))
case bytecode.VMULS:
x, s := regs[b], regs[c].Float()
regs[a] = Vec(x.Lane(0)*s, x.Lane(1)*s, x.Lane(2)*s, x.Lane(3)*s)
case bytecode.VDIVS:
x, s := regs[b], regs[c].Float()
regs[a] = Vec(x.Lane(0)/s, x.Lane(1)/s, x.Lane(2)/s, x.Lane(3)/s)
case bytecode.VNEG:
x := regs[b]
regs[a] = Vec(-x.Lane(0), -x.Lane(1), -x.Lane(2), -x.Lane(3))
case bytecode.ISNIL:
regs[a] = Bool(regs[b].IsZero())
case bytecode.FAULT:
msg, _ := m.Str(f.consts[in.Bx()])
m.trap(top, pc, CodeExplicit, msg)
return
default:
m.trap(top, pc, CodeLink, "invalid opcode")
return
}
}
}
func ftoi(f float32) int64 {
switch {
case f != f:
return 0
case f >= 9.223372036854776e18:
return math.MaxInt64
case f <= -9.223372036854776e18:
return math.MinInt64
}
return int64(f)
}