Files
sanspie cf68c71f05 Stage 6: coroutines, signals, ScriptRef, workspace CLI, editor tooling
Language: yield/signal/coroutine become keywords, start is contextual.
Coroutine methods (func Name() coroutine) with start and
yield / wait / frames / fixed / await; signals with Emit/Connect and
Connection handles; ScriptRef<T> and node.GetScript<T>() for fields,
plain methods and signals of other scripts. Diagnostics AES2050-AES2059.

Bytecode v3: ScriptRefs, XFields, XMethods and Connects tables resolved by
name in vm.Link, per-script signals, coroutine flag on functions; new
opcodes START/YIELD/EMIT/CONNECT/DISCONNECT/CONNECTED/COSTOP/CORUN/
SELFREF/GETSCRIPT/GETXF/SETXF/CALLR, checked by verify.

Runtime: script/coroutine scheduler (frames/fixed counters, time heap,
await lists; only due coroutines are visited) and script/signal registry
(snapshot per Emit, deferred removal, no allocations in steady state).
Hot reload cancels coroutines of changed scripts, rebinds connections by
name and signature, then runs Reloaded.

Tooling: script/build removed, aego check/test/dump run on the workspace
(test builds include _test.aes); langsvc completion, hover, signatures and
semantic tokens for the new syntax; editor highlighting, snippets with
placeholders, coroutine/signal profiler graphs and stats.

Docs (script.md sections 16-19, editor.md, ARCHITECTURE), test fixtures and
golden files, platformer enemies wired to the player, scriptreload demo,
and run.Config.FrameTime for deterministic headless fixed steps.

Modules (6.5): engine/module registry and ComponentBuilder are the one
registration path for engine subsystems and native code; render2d,
render3d and script/engine are modules over scene/frame, the script
component moved to scene/scriptcomp, and scene/components and
scene/systems are gone. Editor inspector, templates, picking shapes and
extension hooks are driven by descriptors and editor/builtins; sdk gains
Component/EditorHook; depcheck checks the module graph and the
aego_runtime graph.

Also: stop tracking built tool binaries; gl33 activates the scratch unit
before binding for texture updates.

Verification: typed component method adapters (Method0..2, Proc0..2)
with direct field access and no allocations; app.Build as the single
construction path for run and the editor; modules attach per scene
document; depcheck aego_editor pass; diagnostic codes in one place and
every code covered by a test; indexed coroutine heaps with eager
cancel; dead component references and nil intrinsics fault with
AES6004, host faults keep their code; GC roots of struct fields checked
in verify and vm.Link; 255-field limit; AES4010 for script packages
shadowing host modules; architecture registry D111-D147.

Golden: the harness renders on the thread that owns the GL context,
ReadPixels returns rows in memory order (render targets are top-down per
D17), and the first rendering references are committed.
2026-10-12 14:22:08 +03:00

169 lines
3.8 KiB
Go

package reload
import (
"fmt"
"slices"
"strings"
"time"
"aego/schema"
"aego/script/abi"
"aego/script/runtime"
"aego/script/vm"
)
type Report struct {
From uint64
Generation uint64
Migrated int
Reset int
Killed int
Canceled int
Disconnected int
Elapsed time.Duration
}
func (r Report) String() string {
var b strings.Builder
fmt.Fprintf(&b, "Hot reload %d → %d: %d migrated, %d reset", r.From, r.Generation, r.Migrated, r.Reset)
if r.Killed > 0 {
fmt.Fprintf(&b, ", %d stopped", r.Killed)
}
fmt.Fprintf(&b, ", %d coroutines canceled, %d handlers disconnected, %.1f ms",
r.Canceled, r.Disconnected, float64(r.Elapsed.Microseconds())/1000)
return b.String()
}
type converter struct {
m *vm.Machine
from *layout
to *layout
}
func (c *converter) value(a, b abi.TypeRef, src, dst []vm.Value) bool {
switch classify(a, b, c.from, c.to) {
case Same, Reresolve:
copy(dst, src)
return true
case Struct:
return c.structValue(a, b, src, dst)
case EnumByName:
old, cur := c.from.enums[a.Name], c.to.enums[b.Name]
i := src[0].Int()
if i < 0 || int(i) >= len(old) {
return false
}
j := slices.Index(cur, old[i])
if j < 0 {
return false
}
dst[0] = vm.Int(int64(j))
return true
case Slice:
return c.sliceValue(a, b, src, dst)
}
return false
}
func (c *converter) structValue(a, b abi.TypeRef, src, dst []vm.Value) bool {
os, ns := c.from.structs[a.Name], c.to.structs[b.Name]
for _, nf := range ns.Fields {
of := structField(os, nf.Name)
if of == nil || int(of.Slot)+int(of.Slots) > len(src) || int(nf.Slot)+int(nf.Slots) > len(dst) {
continue
}
ot, ok1 := abi.Parse(of.Type)
nt, ok2 := abi.Parse(nf.Type)
if !ok1 || !ok2 {
continue
}
d := dst[nf.Slot : int(nf.Slot)+int(nf.Slots)]
tmp := append([]vm.Value(nil), d...)
if c.value(ot, nt, src[of.Slot:int(of.Slot)+int(of.Slots)], tmp) {
copy(d, tmp)
}
}
return true
}
func (c *converter) sliceValue(a, b abi.TypeRef, src, dst []vm.Value) bool {
if src[0].A == 0 {
dst[0] = vm.Value{}
return true
}
elems, w, ok := c.m.SliceData(src[0])
if !ok || w == 0 {
return false
}
ae, be := *a.Elem, *b.Elem
nw := c.to.slots(be)
n := len(elems) / w
out := make([]vm.Value, n*nw)
for k := 0; k < n; k++ {
if !c.value(ae, be, elems[k*w:(k+1)*w], out[k*nw:(k+1)*nw]) {
return false
}
}
dst[0] = c.m.MakeSlice(nw, c.to.mask(be), out)
return true
}
func (p *Plan) migrate(c *converter, newName string, src, dst []vm.Value) int {
tc := p.byName[newName]
if tc == nil || tc.Old == nil {
return 0
}
reset := 0
for _, fm := range tc.Fields {
if fm.OldSlot+fm.OldSlots > len(src) || fm.NewSlot+fm.NewSlots > len(dst) {
reset++
continue
}
s := src[fm.OldSlot : fm.OldSlot+fm.OldSlots]
d := dst[fm.NewSlot : fm.NewSlot+fm.NewSlots]
switch fm.Conv {
case Same, Reresolve:
copy(d, s)
case Default:
reset++
default:
tmp := append([]vm.Value(nil), d...)
if c.value(fm.Old, fm.New, s, tmp) {
copy(d, tmp)
} else {
reset++
}
}
}
return reset
}
func Apply(r *runtime.Runtime, reg *schema.Registry, p *Plan) (Report, error) {
start := time.Now()
rep := Report{From: p.From.ID, Generation: p.To.ID}
sw := r.BeginSwap(reg)
if err := sw.LoadModules(p.To.Ordered()); err != nil {
sw.Abort()
return rep, err
}
c := &converter{m: r.Machine(), from: p.from, to: p.to}
migrated, reset, killed, err := sw.Migrate(func(_, newName string, src, dst []vm.Value) int {
return p.migrate(c, newName, src, dst)
})
if err != nil {
sw.Abort()
return rep, err
}
again, err := sw.Commit(p.To.ID, p.changed)
if err != nil {
sw.Abort()
return rep, err
}
rep.Canceled = r.CancelChanged(p.changed)
rep.Disconnected = r.RebindSignals()
r.CallReloaded(again)
rep.Migrated, rep.Reset, rep.Killed = migrated, reset, killed
rep.Elapsed = time.Since(start)
return rep, nil
}