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

220 lines
4.8 KiB
Go

package workspace
import (
"maps"
"sort"
"strings"
"aego/script/compiler"
"aego/script/diag"
"aego/script/source"
)
type BuildOptions struct {
IncludeTests bool
}
func IsTest(path string) bool {
return strings.HasSuffix(path, "_test"+Ext)
}
func (w *Workspace) Build() (*Generation, []diag.Diagnostic) {
return w.BuildWith(BuildOptions{})
}
func (w *Workspace) BuildWith(opt BuildOptions) (*Generation, []diag.Diagnostic) {
if opt.IncludeTests {
return w.buildTests()
}
return w.commit()
}
func (w *Workspace) buildTests() (*Generation, []diag.Diagnostic) {
w.bmu.Lock()
defer w.bmu.Unlock()
w.mu.Lock()
in := w.inputs(false, true)
tab, module, debug := w.table(), w.cfg.Module, w.cfg.Debug
w.mu.Unlock()
a, _ := in.analyze(tab, module)
ds := append([]diag.Diagnostic(nil), a.Diagnostics...)
g := newGeneration()
g.Sources, g.ABIHash = in.sm, tab.Hash()
copt := compiler.Options{Sources: in.sm, Debug: debug}
for _, p := range a.Order {
mod, cds := compiler.Compile(a, p, copt)
if mod == nil {
g.Failed[p] = cds
continue
}
g.Modules[p] = mod
g.Hashes[p] = compiler.InterfaceHash(a, p)
g.Changed = append(g.Changed, p)
}
diag.Sort(ds)
return g, ds
}
func blocked(pkg, why string) []diag.Diagnostic {
return []diag.Diagnostic{diag.Errorf(diag.CompileBlocked, source.Span{}, "package %s %s", pkg, why)}
}
func (w *Workspace) commit() (*Generation, []diag.Diagnostic) {
w.bmu.Lock()
defer w.bmu.Unlock()
w.mu.Lock()
in := w.inputs(false, false)
tab, module, debug := w.table(), w.cfg.Module, w.cfg.Debug
prev := w.committed
w.mu.Unlock()
a, _ := in.analyze(tab, module)
ds := append([]diag.Diagnostic(nil), a.Diagnostics...)
abiHash := tab.Hash()
broken := map[string]bool{}
errsOf := map[string][]diag.Diagnostic{}
for _, d := range a.Diagnostics {
if d.Severity != diag.Error {
continue
}
if pkg, ok := in.pkgOf[d.Primary.File]; ok {
broken[pkg] = true
errsOf[pkg] = append(errsOf[pkg], d)
}
}
closure := map[string]map[string]bool{}
var deps func(string) map[string]bool
deps = func(p string) map[string]bool {
if c, ok := closure[p]; ok {
return c
}
c := map[string]bool{}
closure[p] = c
if cp := a.Packages[p]; cp != nil {
for _, d := range cp.Imports {
c[d] = true
for x := range deps(d) {
c[x] = true
}
}
}
return c
}
iface := make(map[string][32]byte, len(a.Order))
for _, p := range a.Order {
iface[p] = compiler.InterfaceHash(a, p)
}
opt := compiler.Options{Sources: in.sm, Debug: debug}
next := map[string]built{}
failed := map[string][]diag.Diagnostic{}
for _, p := range a.Order {
dh := map[string][32]byte{}
for d := range deps(p) {
dh[d] = iface[d]
}
key := compiler.CacheKey(in.srcs[p], dh, abiHash, opt)
if old, ok := w.state[p]; ok && old.key == key {
continue
}
if broken[p] {
failed[p] = errsOf[p]
continue
}
if dep := firstBroken(deps(p), broken); dep != "" {
failed[p] = blocked(p, "depends on broken package "+dep)
continue
}
mod, cds := compiler.Compile(a, p, opt)
var extra []diag.Diagnostic
for _, d := range cds {
if d.Code != diag.CompileBlocked {
extra = append(extra, d)
}
}
ds = append(ds, extra...)
if mod == nil {
if len(extra) == 0 {
extra = cds
}
failed[p] = extra
continue
}
next[p] = built{key: key, iface: iface[p], mod: mod}
}
changedIface := func(p string) bool {
old, ok := w.state[p]
return !ok || old.iface != iface[p]
}
for again := true; again; {
again = false
for _, p := range sortedKeys(next) {
for _, h := range sortedKeys(failed) {
if _, live := prev.Modules[h]; !live {
continue
}
if (deps(p)[h] && changedIface(h)) || (deps(h)[p] && changedIface(p)) {
delete(next, p)
failed[p] = blocked(p, "is held back until "+h+" compiles")
again = true
break
}
}
}
}
var removed []string
for p := range w.state {
if _, ok := a.Packages[p]; !ok {
removed = append(removed, p)
}
}
diag.Sort(ds)
w.mu.Lock()
defer w.mu.Unlock()
if len(next) == 0 && len(removed) == 0 {
g := *prev
g.Changed, g.Failed, g.Sources = nil, failed, in.sm
w.committed = &g
return &g, ds
}
g := &Generation{
Modules: maps.Clone(prev.Modules),
Hashes: maps.Clone(prev.Hashes),
ABIHash: abiHash,
Failed: failed,
Sources: in.sm,
}
for p, b := range next {
w.state[p] = b
g.Modules[p] = b.mod
g.Hashes[p] = b.key
g.Changed = append(g.Changed, p)
}
for _, p := range removed {
delete(w.state, p)
delete(g.Modules, p)
delete(g.Hashes, p)
g.Changed = append(g.Changed, p)
}
sort.Strings(g.Changed)
w.nextID++
g.ID = w.nextID
w.committed = g
return g, ds
}
func firstBroken(set map[string]bool, broken map[string]bool) string {
for _, d := range sortedKeys(set) {
if broken[d] {
return d
}
}
return ""
}