Files
aego/script/checker/stmt.go
T
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

311 lines
7.2 KiB
Go

package checker
import (
"strings"
"aego/script/ast"
"aego/script/diag"
"aego/script/resolver"
"aego/script/token"
"aego/script/types"
)
func (c *checker) stmts(list []ast.Stmt) {
for _, s := range list {
c.stmt(s)
}
}
func (c *checker) block(list []ast.Stmt) {
c.scope = resolver.NewScope(c.scope)
c.stmts(list)
c.scope = c.scope.Parent
}
func (c *checker) stmt(s ast.Stmt) {
switch s := s.(type) {
case *ast.VarStmt:
t := c.typeExpr(s.Type)
if s.Value != nil {
c.assign(s.Value, c.value(s.Value), t, "variable declaration")
}
c.declare(s.Name, t)
case *ast.ShortVarStmt:
c.shortVar(s)
case *ast.AssignStmt:
c.assignStmt(s)
case *ast.IncDecStmt:
x := c.expr(s.X)
switch {
case x.mode == mInvalid:
case x.mode != mValue || !x.addr:
c.errorf(diag.NotAssignable, s.X.Span(), "cannot assign to this expression")
case x.t != types.Int && x.t != types.Float:
c.errorf(diag.InvalidOperation, s.Sp, "increment and decrement are not defined on %s", x.t)
}
case *ast.ExprStmt:
c.exprStmt(s)
case *ast.YieldStmt:
c.yieldStmt(s)
case *ast.ReturnStmt:
c.returnStmt(s)
case *ast.BranchStmt:
if s.Tok == token.Break && c.breaks == 0 {
c.errorf(diag.MisplacedBranch, s.Sp, "break is not in a loop or switch")
}
if s.Tok == token.Continue && c.loops == 0 {
c.errorf(diag.MisplacedBranch, s.Sp, "continue is not in a loop")
}
case *ast.BlockStmt:
c.block(s.Stmts)
case *ast.IfStmt:
c.cond(s.Cond)
if s.Then != nil {
c.block(s.Then.Stmts)
}
if s.Else != nil {
c.stmt(s.Else)
}
case *ast.ForStmt:
c.forStmt(s)
case *ast.SwitchStmt:
c.switchStmt(s)
}
}
func (c *checker) declare(id *ast.Ident, t *types.Type) {
if id.Name == "_" {
return
}
obj := &resolver.Object{Kind: resolver.ObjVar, Name: id.Name, Pkg: c.rp.Path, Ident: id, Decl: id, Type: t, Index: len(c.fn.Locals)}
if prev := c.scope.Insert(obj); prev != nil {
c.a.Diagnostics = append(c.a.Diagnostics, diag.Errorf(diag.Redeclared, id.Sp, "%s redeclared in this block", id.Name).
WithNote(prev.Span(), "previous declaration"))
}
c.fn.Locals = append(c.fn.Locals, obj)
c.def(id, obj)
}
func (c *checker) shortVar(s *ast.ShortVarStmt) {
x := c.value(s.Value)
t := types.Invalid
switch {
case x.mode == mInvalid:
case x.t == types.Nil:
c.errorf(diag.NilMisuse, s.Value.Span(), "use of untyped nil in a variable declaration")
default:
t = types.Default(x.t)
c.assign(s.Value, x, t, "variable declaration")
}
c.declare(s.Name, t)
}
func (c *checker) assignStmt(s *ast.AssignStmt) {
lhs := c.expr(s.Target)
rhs := c.value(s.Value)
if lhs.mode == mInvalid {
return
}
if lhs.mode != mValue || !lhs.addr {
c.errorf(diag.NotAssignable, s.Target.Span(), "cannot assign to this expression")
return
}
if s.Op == token.Assign {
c.assign(s.Value, rhs, lhs.t, "assignment")
return
}
if rhs.mode == mInvalid {
return
}
r := c.binop(s.Sp, s.Op.AssignOp(), lhs, rhs, s.Target, s.Value)
if r.mode != mInvalid && r.t != lhs.t {
c.errorf(diag.TypeMismatch, s.Sp, "cannot assign %s to %s", r.t, lhs.t)
}
}
func unparen(e ast.Expr) ast.Expr {
for {
p, ok := e.(*ast.ParenExpr)
if !ok {
return e
}
e = p.X
}
}
func (c *checker) exprStmt(s *ast.ExprStmt) {
switch x := unparen(s.X).(type) {
case *ast.StartExpr:
c.expr(x)
return
case *ast.CallExpr:
c.expr(x)
if k := c.a.Calls[x]; k != nil {
switch k.Kind {
case CallLen, CallAppend, CallConvert, CallGetComponent, CallHasComponent,
CallCoRunning, CallConnected, CallGetScript:
c.errorf(diag.Unused, s.X.Span(), "result of this call is not used")
}
}
return
}
if x := c.expr(s.X); x.mode != mInvalid {
c.errorf(diag.Unused, s.X.Span(), "value is not used")
}
}
func (c *checker) returnStmt(s *ast.ReturnStmt) {
res := c.fn.Result
switch {
case s.Value == nil && res != types.Void:
c.errorf(diag.ReturnMismatch, s.Sp, "missing return value of type %s", res)
case s.Value != nil && res == types.Void:
c.value(s.Value)
c.errorf(diag.ReturnMismatch, s.Value.Span(), "%s returns no value", c.fn.Name)
case s.Value != nil:
c.assign(s.Value, c.value(s.Value), res, "return")
}
}
func (c *checker) cond(e ast.Expr) {
x := c.value(e)
if x.mode != mInvalid && x.t != types.Bool {
c.errorf(diag.TypeMismatch, e.Span(), "condition must be bool, not %s", x.t)
}
}
func (c *checker) forStmt(s *ast.ForStmt) {
c.scope = resolver.NewScope(c.scope)
if s.Init != nil {
c.stmt(s.Init)
}
if s.Cond != nil {
c.cond(s.Cond)
}
if s.Post != nil {
if _, ok := s.Post.(*ast.ShortVarStmt); ok {
c.errorf(diag.InvalidOperation, s.Post.Span(), "cannot declare in the post statement of a for loop")
}
c.stmt(s.Post)
}
c.loops++
c.breaks++
if s.Body != nil {
c.block(s.Body.Stmts)
}
c.loops--
c.breaks--
c.scope = c.scope.Parent
}
func (c *checker) switchStmt(s *ast.SwitchStmt) {
tag := c.value(s.Tag)
t := types.Invalid
switch {
case tag.mode == mInvalid:
case tag.t == types.Nil:
c.errorf(diag.NilMisuse, s.Tag.Span(), "cannot switch on nil")
default:
t = types.Default(tag.t)
c.assign(s.Tag, tag, t, "switch")
if !t.Comparable() {
c.errorf(diag.InvalidOperation, s.Tag.Span(), "cannot switch on %s", t)
t = types.Invalid
}
}
seen := map[string]bool{}
covered := map[int64]bool{}
hasDefault := false
c.breaks++
for _, cl := range s.Cases {
if cl.Default {
if hasDefault {
c.errorf(diag.DuplicateCase, cl.Sp, "multiple defaults in switch")
}
hasDefault = true
}
for _, v := range cl.Values {
x := c.value(v)
if t == types.Invalid || !c.assign(v, x, t, "case") {
continue
}
k := c.a.Types[v].Const
if k == nil {
continue
}
if seen[k.Key()] {
c.errorf(diag.DuplicateCase, v.Span(), "duplicate case %s", k)
}
seen[k.Key()] = true
if k.Kind == types.CInt {
covered[k.Int] = true
}
}
c.block(cl.Body)
}
c.breaks--
if t.Kind == types.KEnum && !hasDefault {
var missing []string
for i, m := range t.Members {
if !covered[t.Values[i]] {
missing = append(missing, m)
}
}
if len(missing) > 0 {
c.errorf(diag.NotExhaustive, s.Tag.Span(), "switch on %s is not exhaustive, missing %s; add the cases or a default", t, strings.Join(missing, ", "))
}
}
}
func terminating(list []ast.Stmt) bool {
return len(list) > 0 && isTerminating(list[len(list)-1])
}
func isTerminating(s ast.Stmt) bool {
switch s := s.(type) {
case *ast.ReturnStmt:
return true
case *ast.BlockStmt:
return terminating(s.Stmts)
case *ast.IfStmt:
return s.Else != nil && s.Then != nil && terminating(s.Then.Stmts) && isTerminating(s.Else)
case *ast.ForStmt:
return s.Cond == nil && s.Body != nil && !hasBreak(s.Body.Stmts)
case *ast.SwitchStmt:
hasDefault := false
for _, cl := range s.Cases {
if cl.Default {
hasDefault = true
}
if !terminating(cl.Body) || hasBreak(cl.Body) {
return false
}
}
return hasDefault
}
return false
}
func hasBreak(list []ast.Stmt) bool {
for _, s := range list {
switch s := s.(type) {
case *ast.BranchStmt:
if s.Tok == token.Break {
return true
}
case *ast.BlockStmt:
if hasBreak(s.Stmts) {
return true
}
case *ast.IfStmt:
if s.Then != nil && hasBreak(s.Then.Stmts) {
return true
}
if s.Else != nil && hasBreak([]ast.Stmt{s.Else}) {
return true
}
}
}
return false
}