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

328 lines
6.6 KiB
Go

package compiler
import (
"fmt"
"aego/script/asm"
"aego/script/ast"
"aego/script/bytecode"
"aego/script/checker"
"aego/script/diag"
"aego/script/resolver"
"aego/script/source"
"aego/script/token"
"aego/script/types"
)
type target struct {
brk asm.Label
cont asm.Label
loop bool
}
type fn struct {
u *unit
f *checker.Func
b *asm.Builder
regs map[*resolver.Object]int
top int
targets []target
}
func (u *unit) function(f *checker.Func, idx int) {
c := &fn{u: u, f: f, b: asm.NewBuilder(), regs: map[*resolver.Object]int{}}
for _, p := range f.Params {
c.regs[p] = c.alloc(p.Type.Slots())
}
params := c.top
c.line(f.Decl.Sp)
if f.Decl.Body != nil {
c.stmts(f.Decl.Body.Stmts)
end := f.Decl.Body.Sp
if end.End > end.Start {
end.Start = end.End - 1
}
c.line(end)
}
if f.Result == types.Void {
c.b.ABC(bytecode.RET, 0, 0, 0)
} else {
c.b.ABx(bytecode.FAULT, 0, c.b.Const(bytecode.StringK("missing return in "+f.FullName())))
}
code, consts, lines, err := c.b.Finish()
regs := max(c.b.MaxReg(), 1)
if err == nil && regs > bytecode.MaxRegisters {
err = fmt.Errorf("it needs %d registers, the limit is %d", regs, bytecode.MaxRegisters)
}
if err != nil {
u.diags = append(u.diags, diag.Errorf(diag.RegisterLimit, f.Decl.Name.Sp, "%s cannot be compiled: %v", f.FullName(), err))
return
}
script := int32(bytecode.NoScript)
if f.Script != nil {
script = int32(u.scripts[f.Script])
}
file := ""
if u.opt.Sources != nil {
file = u.opt.Sources.Path(f.Decl.Sp.File)
}
u.mod.Functions[idx] = bytecode.Function{
Name: funcName(f), File: file, Params: uint8(params), Results: uint8(f.Result.Slots()),
Registers: uint8(regs), Script: script, Coroutine: f.Decl.Coroutine,
Code: code, Consts: consts, Lines: lines,
}
}
func (c *fn) alloc(n int) int {
r := c.top
c.top += n
c.b.Use(c.top)
return r
}
func (c *fn) line(sp source.Span) {
if c.u.opt.Sources == nil || !sp.Valid() {
return
}
l, _ := c.u.opt.Sources.LineCol(sp.File, sp.Start)
c.b.SetLine(l)
}
func (c *fn) stmts(list []ast.Stmt) {
for _, s := range list {
c.stmt(s)
}
}
func (c *fn) block(list []ast.Stmt) {
mark := c.top
c.stmts(list)
c.top = mark
}
func (c *fn) declare(id *ast.Ident, value ast.Expr, t *types.Type) {
n := t.Slots()
r := c.alloc(n)
if obj := c.u.a.Defs[id]; obj != nil {
c.regs[obj] = r
}
if value != nil {
c.exprTo(value, r)
} else {
c.zero(r, n)
}
c.top = r + n
}
func (c *fn) stmt(s ast.Stmt) {
c.line(s.Span())
mark := c.top
switch s := s.(type) {
case *ast.VarStmt:
c.declare(s.Name, s.Value, c.declType(s.Name, s.Value))
return
case *ast.ShortVarStmt:
c.declare(s.Name, s.Value, c.declType(s.Name, s.Value))
return
case *ast.AssignStmt:
c.assign(s)
case *ast.IncDecStmt:
c.incDec(s)
case *ast.ExprStmt:
switch x := unparen(s.X).(type) {
case *ast.CallExpr:
c.callTo(x, 0, false)
case *ast.StartExpr:
c.startTo(x, c.alloc(1))
}
case *ast.YieldStmt:
c.yield(s)
case *ast.ReturnStmt:
if s.Value == nil {
c.b.ABC(bytecode.RET, 0, 0, 0)
} else {
r := c.operand(s.Value)
c.b.ABC(bytecode.RET, r, c.f.Result.Slots(), 0)
}
case *ast.BranchStmt:
c.branch(s)
case *ast.BlockStmt:
c.block(s.Stmts)
case *ast.IfStmt:
c.ifStmt(s)
case *ast.ForStmt:
c.forStmt(s)
case *ast.SwitchStmt:
c.switchStmt(s)
}
c.top = mark
}
func (c *fn) declType(id *ast.Ident, value ast.Expr) *types.Type {
if obj := c.u.a.Defs[id]; obj != nil && obj.Type != nil {
return obj.Type
}
if value != nil {
return c.typ(value)
}
return types.Invalid
}
func logical(e ast.Expr) bool {
b, ok := unparen(e).(*ast.BinaryExpr)
return ok && (b.Op == token.AndAnd || b.Op == token.OrOr)
}
func (c *fn) assign(s *ast.AssignStmt) {
t := c.typ(s.Target)
n := t.Slots()
p, ok := c.placeOf(s.Target)
if !ok {
return
}
if s.Op == token.Assign {
if p.kind == pReg && n == 1 && !logical(s.Value) {
c.exprTo(s.Value, p.reg)
return
}
v := c.alloc(n)
c.exprTo(s.Value, v)
c.store(p, v)
return
}
op := s.Op.AssignOp()
if p.kind == pReg {
y := c.operand(s.Value)
c.arith(p.reg, op, p.reg, y, t, c.typ(s.Value))
return
}
cur := c.alloc(n)
c.load(p, cur)
y := c.operand(s.Value)
c.arith(cur, op, cur, y, t, c.typ(s.Value))
c.store(p, cur)
}
func (c *fn) incDec(s *ast.IncDecStmt) {
t := c.typ(s.X)
p, ok := c.placeOf(s.X)
if !ok {
return
}
one := c.alloc(1)
if t.Kind == types.KFloat {
c.b.ABx(bytecode.LOADK, one, c.b.Const(bytecode.FloatK(1)))
} else {
c.b.AsBx(bytecode.LOADI, one, 1)
}
op := token.Add
if !s.Inc {
op = token.Sub
}
if p.kind == pReg {
c.arith(p.reg, op, p.reg, one, t, t)
return
}
cur := c.alloc(1)
c.load(p, cur)
c.arith(cur, op, cur, one, t, t)
c.store(p, cur)
}
func (c *fn) branch(s *ast.BranchStmt) {
for i := len(c.targets) - 1; i >= 0; i-- {
t := c.targets[i]
if s.Tok == token.Break {
c.b.Jump(bytecode.JMP, 0, t.brk)
return
}
if t.loop {
c.b.Jump(bytecode.JMP, 0, t.cont)
return
}
}
}
func (c *fn) cond(e ast.Expr, onFalse asm.Label) {
mark := c.top
r := c.operand(e)
c.b.Jump(bytecode.JMPNOT, r, onFalse)
c.top = mark
}
func (c *fn) ifStmt(s *ast.IfStmt) {
elseL, end := c.b.NewLabel(), c.b.NewLabel()
c.cond(s.Cond, elseL)
if s.Then != nil {
c.block(s.Then.Stmts)
}
if s.Else != nil {
c.b.Jump(bytecode.JMP, 0, end)
}
c.b.Mark(elseL)
if s.Else != nil {
c.stmt(s.Else)
c.b.Mark(end)
}
}
func (c *fn) forStmt(s *ast.ForStmt) {
mark := c.top
if s.Init != nil {
c.stmt(s.Init)
}
head, cont, exit := c.b.NewLabel(), c.b.NewLabel(), c.b.NewLabel()
c.b.Mark(head)
if s.Cond != nil {
c.cond(s.Cond, exit)
}
c.targets = append(c.targets, target{brk: exit, cont: cont, loop: true})
if s.Body != nil {
c.block(s.Body.Stmts)
}
c.targets = c.targets[:len(c.targets)-1]
c.b.Mark(cont)
if s.Post != nil {
c.stmt(s.Post)
}
c.b.Jump(bytecode.JMP, 0, head)
c.b.Mark(exit)
c.top = mark
}
func (c *fn) switchStmt(s *ast.SwitchStmt) {
t := c.typ(s.Tag)
tag := c.alloc(t.Slots())
c.exprTo(s.Tag, tag)
exit := c.b.NewLabel()
bodies := make([]asm.Label, len(s.Cases))
def := -1
for i, cl := range s.Cases {
bodies[i] = c.b.NewLabel()
if cl.Default {
def = i
}
for _, v := range cl.Values {
mark := c.top
vr := c.operand(v)
eq := c.alloc(1)
c.equal(eq, tag, vr, t)
c.b.Jump(bytecode.JMPIF, eq, bodies[i])
c.top = mark
}
}
if def >= 0 {
c.b.Jump(bytecode.JMP, 0, bodies[def])
} else {
c.b.Jump(bytecode.JMP, 0, exit)
}
c.targets = append(c.targets, target{brk: exit})
for i, cl := range s.Cases {
c.b.Mark(bodies[i])
c.block(cl.Body)
c.b.Jump(bytecode.JMP, 0, exit)
}
c.targets = c.targets[:len(c.targets)-1]
c.b.Mark(exit)
}