Files
aego/script/lexer/lexer.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

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(), "")
}