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

225 lines
5.1 KiB
Go

package script
import (
"path"
"sort"
"strconv"
"strings"
"sync"
"aego/asset"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/log"
"aego/script/bytecode"
"aego/script/diag"
"aego/script/source"
"aego/script/workspace"
)
const (
ImporterID = "script"
Group = "scripts"
Root = "scripts"
)
type CompileError struct {
Package string
Summary string
Text string
Diagnostics []diag.Diagnostic
}
func (e *CompileError) Error() string {
return e.Summary
}
type Problem struct {
Package string
Path string
Line int
Col int
Code string
Error bool
Message string
Text string
}
type page struct {
hash [32]byte
data []byte
}
type Importer struct {
ws *workspace.Workspace
pages map[string]page
mu sync.Mutex
problems []Problem
rev uint64
}
func New(ws *workspace.Workspace) *Importer {
return &Importer{ws: ws, pages: map[string]page{}}
}
func (*Importer) ID() string { return ImporterID }
func (*Importer) Version() int { return 1 }
func (*Importer) GroupVersion() int { return 2 }
func (*Importer) Extensions() []string { return []string{".aes"} }
func (*Importer) Defaults() pipeline.Settings { return pipeline.Settings{} }
func (*Importer) Import(ctx *pipeline.ImportContext) (*pipeline.Result, error) {
return &pipeline.Result{Kind: asset.ArtScript, Group: Group, Source: ctx.Source}, nil
}
func (i *Importer) Workspace() *workspace.Workspace {
return i.ws
}
func (i *Importer) Module() string {
if i.ws == nil {
return "game"
}
return i.ws.Module()
}
func PackageOf(module, file string) string {
rel := strings.TrimPrefix(strings.TrimPrefix(path.Dir(file), Root), "/")
if rel == "" || rel == "." {
return module
}
return module + "/" + rel
}
func (i *Importer) Problems() ([]Problem, uint64) {
i.mu.Lock()
defer i.mu.Unlock()
return i.problems, i.rev
}
func (i *Importer) Report() string {
ps, _ := i.Problems()
var b strings.Builder
for _, p := range ps {
if p.Error {
b.WriteString(p.Text)
}
}
return strings.TrimRight(b.String(), "\n")
}
func (i *Importer) publish(ps []Problem) {
i.mu.Lock()
i.problems = ps
i.rev++
i.mu.Unlock()
}
func problemOf(sm *source.Manager, pkg string, d diag.Diagnostic) Problem {
line, col := sm.LineCol(d.Primary.File, d.Primary.Start)
return Problem{
Package: pkg,
Path: sm.Path(d.Primary.File),
Line: line,
Col: col,
Code: d.Code,
Error: d.Severity == diag.Error,
Message: d.Message,
Text: diag.Format(sm, d),
}
}
func (i *Importer) encode(pkg string, g *workspace.Generation) []byte {
h := g.Hashes[pkg]
if p, ok := i.pages[pkg]; ok && p.hash == h {
return p.data
}
data := bytecode.Encode(g.Modules[pkg])
i.pages[pkg] = page{hash: h, data: data}
return data
}
func (i *Importer) Finalize(ctx *pipeline.GroupContext, members []pipeline.GroupMember) (map[ids.AssetID]*pipeline.Result, []pipeline.Produced, error) {
results := make(map[ids.AssetID]*pipeline.Result, len(members))
files := make(map[string][]byte, len(members))
owners := map[string][]ids.AssetID{}
for _, m := range members {
results[m.ID] = &pipeline.Result{Kind: asset.ArtScript}
if m.Result == nil {
continue
}
files[m.Path] = m.Result.Source
if i.ws == nil || strings.HasSuffix(m.Path, "_test.aes") {
continue
}
if pkg, ok := i.ws.PackageOf(m.Path); ok {
owners[pkg] = append(owners[pkg], m.ID)
}
}
if i.ws == nil {
i.publish(nil)
return results, nil, nil
}
i.ws.Sync(files)
gen, ds := i.ws.Build()
sm := gen.Sources
var problems []Problem
for _, d := range ds {
if d.Severity == diag.Info {
continue
}
pkg, _ := i.ws.PackageOf(sm.Path(d.Primary.File))
problems = append(problems, problemOf(sm, pkg, d))
}
failed := make([]string, 0, len(gen.Failed))
for pkg := range gen.Failed {
failed = append(failed, pkg)
}
sort.Strings(failed)
for _, pkg := range failed {
fds := gen.Failed[pkg]
for _, d := range fds {
if !d.Primary.Valid() {
problems = append(problems, problemOf(sm, pkg, d))
}
}
err := failure(sm, pkg, fds)
for _, id := range owners[pkg] {
results[id].Error = err
}
}
var pages []pipeline.Produced
for pkg := range gen.Modules {
pages = append(pages, pipeline.Produced{Kind: asset.ArtScript, Name: pkg, Data: i.encode(pkg, gen)})
}
for _, pkg := range failed {
if _, ok := gen.Modules[pkg]; ok || ctx.Previous == nil {
continue
}
if data, ok := ctx.Previous(pkg); ok {
pages = append(pages, pipeline.Produced{Kind: asset.ArtScript, Name: pkg, Data: data})
ctx.Log.Warn("script package has errors, keeping the last good build", log.F("package", pkg))
}
}
sort.Slice(pages, func(a, b int) bool { return pages[a].Name < pages[b].Name })
i.publish(problems)
return results, pages, nil
}
func failure(sm *source.Manager, pkg string, ds []diag.Diagnostic) *CompileError {
var b strings.Builder
n := 0
for _, d := range ds {
if d.Severity == diag.Error {
b.WriteString(diag.Format(sm, d))
n++
}
}
return &CompileError{
Package: pkg,
Summary: "package " + pkg + " has " + strconv.Itoa(n) + " error(s)",
Text: b.String(),
Diagnostics: ds,
}
}