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

223 lines
4.8 KiB
Go

package reload
import (
"fmt"
"slices"
"sort"
"aego/script/abi"
"aego/script/bytecode"
"aego/script/diag"
"aego/script/source"
)
type Conversion uint8
const (
Same Conversion = iota
Default
EnumByName
Struct
Slice
Reresolve
)
type FieldMap struct {
Name string
OldSlot int
NewSlot int
OldSlots int
NewSlots int
Old abi.TypeRef
New abi.TypeRef
Conv Conversion
}
type TypeChange struct {
Name string
Old *bytecode.ScriptDef
New *bytecode.ScriptDef
Fields []FieldMap
Reset []string
Removed []string
Changed bool
}
type Plan struct {
From Program
To Program
Types []TypeChange
Diags []diag.Diagnostic
from *layout
to *layout
byName map[string]*TypeChange
}
func (p *Plan) warn(format string, args ...any) {
p.Diags = append(p.Diags, diag.Warnf("", source.Span{}, format, args...))
}
func (p *Plan) changed(name string) bool {
tc := p.byName[name]
return tc == nil || tc.Changed
}
func sameLayout(a, b abi.TypeRef, from, to *layout) bool {
if !a.Equal(b) {
return false
}
switch a.Kind {
case abi.Struct:
os, ns := from.structs[a.Name], to.structs[b.Name]
if os == nil || ns == nil {
return os == nil && ns == nil
}
if os.Slots != ns.Slots || len(os.Fields) != len(ns.Fields) {
return false
}
for i := range os.Fields {
of, nf := os.Fields[i], ns.Fields[i]
if of.Name != nf.Name || of.Slot != nf.Slot || of.Slots != nf.Slots {
return false
}
ot, ok1 := abi.Parse(of.Type)
nt, ok2 := abi.Parse(nf.Type)
if !ok1 || !ok2 || !sameLayout(ot, nt, from, to) {
return false
}
}
case abi.Slice:
return a.Elem != nil && b.Elem != nil && sameLayout(*a.Elem, *b.Elem, from, to)
case abi.Enum:
return slices.Equal(from.enums[a.Name], to.enums[b.Name])
}
return true
}
func classify(a, b abi.TypeRef, from, to *layout) Conversion {
switch {
case sameLayout(a, b, from, to):
if a.Kind == abi.ComponentRef {
return Reresolve
}
return Same
case a.Kind != b.Kind || a.Name != b.Name:
return Default
}
switch a.Kind {
case abi.Struct:
if from.structs[a.Name] != nil && to.structs[b.Name] != nil {
return Struct
}
case abi.Enum:
if from.enums[a.Name] != nil && to.enums[b.Name] != nil {
return EnumByName
}
case abi.Slice:
if a.Elem != nil && b.Elem != nil && classify(*a.Elem, *b.Elem, from, to) != Default {
return Slice
}
}
return Default
}
func findField(od *bytecode.ScriptDef, name string, former []string) *bytecode.FieldDef {
for _, n := range append([]string{name}, former...) {
for i := range od.Fields {
if od.Fields[i].Name == n {
return &od.Fields[i]
}
}
}
return nil
}
func Build(from, to Program) (*Plan, error) {
p := &Plan{From: from, To: to, from: newLayout(from), to: newLayout(to), byName: map[string]*TypeChange{}}
olds := map[string]*bytecode.ScriptDef{}
oldPkg := map[string]string{}
for pkg, m := range from.Modules {
for i := range m.Scripts {
d := &m.Scripts[i]
olds[d.FullName] = d
oldPkg[d.FullName] = pkg
}
}
matched := map[string]bool{}
for _, nm := range to.Ordered() {
pkg := nm.Package
for i := range nm.Scripts {
nd := &nm.Scripts[i]
od := olds[nd.FullName]
if od == nil {
for _, f := range nd.Former {
if od = olds[f]; od != nil {
break
}
}
}
tc := TypeChange{Name: nd.FullName, New: nd, Old: od, Changed: true}
if od != nil {
matched[od.FullName] = true
tc.Changed = od.FullName != nd.FullName || from.Hashes[oldPkg[od.FullName]] != to.Hashes[pkg]
if err := p.fields(&tc); err != nil {
return nil, err
}
}
p.Types = append(p.Types, tc)
}
}
for i := range p.Types {
p.byName[p.Types[i].Name] = &p.Types[i]
}
names := make([]string, 0, len(olds))
for n := range olds {
if !matched[n] {
names = append(names, n)
}
}
sort.Strings(names)
for _, n := range names {
p.warn("script %s was removed, its instances stop", n)
}
return p, nil
}
func (p *Plan) fields(tc *TypeChange) error {
od, nd := tc.Old, tc.New
used := map[string]bool{}
for _, nf := range nd.Fields {
of := findField(od, nf.Name, nf.Former)
if of == nil {
continue
}
used[of.Name] = true
ot, ok1 := abi.Parse(of.Type)
nt, ok2 := abi.Parse(nf.Type)
if !ok1 || !ok2 {
return fmt.Errorf("script %s field %s has a malformed type", nd.FullName, nf.Name)
}
conv := classify(ot, nt, p.from, p.to)
if conv == Default {
tc.Reset = append(tc.Reset, nf.Name)
p.warn("field %s.%s changed from %s to %s, reset to its default", nd.FullName, nf.Name, of.Type, nf.Type)
}
tc.Fields = append(tc.Fields, FieldMap{
Name: nf.Name,
OldSlot: int(of.Slot),
NewSlot: int(nf.Slot),
OldSlots: int(of.Slots),
NewSlots: int(nf.Slots),
Old: ot,
New: nt,
Conv: conv,
})
}
for _, of := range od.Fields {
if !used[of.Name] {
tc.Removed = append(tc.Removed, of.Name)
}
}
return nil
}