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.
334 lines
9.6 KiB
Go
334 lines
9.6 KiB
Go
package checker
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"aego/script/abi"
|
|
"aego/script/ast"
|
|
"aego/script/diag"
|
|
"aego/script/resolver"
|
|
"aego/script/types"
|
|
)
|
|
|
|
var yieldNames = [...]string{"", "wait", "frames", "fixed", "await"}
|
|
|
|
func scriptFullName(pkg, name string) string {
|
|
return strings.ReplaceAll(pkg, "/", ".") + "." + name
|
|
}
|
|
|
|
func (c *checker) visible(st *ScriptType, name string) bool {
|
|
return st.Pkg == c.rp.Path || abi.Exported(name)
|
|
}
|
|
|
|
func funcParams(f *Func) []*types.Type {
|
|
out := make([]*types.Type, len(f.Params))
|
|
for i, p := range f.Params {
|
|
out[i] = p.Type
|
|
}
|
|
return out
|
|
}
|
|
|
|
func typesText(ts []*types.Type) string {
|
|
parts := make([]string, len(ts))
|
|
for i, t := range ts {
|
|
parts[i] = t.String()
|
|
}
|
|
return "(" + strings.Join(parts, ", ") + ")"
|
|
}
|
|
|
|
func sameParams(f *Func, sg *ScriptSignal) bool {
|
|
if len(f.Params) != len(sg.Params) {
|
|
return false
|
|
}
|
|
for i, p := range f.Params {
|
|
if p.Type != sg.Params[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *checker) start(e *ast.StartExpr) operand {
|
|
obj := c.lookup(e.Name.Name)
|
|
if obj == nil {
|
|
c.errorf(diag.UndefinedName, e.Name.Sp, "undefined: %s", e.Name.Name)
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
c.use(e.Name, obj)
|
|
f, _ := obj.Data.(*Func)
|
|
if obj.Kind != resolver.ObjFunc || f == nil || !f.Decl.Coroutine || f.Script == nil || c.fn == nil || f.Script != c.fn.Script {
|
|
c.errorf(diag.StartTarget, e.Name.Sp, "start needs a coroutine method of this script, %s is not one", e.Name.Name)
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
c.argList(e.Sp, e.Args, f.Name, funcParams(f))
|
|
c.a.Starts[e] = f
|
|
return val(types.Coroutine)
|
|
}
|
|
|
|
func (c *checker) callSignal(e *ast.CallExpr, sel *ast.SelectorExpr, x operand) operand {
|
|
sg := x.sig
|
|
c.noTypeArgs(e)
|
|
switch sel.Sel.Name {
|
|
case "Emit":
|
|
if x.src != nil {
|
|
c.errorf(diag.BadConnect, sel.Sel.Sp, "signal %s can be emitted only by its own script", sg.Name)
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
c.argList(e.Sp, e.Args, sg.Name+".Emit", sg.Params)
|
|
c.a.Calls[e] = &Call{Kind: CallEmit, Signal: sg}
|
|
return operand{mode: mVoid, t: types.Void}
|
|
case "Connect":
|
|
if len(e.Args) != 1 {
|
|
c.errorf(diag.ArgCount, e.Sp, "Connect takes exactly one handler")
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
h, recv, ok := c.handler(e.Args[0], sg)
|
|
if !ok {
|
|
return invalid
|
|
}
|
|
c.a.Calls[e] = &Call{Kind: CallConnect, Signal: sg, Recv: x.src, Handler: h, HandlerRecv: recv}
|
|
return val(types.Connection)
|
|
}
|
|
c.errorf(diag.UnknownField, sel.Sel.Sp, "signal %s has no method %s, use Emit or Connect", sg.Name, sel.Sel.Name)
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
|
|
func (c *checker) handler(arg ast.Expr, sg *ScriptSignal) (*Func, ast.Expr, bool) {
|
|
var f *Func
|
|
var recv ast.Expr
|
|
switch a := unparen(arg).(type) {
|
|
case *ast.Ident:
|
|
if o := c.lookup(a.Name); o != nil && o.Kind == resolver.ObjFunc {
|
|
if fn, ok := o.Data.(*Func); ok && fn.Script != nil && c.fn != nil && fn.Script == c.fn.Script {
|
|
c.use(a, o)
|
|
f = fn
|
|
}
|
|
}
|
|
case *ast.SelectorExpr:
|
|
x := c.expr(a.X)
|
|
if x.mode == mValue && x.t.Kind == types.KScriptRef {
|
|
if st := c.a.scripts[x.t.Script]; st != nil && c.visible(st, a.Sel.Name) {
|
|
if fn := st.Methods[a.Sel.Name]; fn != nil {
|
|
if o := st.scope.LookupLocal(fn.Name); o != nil {
|
|
c.use(a.Sel, o)
|
|
}
|
|
f, recv = fn, a.X
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if f == nil {
|
|
c.errorf(diag.BadConnect, arg.Span(), "Connect needs a method name of this script or ref.Method")
|
|
return nil, nil, false
|
|
}
|
|
if f.Decl.Coroutine || f.Script.IsLifecycle(f) {
|
|
c.errorf(diag.BadConnect, arg.Span(), "%s cannot handle a signal, use a plain method", f.Name)
|
|
return nil, nil, false
|
|
}
|
|
if !sameParams(f, sg) || f.Result != types.Void {
|
|
c.a.Diagnostics = append(c.a.Diagnostics,
|
|
diag.Errorf(diag.HandlerMismatch, arg.Span(), "handler %s does not match signal %s", f.Name, sg.Name).
|
|
WithNote(sg.Decl.Name.Sp, "signal %s%s, handler %s%s", sg.Name, typesText(sg.Params), f.Name, typesText(funcParams(f))))
|
|
return nil, nil, false
|
|
}
|
|
return f, recv, true
|
|
}
|
|
|
|
func (c *checker) callScriptMethod(e *ast.CallExpr, sel *ast.SelectorExpr, recv operand) operand {
|
|
name := sel.Sel.Name
|
|
st := c.a.scripts[recv.t.Script]
|
|
var f *Func
|
|
if st != nil {
|
|
f = st.Methods[name]
|
|
}
|
|
switch {
|
|
case f == nil:
|
|
c.errorf(diag.UnknownField, sel.Sel.Sp, "%s has no method %s", recv.t, name)
|
|
case !c.visible(st, name):
|
|
c.errorf(diag.NotExported, sel.Sel.Sp, "%s.%s is not exported", st.Name, name)
|
|
case st.IsLifecycle(f) || f.Decl.Coroutine:
|
|
c.errorf(diag.ScriptRefCall, sel.Sel.Sp, "%s.%s cannot be called through a ScriptRef, lifecycle methods and coroutines run only on their own instance", st.Name, name)
|
|
default:
|
|
if o := st.scope.LookupLocal(name); o != nil {
|
|
c.use(sel.Sel, o)
|
|
}
|
|
c.noTypeArgs(e)
|
|
c.arguments(e, name, funcParams(f))
|
|
c.a.Calls[e] = &Call{Kind: CallScriptMethod, Func: f, Recv: sel.X}
|
|
return result(f.Result)
|
|
}
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
|
|
func (c *checker) callHandle(e *ast.CallExpr, sel *ast.SelectorExpr, recv operand) operand {
|
|
c.noTypeArgs(e)
|
|
name := sel.Sel.Name
|
|
kind, res := CallCoStop, types.Void
|
|
switch {
|
|
case recv.t.Kind == types.KCoroutine && name == "Stop":
|
|
case recv.t.Kind == types.KCoroutine && name == "Running":
|
|
kind, res = CallCoRunning, types.Bool
|
|
case recv.t.Kind == types.KConnection && name == "Disconnect":
|
|
kind = CallDisconnect
|
|
case recv.t.Kind == types.KConnection && name == "Connected":
|
|
kind, res = CallConnected, types.Bool
|
|
default:
|
|
c.errorf(diag.UnknownField, sel.Sel.Sp, "%s has no method %s", recv.t, name)
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
if len(e.Args) != 0 {
|
|
c.errorf(diag.ArgCount, e.Sp, "%s takes no arguments", name)
|
|
c.args(e.Args)
|
|
}
|
|
c.a.Calls[e] = &Call{Kind: kind, Recv: sel.X}
|
|
return result(res)
|
|
}
|
|
|
|
func (c *checker) getScript(e *ast.CallExpr, sel *ast.SelectorExpr) operand {
|
|
if len(e.TypeArgs) != 1 {
|
|
c.errorf(diag.BadTypeArg, e.Sp, "GetScript needs one script type argument, like GetScript<Enemy>()")
|
|
c.args(e.Args)
|
|
return invalid
|
|
}
|
|
if len(e.Args) != 0 {
|
|
c.errorf(diag.ArgCount, e.Sp, "GetScript takes no arguments")
|
|
c.args(e.Args)
|
|
}
|
|
nt, ok := e.TypeArgs[0].(*ast.NamedType)
|
|
if !ok {
|
|
c.errorf(diag.ScriptRefTarget, e.TypeArgs[0].Span(), "GetScript argument must be a script type")
|
|
return invalid
|
|
}
|
|
full, ok := c.scriptTarget(nt)
|
|
if !ok {
|
|
return invalid
|
|
}
|
|
c.a.Calls[e] = &Call{Kind: CallGetScript, Target: full, Recv: sel.X}
|
|
return val(c.u.ScriptRef(full))
|
|
}
|
|
|
|
func (c *checker) scriptMember(s *ast.SelectorExpr, st *ScriptType) operand {
|
|
name := s.Sel.Name
|
|
o := st.scope.LookupLocal(name)
|
|
if o == nil || o.Kind == resolver.ObjNode {
|
|
c.errorf(diag.UnknownField, s.Sel.Sp, "%s has no field, signal or method %s", st.Name, name)
|
|
return invalid
|
|
}
|
|
if !c.visible(st, name) {
|
|
c.errorf(diag.NotExported, s.Sel.Sp, "%s.%s is not exported", st.Name, name)
|
|
return invalid
|
|
}
|
|
c.use(s.Sel, o)
|
|
switch o.Kind {
|
|
case resolver.ObjField:
|
|
f := o.Data.(*ScriptField)
|
|
c.a.Selections[s] = &Selection{Kind: SelScriptField, Script: st, ScriptField: f}
|
|
if f.Type == types.Invalid {
|
|
return invalid
|
|
}
|
|
return operand{mode: mValue, t: f.Type, addr: true}
|
|
case resolver.ObjSignal:
|
|
sg := o.Data.(*ScriptSignal)
|
|
c.a.Selections[s] = &Selection{Kind: SelSignal, Script: st, Signal: sg}
|
|
return operand{mode: mSignal, t: types.Void, sig: sg, src: s.X}
|
|
}
|
|
c.errorf(diag.NotValue, s.Sp, "method %s must be called", name)
|
|
return invalid
|
|
}
|
|
|
|
func (c *checker) scriptTarget(arg *ast.NamedType) (string, bool) {
|
|
if len(arg.Args) > 0 {
|
|
c.errorf(diag.ScriptRefTarget, arg.Args[0].Span(), "%s is not generic", arg.Name.Name)
|
|
return "", false
|
|
}
|
|
var obj *resolver.Object
|
|
if arg.Pkg != nil {
|
|
p := c.lookup(arg.Pkg.Name)
|
|
if p == nil || p.Kind != resolver.ObjPkg || p.Host || p.Target == "" {
|
|
c.errorf(diag.ScriptRefTarget, arg.Sp, "%s.%s is not a script type", arg.Pkg.Name, arg.Name.Name)
|
|
return "", false
|
|
}
|
|
c.use(arg.Pkg, p)
|
|
if obj = c.member(arg.Name, p); obj == nil {
|
|
return "", false
|
|
}
|
|
} else {
|
|
if obj = c.lookup(arg.Name.Name); obj == nil {
|
|
if obj = c.uniqueScript(arg); obj == nil {
|
|
return "", false
|
|
}
|
|
}
|
|
c.use(arg.Name, obj)
|
|
}
|
|
if obj.Kind != resolver.ObjScript {
|
|
c.errorf(diag.ScriptRefTarget, arg.Sp, "%s is not a script type", arg.Name.Name)
|
|
return "", false
|
|
}
|
|
return scriptFullName(obj.Pkg, obj.Name), true
|
|
}
|
|
|
|
func (c *checker) uniqueScript(arg *ast.NamedType) *resolver.Object {
|
|
name := arg.Name.Name
|
|
var found []*resolver.Object
|
|
for _, p := range append([]string{c.rp.Path}, c.rp.Deps...) {
|
|
rp := c.tab.Packages[p]
|
|
if rp == nil {
|
|
continue
|
|
}
|
|
o := rp.Scope.LookupLocal(name)
|
|
if o == nil || o.Kind != resolver.ObjScript || (p != c.rp.Path && !o.Exported()) {
|
|
continue
|
|
}
|
|
found = append(found, o)
|
|
}
|
|
switch len(found) {
|
|
case 1:
|
|
return found[0]
|
|
case 0:
|
|
c.errorf(diag.ScriptRefTarget, arg.Sp, "unknown script type %s, import the package that declares it", name)
|
|
default:
|
|
names := make([]string, len(found))
|
|
for i, o := range found {
|
|
names[i] = path(o.Pkg) + "." + o.Name
|
|
}
|
|
c.errorf(diag.ScriptRefTarget, arg.Sp, "script name %s is ambiguous, write one of: %s", name, strings.Join(names, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func path(pkg string) string {
|
|
return pkg[strings.LastIndexByte(pkg, '/')+1:]
|
|
}
|
|
|
|
func (c *checker) yieldStmt(s *ast.YieldStmt) {
|
|
if !c.fn.Decl.Coroutine {
|
|
c.errorf(diag.YieldOutside, s.Sp, "yield is only allowed in a coroutine, declare it as func %s(...) coroutine", c.fn.Name)
|
|
}
|
|
var want *types.Type
|
|
switch s.Kind {
|
|
case ast.YieldWait:
|
|
want = types.Float
|
|
case ast.YieldFrames, ast.YieldFixed:
|
|
want = types.Int
|
|
case ast.YieldAwait:
|
|
want = types.Coroutine
|
|
default:
|
|
return
|
|
}
|
|
x := c.value(s.Value)
|
|
if x.mode == mInvalid {
|
|
return
|
|
}
|
|
y := x
|
|
if !c.coerce(s.Value, &y, want) {
|
|
c.errorf(diag.YieldArg, s.Value.Span(), "yield %s needs %s, not %s", yieldNames[s.Kind], want, x.t)
|
|
}
|
|
}
|