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.
262 lines
6.4 KiB
Go
262 lines
6.4 KiB
Go
package lexer
|
|
|
|
import (
|
|
"bytes"
|
|
"unicode/utf8"
|
|
|
|
"aego/script/diag"
|
|
"aego/script/source"
|
|
"aego/script/token"
|
|
)
|
|
|
|
var (
|
|
blockEnd = []byte("*/")
|
|
bom = []byte{0xEF, 0xBB, 0xBF}
|
|
)
|
|
|
|
type alt struct {
|
|
c byte
|
|
k token.Kind
|
|
}
|
|
|
|
type Lexer struct {
|
|
file source.FileID
|
|
src []byte
|
|
off int
|
|
semi bool
|
|
diags []diag.Diagnostic
|
|
buf []byte
|
|
in *source.Interner
|
|
comments []Comment
|
|
}
|
|
|
|
func New(file source.FileID, src []byte, in *source.Interner) *Lexer {
|
|
if in == nil {
|
|
in = source.NewInterner()
|
|
}
|
|
l := &Lexer{file: file, src: src, in: in}
|
|
if bytes.HasPrefix(src, bom) {
|
|
l.off = len(bom)
|
|
}
|
|
return l
|
|
}
|
|
|
|
func Tokenize(file source.FileID, src []byte) ([]token.Token, []diag.Diagnostic) {
|
|
return TokenizeIn(file, src, source.NewInterner())
|
|
}
|
|
|
|
func TokenizeIn(file source.FileID, src []byte, in *source.Interner) ([]token.Token, []diag.Diagnostic) {
|
|
l := New(file, src, in)
|
|
out := make([]token.Token, 0, 16+len(src)/4)
|
|
for {
|
|
t := l.Next()
|
|
out = append(out, t)
|
|
if t.Kind == token.EOF {
|
|
return out, l.diags
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *Lexer) Diagnostics() []diag.Diagnostic {
|
|
return l.diags
|
|
}
|
|
|
|
func (l *Lexer) at(i int) byte {
|
|
if j := l.off + i; j < len(l.src) {
|
|
return l.src[j]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (l *Lexer) run(pred func(byte) bool) int {
|
|
start := l.off
|
|
for l.off < len(l.src) && pred(l.src[l.off]) {
|
|
l.off++
|
|
}
|
|
return l.off - start
|
|
}
|
|
|
|
func (l *Lexer) intern(b []byte) string {
|
|
return l.in.Intern(b)
|
|
}
|
|
|
|
func (l *Lexer) errorf(code string, start, end int, format string, args ...any) {
|
|
sp := source.Span{File: l.file, Start: uint32(start), End: uint32(end)}
|
|
l.diags = append(l.diags, diag.Errorf(code, sp, format, args...))
|
|
}
|
|
|
|
func (l *Lexer) auto(at int) token.Token {
|
|
l.semi = false
|
|
return token.Token{Kind: token.Semicolon, Start: uint32(at), End: uint32(at), Text: "\n"}
|
|
}
|
|
|
|
func endsStatement(k token.Kind) bool {
|
|
switch k {
|
|
case token.Ident, token.Int, token.Float, token.String,
|
|
token.True, token.False, token.Nil,
|
|
token.RParen, token.RBrack, token.RBrace,
|
|
token.Return, token.Break, token.Continue, token.Yield,
|
|
token.Inc, token.Dec:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (l *Lexer) emit(k token.Kind, start int, text, value string) token.Token {
|
|
l.semi = endsStatement(k)
|
|
return token.Token{Kind: k, Start: uint32(start), End: uint32(l.off), Text: text, Value: value}
|
|
}
|
|
|
|
func (l *Lexer) Next() token.Token {
|
|
for l.off < len(l.src) {
|
|
c := l.src[l.off]
|
|
switch {
|
|
case c == ' ' || c == '\t' || c == '\r':
|
|
l.off++
|
|
case c == '\n':
|
|
if l.semi {
|
|
t := l.auto(l.off)
|
|
l.off++
|
|
return t
|
|
}
|
|
l.off++
|
|
case c == '/' && l.at(1) == '/':
|
|
start := l.off
|
|
for l.off < len(l.src) && l.src[l.off] != '\n' {
|
|
l.off++
|
|
}
|
|
l.comments = append(l.comments, Comment{Start: uint32(start), End: uint32(l.off), Line: true})
|
|
case c == '/' && l.at(1) == '*':
|
|
start := l.off
|
|
end := bytes.Index(l.src[l.off+2:], blockEnd)
|
|
if end < 0 {
|
|
l.errorf(diag.UnterminatedComment, start, start+2, "block comment is not terminated")
|
|
l.off = len(l.src)
|
|
l.comments = append(l.comments, Comment{Start: uint32(start), End: uint32(l.off)})
|
|
continue
|
|
}
|
|
body := l.src[l.off+2 : l.off+2+end]
|
|
l.off += end + 4
|
|
l.comments = append(l.comments, Comment{Start: uint32(start), End: uint32(l.off)})
|
|
if l.semi && bytes.IndexByte(body, '\n') >= 0 {
|
|
return l.auto(start)
|
|
}
|
|
default:
|
|
return l.scan()
|
|
}
|
|
}
|
|
if l.semi {
|
|
return l.auto(len(l.src))
|
|
}
|
|
return token.Token{Kind: token.EOF, Start: uint32(len(l.src)), End: uint32(len(l.src))}
|
|
}
|
|
|
|
func (l *Lexer) choose(def token.Kind, alts ...alt) token.Kind {
|
|
if l.off < len(l.src) {
|
|
for _, a := range alts {
|
|
if l.src[l.off] == a.c {
|
|
l.off++
|
|
return a.k
|
|
}
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func (l *Lexer) scan() token.Token {
|
|
start := l.off
|
|
c := l.src[start]
|
|
switch {
|
|
case isLetter(c):
|
|
l.run(isIdentPart)
|
|
text := l.intern(l.src[start:l.off])
|
|
k := token.Lookup(text)
|
|
if k.IsReserved() {
|
|
l.errorf(diag.ReservedWord, start, l.off, "%q is reserved for a future version of Aego Script", text)
|
|
k = token.Ident
|
|
}
|
|
return l.emit(k, start, text, "")
|
|
case isDigit(c) || (c == '.' && isDigit(l.at(1))):
|
|
return l.number(start)
|
|
case c == '"':
|
|
return l.str(start)
|
|
case c == '\'':
|
|
l.off++
|
|
for l.off < len(l.src) && l.src[l.off] != '\'' && l.src[l.off] != '\n' {
|
|
l.off++
|
|
}
|
|
if l.off < len(l.src) && l.src[l.off] == '\'' {
|
|
l.off++
|
|
}
|
|
l.errorf(diag.UnexpectedChar, start, l.off, "character literals are not supported, use a string")
|
|
return l.emit(token.Illegal, start, string(l.src[start:l.off]), "")
|
|
case c >= utf8.RuneSelf:
|
|
r, size := utf8.DecodeRune(l.src[start:])
|
|
l.off += size
|
|
if r == utf8.RuneError && size == 1 {
|
|
l.errorf(diag.InvalidUTF8, start, l.off, "invalid UTF-8 encoding")
|
|
} else {
|
|
l.errorf(diag.UnexpectedChar, start, l.off, "identifiers may use ASCII letters only; %q is not allowed", r)
|
|
}
|
|
return l.emit(token.Illegal, start, string(l.src[start:l.off]), "")
|
|
}
|
|
l.off++
|
|
k := token.Illegal
|
|
switch c {
|
|
case '+':
|
|
k = l.choose(token.Add, alt{'+', token.Inc}, alt{'=', token.AddAssign})
|
|
case '-':
|
|
k = l.choose(token.Sub, alt{'-', token.Dec}, alt{'=', token.SubAssign})
|
|
case '*':
|
|
k = l.choose(token.Mul, alt{'=', token.MulAssign})
|
|
case '/':
|
|
k = l.choose(token.Quo, alt{'=', token.QuoAssign})
|
|
case '%':
|
|
k = l.choose(token.Rem, alt{'=', token.RemAssign})
|
|
case '=':
|
|
k = l.choose(token.Assign, alt{'=', token.Eq})
|
|
case '!':
|
|
k = l.choose(token.Not, alt{'=', token.Ne})
|
|
case '<':
|
|
k = l.choose(token.Lt, alt{'=', token.Le})
|
|
case '>':
|
|
k = l.choose(token.Gt, alt{'=', token.Ge})
|
|
case ':':
|
|
k = l.choose(token.Colon, alt{'=', token.Define})
|
|
case '&':
|
|
k = l.choose(token.Illegal, alt{'&', token.AndAnd})
|
|
case '|':
|
|
k = l.choose(token.Illegal, alt{'|', token.OrOr})
|
|
case '.':
|
|
k = token.Period
|
|
case ',':
|
|
k = token.Comma
|
|
case ';':
|
|
k = token.Semicolon
|
|
case '(':
|
|
k = token.LParen
|
|
case ')':
|
|
k = token.RParen
|
|
case '[':
|
|
k = token.LBrack
|
|
case ']':
|
|
k = token.RBrack
|
|
case '{':
|
|
k = token.LBrace
|
|
case '}':
|
|
k = token.RBrace
|
|
case '@':
|
|
k = token.At
|
|
}
|
|
if k == token.Illegal {
|
|
text := string(l.src[start:l.off])
|
|
if c == '&' || c == '|' {
|
|
l.errorf(diag.UnexpectedChar, start, l.off, "operator %q is not supported, Aego Script has no bitwise operators", text)
|
|
} else {
|
|
l.errorf(diag.UnexpectedChar, start, l.off, "unexpected character %q", text)
|
|
}
|
|
return l.emit(token.Illegal, start, text, "")
|
|
}
|
|
return l.emit(k, start, k.String(), "")
|
|
}
|