Function-entry Frame() allocation counted only top-level LOCAL
declarations from fn.Body. Mid-function LOCALs hidden inside an
IF / FOR / WHILE / DO CASE / SWITCH / SEQUENCE block weren't
included, so the runtime allocated a frame too small to hold them.
Subsequent reads/writes via PopLocalFast / PushLocalFast / LocalAdd
to those slot indices then either silently scribbled past the frame
(read-back saw NIL) or panicked with "local variable index out of
range" once the index exceeded the underlying slice.
This is the underlying bug behind frb_demo Section 4 — the
`LOCAL ch := Channel(1)` declared inside `IF pAsync != NIL` got
slot N+1 from the codegen but the runtime only allocated N. The
Channel value was scribbled past the frame, ChReceive then read
NIL from a non-existent slot, and the goroutine's ChSend(49) had
nowhere to land.
New helper gen_util.go::countLocalsInStmts walks every nested body
(IF + ElseIfs + ElseBody, ForStmt, ForEachStmt, DoWhileStmt,
SeqStmt's Body + RecoverBody, SwitchStmt's Cases + Otherwise) and
totals every ScopeLocal VarDecl. The function-emit caller adds this
to the top-level count before sizing the Frame.
Test fixture (tests/frb/test_frb_goroutine.prg) reproduces the
demo Section 4 shape — `LOCAL ch := Channel(1)` inside IF, then
`Go("WORKER", ch, 7)`, then ChReceive(ch). Wired into the FRB
runner so it stands at 6/6.
Other gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
std.ch suite : 15/15
FRB suite : 6/6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1641 lines
49 KiB
Go
1641 lines
49 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Go code generator for the Five language.
|
|
// Converts Five AST into Go source code that calls hbrt runtime functions.
|
|
//
|
|
// Design references:
|
|
// - Harbour: gencc.c — pcode → hb_xvm*() C function calls
|
|
// - tsgo: internal/printer/printer.go — AST → text via Writer interface
|
|
// - Pattern: AST node → Thread method call (t.PushInt, t.Plus, etc.)
|
|
//
|
|
// Generated code structure:
|
|
// package main
|
|
// import ("five/hbrt"; "five/hbrtl")
|
|
// var symbols = hbrt.NewModule(...)
|
|
// func FV_MAIN(t *hbrt.Thread) { ... }
|
|
// func main() { vm := hbrt.NewVM(); ... vm.Run("MAIN") }
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"five/compiler/token"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Generator produces Go source code from a Five AST.
|
|
type Generator struct {
|
|
buf strings.Builder
|
|
indent int
|
|
file *ast.File
|
|
symbols []symbolEntry
|
|
imports map[string]bool
|
|
importAlias map[string]string // path → alias ("_", "name", or "")
|
|
curLocals localMap // current function's local variable map
|
|
goFastFuncs []goFastEntry // Go functions to register as FastFunc
|
|
staticVars map[string]string // top-level STATIC: upper name → Go var name
|
|
IsLibrary bool // if true, no main() generated, symbols use unique name
|
|
hoistedFields []string // field names hoisted outside FOR loop (nil = not hoisting)
|
|
hoistedDW bool // DO WHILE has hoisted _dwa/_darea
|
|
symCache map[string]string // symbol name → cached variable name (nil = not caching)
|
|
Debug bool // if true, emit t.DebugLine() calls
|
|
forLabelSeq int // monotonic counter for FOR..NEXT LOOP labels
|
|
curForLabel string // current FOR loop's LOOP goto label ("" if not in FOR)
|
|
blockSeq int // monotonic counter for unique closure capture names
|
|
// Per-function constant locals: LOCAL names (uppercase) whose sole
|
|
// assignment is a literal initializer. Reads get substituted inline.
|
|
constLocals map[string]*ast.LiteralExpr
|
|
// Class name of the currently-emitting method body, used to resolve
|
|
// ::super: at compile time against the defining class's Parent.
|
|
curMethodClass string
|
|
}
|
|
|
|
type symbolEntry struct {
|
|
name string
|
|
scope string // "hbrt.FsPublic|hbrt.FsLocal" etc.
|
|
fn string // Go function name: "FV_MAIN"
|
|
}
|
|
|
|
// Generate converts an AST File into Go source code.
|
|
func Generate(file *ast.File) string {
|
|
return doGenerate(file, false, false)
|
|
}
|
|
|
|
// GenerateWithDebug is like Generate but includes DebugLine calls.
|
|
func GenerateWithDebug(file *ast.File) string {
|
|
return doGenerate(file, true, false)
|
|
}
|
|
|
|
// GenerateLibrary generates Go code without main() — for multi-PRG builds.
|
|
func GenerateLibrary(file *ast.File) string {
|
|
return doGenerate(file, false, true)
|
|
}
|
|
|
|
// generate is the unified internal implementation.
|
|
// doGenerate is the unified internal implementation for all Generate* variants.
|
|
func doGenerate(file *ast.File, debug, library bool) string {
|
|
g := &Generator{
|
|
file: file,
|
|
imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true},
|
|
Debug: debug,
|
|
IsLibrary: library,
|
|
}
|
|
|
|
// Collect symbols from declarations
|
|
for _, d := range file.Decls {
|
|
switch decl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
scope := "hbrt.FsPublic|hbrt.FsLocal"
|
|
if !library && (decl.Name == "Main" || decl.Name == "MAIN") {
|
|
scope += "|hbrt.FsFirst"
|
|
}
|
|
g.symbols = append(g.symbols, symbolEntry{
|
|
name: strings.ToUpper(decl.Name),
|
|
scope: scope,
|
|
fn: "FV_" + strings.ToUpper(decl.Name),
|
|
})
|
|
case *ast.ClassDecl:
|
|
className := strings.ToUpper(decl.Name)
|
|
g.symbols = append(g.symbols, symbolEntry{
|
|
name: className,
|
|
scope: "hbrt.FsPublic|hbrt.FsLocal",
|
|
fn: "FV_" + className + "_CTOR",
|
|
})
|
|
}
|
|
}
|
|
|
|
if hasXBaseCommands(file) {
|
|
g.imports["five/hbrdd"] = true
|
|
g.imports["five/hbrdd/dbf"] = true
|
|
}
|
|
|
|
g.importAlias = make(map[string]string)
|
|
for _, imp := range file.Imports {
|
|
g.imports[imp.Path] = true
|
|
if imp.Alias != "" {
|
|
g.importAlias[imp.Path] = imp.Alias
|
|
}
|
|
}
|
|
|
|
if hasXBaseCommands(file) {
|
|
// Blank-import the in-memory RDD so MEMRDD / "mem:" paths work
|
|
// from PRG (the driver registers itself in its init).
|
|
g.imports["five/hbrdd/mem"] = true
|
|
g.importAlias["five/hbrdd/mem"] = "_"
|
|
}
|
|
|
|
g.symCache = map[string]string{}
|
|
|
|
g.emitHeader()
|
|
g.emitSymbols()
|
|
for _, d := range file.Decls {
|
|
g.emitDecl(d)
|
|
}
|
|
g.emitFastFuncRegistrations()
|
|
if library {
|
|
g.emitInitModule()
|
|
} else {
|
|
g.emitMain()
|
|
}
|
|
g.emitSymCache()
|
|
|
|
// Patch deferred imports: inline RTL may add "fmt"/"strings" after header was emitted.
|
|
result := g.buf.String()
|
|
var deferred string
|
|
if g.imports["fmt"] && !strings.Contains(result, "\"fmt\"") {
|
|
deferred += "\t\"fmt\"\n"
|
|
}
|
|
if g.imports["strings"] && !strings.Contains(result, "\"strings\"") {
|
|
deferred += "\t\"strings\"\n"
|
|
}
|
|
result = strings.Replace(result, "\t/*DEFERRED_IMPORTS*/\n", deferred, 1)
|
|
// Patch guards
|
|
if g.imports["strings"] {
|
|
result = strings.Replace(result, "/*GUARD_STRINGS*/", "var _ = strings.TrimLeft", 1)
|
|
} else {
|
|
result = strings.Replace(result, "/*GUARD_STRINGS*/\n", "", 1)
|
|
}
|
|
if g.imports["fmt"] {
|
|
result = strings.Replace(result, "/*GUARD_FMT*/", "var _ = fmt.Sprintf", 1)
|
|
} else {
|
|
result = strings.Replace(result, "/*GUARD_FMT*/\n", "", 1)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- Emit infrastructure ---
|
|
|
|
func (g *Generator) write(s string) {
|
|
g.buf.WriteString(s)
|
|
}
|
|
|
|
func (g *Generator) writef(format string, args ...interface{}) {
|
|
fmt.Fprintf(&g.buf, format, args...)
|
|
}
|
|
|
|
// symVar returns the package-level cache variable name for the given
|
|
// symbol, registering it if first seen. The name embeds a per-file
|
|
// prefix so multi-PRG builds don't collide on identical symbol names
|
|
// across files. Characters that can't appear in a Go identifier are
|
|
// replaced with underscores.
|
|
func (g *Generator) symVar(name string) string {
|
|
if g.symCache == nil {
|
|
g.symCache = map[string]string{}
|
|
}
|
|
if v, ok := g.symCache[name]; ok {
|
|
return v
|
|
}
|
|
var sb strings.Builder
|
|
sb.WriteString("_sym_")
|
|
sb.WriteString(g.fileKey())
|
|
sb.WriteByte('_')
|
|
for i := 0; i < len(name); i++ {
|
|
c := name[i]
|
|
switch {
|
|
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9', c == '_':
|
|
sb.WriteByte(c)
|
|
default:
|
|
sb.WriteByte('_')
|
|
}
|
|
}
|
|
v := sb.String()
|
|
g.symCache[name] = v
|
|
return v
|
|
}
|
|
|
|
// fileKey derives a short identifier-safe prefix from g.file.Name so
|
|
// package-level symbol caches don't collide across PRG files.
|
|
func (g *Generator) fileKey() string {
|
|
base := g.file.Name
|
|
if idx := strings.LastIndex(base, "/"); idx >= 0 {
|
|
base = base[idx+1:]
|
|
}
|
|
if idx := strings.LastIndexByte(base, '.'); idx >= 0 {
|
|
base = base[:idx]
|
|
}
|
|
var sb strings.Builder
|
|
for i := 0; i < len(base); i++ {
|
|
c := base[i]
|
|
switch {
|
|
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9':
|
|
sb.WriteByte(c)
|
|
default:
|
|
sb.WriteByte('_')
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// emitPushSymbol writes PushSymbol using the lazy-cached package-level
|
|
// variable. First call resolves via VM; subsequent calls skip the
|
|
// RWMutex + map lookup.
|
|
func (g *Generator) emitPushSymbol(name string) {
|
|
v := g.symVar(name)
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, name))
|
|
}
|
|
|
|
|
|
// emitSymCache writes the package-level `var _sym_NAME *hbrt.Symbol`
|
|
// declarations discovered during body emission. Called after all
|
|
// function bodies are emitted so every PushSymbol call site has had
|
|
// a chance to register its target name.
|
|
func (g *Generator) emitSymCache() {
|
|
if len(g.symCache) == 0 {
|
|
return
|
|
}
|
|
// Deterministic order so diffs are stable.
|
|
names := make([]string, 0, len(g.symCache))
|
|
for k := range g.symCache {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
g.writeln("")
|
|
g.writeln("// Cached symbol pointers — populated lazily on first use.")
|
|
for _, n := range names {
|
|
g.writeln(fmt.Sprintf("var %s *hbrt.Symbol", g.symCache[n]))
|
|
}
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) writeln(s string) {
|
|
g.writeIndent()
|
|
g.buf.WriteString(s)
|
|
g.buf.WriteByte('\n')
|
|
}
|
|
|
|
func (g *Generator) writeIndent() {
|
|
for i := 0; i < g.indent; i++ {
|
|
g.buf.WriteByte('\t')
|
|
}
|
|
}
|
|
|
|
// --- File structure ---
|
|
|
|
func (g *Generator) emitHeader() {
|
|
g.writeln("// Code generated by Five compiler. DO NOT EDIT.")
|
|
g.writeln(fmt.Sprintf("// Source: %s", g.file.Name))
|
|
g.writeln("")
|
|
g.writeln("package main")
|
|
g.writeln("")
|
|
|
|
// Imports (deferred placeholder for imports discovered during body emission)
|
|
g.writeln("import (")
|
|
g.indent++
|
|
for imp := range g.imports {
|
|
if alias, ok := g.importAlias[imp]; ok {
|
|
g.writeln(fmt.Sprintf("%s %q", alias, imp))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("%q", imp))
|
|
}
|
|
}
|
|
g.writeln("/*DEFERRED_IMPORTS*/")
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
|
|
// Ensure imports are used
|
|
g.writeln("var _ = hbrtl.RegisterRTL")
|
|
if g.imports["five/hbrdd"] {
|
|
g.writeln("var _ = hbrdd.NewWorkAreaManager")
|
|
g.writeln("var _ dbf.DBFDriver")
|
|
}
|
|
// Always emit — deferred inline RTL may add these imports after header.
|
|
// If not actually imported, the DEFERRED_IMPORTS patch won't add them and
|
|
// Go compiler will error. But we also strip unused guards in the patch step.
|
|
g.writeln("/*GUARD_STRINGS*/")
|
|
g.writeln("/*GUARD_FMT*/")
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitSymbols() {
|
|
varName := "symbols"
|
|
if g.IsLibrary {
|
|
// Unique variable name for library mode
|
|
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
|
|
safeName = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, safeName)
|
|
varName = "symbols_" + safeName
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = hbrt.NewModule(%q,", varName, strings.TrimSuffix(g.file.Name, ".prg")))
|
|
g.indent++
|
|
for _, sym := range g.symbols {
|
|
g.writeln(fmt.Sprintf("hbrt.Sym(%q, %s, %s),", sym.name, sym.scope, sym.fn))
|
|
}
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
}
|
|
|
|
// emitFastFuncRegistrations emits var declarations for Go FastFunc registrations.
|
|
// These are pre-registered at package init time for 3-11x faster calls.
|
|
func (g *Generator) emitFastFuncRegistrations() {
|
|
if len(g.goFastFuncs) == 0 {
|
|
return
|
|
}
|
|
// Deduplicate
|
|
seen := map[string]bool{}
|
|
g.writeln("// Go FastFunc registrations (type-specialized, bypass reflect)")
|
|
g.writeln("var (")
|
|
g.indent++
|
|
for _, ff := range g.goFastFuncs {
|
|
if seen[ff.regName] {
|
|
continue
|
|
}
|
|
seen[ff.regName] = true
|
|
g.writeln(fmt.Sprintf("_ff_%s = hbrt.RegisterFastFunc(%q, %s)", ff.regName, ff.qualName, ff.qualName))
|
|
}
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitMain() {
|
|
// init() runs before main() — set raw mode before ANY Go runtime I/O
|
|
g.writeln("func init() {")
|
|
g.indent++
|
|
g.writeln("hbrtl.InitRawTerminal()")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
g.writeln("func main() {")
|
|
g.indent++
|
|
g.writeln("vm := hbrt.NewVM()")
|
|
g.writeln("hbrtl.RegisterRTL(vm)")
|
|
g.writeln("vm.RegisterModule(symbols)")
|
|
// Pick up any HB_FUNC() registrations from #pragma BEGINDUMP blocks
|
|
// — their package init() queues them into hbrt.dynamicFuncs, and
|
|
// RegisterLibModules pulls them into this VM.
|
|
g.writeln("vm.RegisterLibModules()")
|
|
// Find main function
|
|
mainName := "MAIN"
|
|
for _, sym := range g.symbols {
|
|
if strings.Contains(sym.scope, "FsFirst") {
|
|
mainName = sym.name
|
|
break
|
|
}
|
|
}
|
|
g.writeln(fmt.Sprintf("vm.Run(%q)", mainName))
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitInitModule() {
|
|
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
|
|
safeName = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, safeName)
|
|
varName := "symbols_" + safeName
|
|
|
|
// Register this module's symbols into a global registry
|
|
// that main()'s vm.RegisterModule will pick up
|
|
g.writeln(fmt.Sprintf("func init() {"))
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("hbrt.RegisterLibModule(%s)", varName))
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
// --- Declaration emission ---
|
|
|
|
func (g *Generator) emitDecl(d ast.Decl) {
|
|
switch decl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
g.emitFuncDecl(decl)
|
|
case *ast.ClassDecl:
|
|
g.emitClassDecl(decl)
|
|
case *ast.MethodDecl:
|
|
g.emitMethodDeclStandalone(decl)
|
|
case *ast.VarDecl:
|
|
// Top-level STATIC → package-level var
|
|
if decl.Scope == ast.ScopeStatic {
|
|
g.emitTopLevelStatic(decl)
|
|
}
|
|
case *ast.GoDumpDecl:
|
|
// Inline Go code from #pragma BEGINDUMP ... ENDDUMP. Extract
|
|
// any `import` directives inside the block and fold them into
|
|
// the generator's own imports map — Go insists imports live at
|
|
// the top of the file, so we can't just splat the raw dump in
|
|
// the middle of the declarations.
|
|
if decl.Code != "" {
|
|
body := hoistGoImports(decl.Code, g.imports)
|
|
g.writeln("\n// --- Inline Go code (#pragma BEGINDUMP) ---")
|
|
g.write(body)
|
|
g.writeln("\n// --- End inline Go code ---\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// hoistGoImports scans a raw Go source block for top-level `import`
|
|
// statements, registers each imported path in `imports`, and returns
|
|
// the body with those directives stripped. Handles both forms:
|
|
//
|
|
// import "path"
|
|
// import (
|
|
// "path1"
|
|
// alias "path2"
|
|
// _ "path3"
|
|
// )
|
|
//
|
|
// Imports in the middle of the dump would violate Go's "imports first"
|
|
// rule. Five's own top-of-file import block will already include
|
|
// whatever the hoisted set adds, so the end result links correctly.
|
|
func hoistGoImports(code string, imports map[string]bool) string {
|
|
lines := strings.Split(code, "\n")
|
|
var out []string
|
|
i := 0
|
|
for i < len(lines) {
|
|
trim := strings.TrimSpace(lines[i])
|
|
// Block form: `import (`
|
|
if trim == "import (" || trim == "import(" {
|
|
i++
|
|
for i < len(lines) {
|
|
t := strings.TrimSpace(lines[i])
|
|
if t == ")" {
|
|
i++
|
|
break
|
|
}
|
|
// Parse `path`, `alias path`, `_ path` — each with
|
|
// quoted path as the final token.
|
|
if start := strings.Index(t, `"`); start >= 0 {
|
|
if end := strings.LastIndex(t, `"`); end > start {
|
|
imports[t[start+1:end]] = true
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
// Single form: `import "path"` or `import alias "path"`.
|
|
if strings.HasPrefix(trim, "import ") || strings.HasPrefix(trim, "import\t") {
|
|
if start := strings.Index(trim, `"`); start >= 0 {
|
|
if end := strings.LastIndex(trim, `"`); end > start {
|
|
imports[trim[start+1:end]] = true
|
|
}
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
out = append(out, lines[i])
|
|
i++
|
|
}
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
// emitTopLevelStatic emits module-level STATIC variables as package-level Go vars.
|
|
func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) {
|
|
for _, v := range vd.Vars {
|
|
varName := "static_" + strings.ToUpper(v.Name)
|
|
initVal := "hbrt.MakeNil()"
|
|
if v.Init != nil {
|
|
initVal = g.exprToGoLiteral(v.Init)
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = %s", varName, initVal))
|
|
// Register in staticMap for lookup
|
|
if g.staticVars == nil {
|
|
g.staticVars = make(map[string]string)
|
|
}
|
|
g.staticVars[strings.ToUpper(v.Name)] = varName
|
|
}
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
|
|
goName := "FV_" + strings.ToUpper(fn.Name)
|
|
|
|
// Emit function-level STATIC variables as package-level Go vars.
|
|
// Harbour: STATIC inside FUNCTION persists across calls but is
|
|
// scoped to the function. We prefix with funcname to avoid clashes.
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
|
|
for _, v := range vd.Vars {
|
|
varName := "static_" + strings.ToUpper(fn.Name) + "_" + strings.ToUpper(v.Name)
|
|
initVal := "hbrt.MakeNil()"
|
|
if v.Init != nil {
|
|
initVal = g.exprToGoLiteral(v.Init)
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = %s", varName, initVal))
|
|
if g.staticVars == nil {
|
|
g.staticVars = make(map[string]string)
|
|
}
|
|
g.staticVars[strings.ToUpper(v.Name)] = varName
|
|
}
|
|
}
|
|
}
|
|
// Also scan body for mid-function STATIC declarations
|
|
for _, s := range fn.Body {
|
|
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
|
|
for _, v := range vd.Vars {
|
|
varName := "static_" + strings.ToUpper(fn.Name) + "_" + strings.ToUpper(v.Name)
|
|
initVal := "hbrt.MakeNil()"
|
|
if v.Init != nil {
|
|
initVal = g.exprToGoLiteral(v.Init)
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = %s", varName, initVal))
|
|
if g.staticVars == nil {
|
|
g.staticVars = make(map[string]string)
|
|
}
|
|
g.staticVars[strings.ToUpper(v.Name)] = varName
|
|
}
|
|
}
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goName))
|
|
g.indent++
|
|
|
|
// Count params and locals (including mid-function LOCALs in Body —
|
|
// also the ones nested inside IF / FOR / WHILE / DO CASE / BEGIN
|
|
// SEQUENCE / etc.). Without recursion the function frame was sized
|
|
// to only the top-level LOCALs, so a nested `LOCAL ch :=
|
|
// Channel(1)` inside `IF ...` got slot N+1 from the codegen but
|
|
// the runtime allocated N slots — silent miscompile when the
|
|
// nested local was later read (see frb_demo Section 4).
|
|
nParams := len(fn.Params)
|
|
nLocals := 0
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
nLocals += len(vd.Vars)
|
|
}
|
|
}
|
|
nLocals += countLocalsInStmts(fn.Body)
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
g.writeln("")
|
|
|
|
// Build local map FIRST (needed for init expressions that reference params)
|
|
g.curLocals = g.buildLocalMap(fn)
|
|
// Also include mid-function LOCALs so their names show up in the
|
|
// debugger / error.log for any frame that has executed past the decl.
|
|
{
|
|
idx := len(g.curLocals) + 1
|
|
scanBodyLocals(fn.Body, g.curLocals, &idx)
|
|
}
|
|
g.emitLocalNames(fn.Name, g.curLocals)
|
|
// Scan for LOCALs that are literal-initialised and never reassigned
|
|
// so reads can be constant-propagated at emit time.
|
|
g.constLocals = collectConstLocals(fn)
|
|
|
|
// Emit LOCAL initializers. LOCALs that were const-propagated have
|
|
// their reads substituted inline, so the init store has no live
|
|
// reader — skip it (dead-store elimination).
|
|
localIdx := nParams + 1 // 1-based, params come first
|
|
for _, d := range fn.Decls {
|
|
vd, ok := d.(*ast.VarDecl)
|
|
if !ok || vd.Scope != ast.ScopeLocal {
|
|
continue
|
|
}
|
|
for _, v := range vd.Vars {
|
|
if v.Init != nil {
|
|
if _, isConst := g.constLocals[strings.ToUpper(v.Name)]; !isConst {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", localIdx))
|
|
}
|
|
}
|
|
localIdx++
|
|
}
|
|
}
|
|
|
|
// Emit body statements
|
|
for _, stmt := range fn.Body {
|
|
g.emitStmt(stmt, g.curLocals)
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
|
|
// Drop per-function constant map so it doesn't leak to siblings.
|
|
g.constLocals = nil
|
|
|
|
// Remove function-scoped STATIC vars from the map so they don't
|
|
// leak into other functions that happen to share a variable name.
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
|
|
for _, v := range vd.Vars {
|
|
delete(g.staticVars, strings.ToUpper(v.Name))
|
|
}
|
|
}
|
|
}
|
|
for _, s := range fn.Body {
|
|
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
|
|
for _, v := range vd.Vars {
|
|
delete(g.staticVars, strings.ToUpper(v.Name))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type localMap map[string]int
|
|
|
|
// emitLocalNames tells the runtime the PRG-level names of this frame's
|
|
// params+locals, so the debugger and error.log can show "i, nSum"
|
|
// instead of "_1, _2". Emitted as an inline slice literal — one
|
|
// allocation per call (24-byte slice header); string literals are
|
|
// interned so the underlying data sits in the binary's data segment.
|
|
func (g *Generator) emitLocalNames(funcName string, locals localMap) {
|
|
if len(locals) == 0 {
|
|
return
|
|
}
|
|
// Reverse the map: slot index → PRG name.
|
|
names := make([]string, len(locals))
|
|
for name, idx := range locals {
|
|
if idx >= 1 && idx <= len(names) {
|
|
names[idx-1] = name
|
|
}
|
|
}
|
|
// Drop trailing empty slots (defensive — map may have holes).
|
|
for len(names) > 0 && names[len(names)-1] == "" {
|
|
names = names[:len(names)-1]
|
|
}
|
|
if len(names) == 0 {
|
|
return
|
|
}
|
|
var sb strings.Builder
|
|
sb.WriteString("t.SetLocalNames([]string{")
|
|
for i, n := range names {
|
|
if i > 0 {
|
|
sb.WriteString(", ")
|
|
}
|
|
sb.WriteString(strconv.Quote(n))
|
|
}
|
|
sb.WriteString("})")
|
|
g.writeln(sb.String())
|
|
}
|
|
|
|
func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap {
|
|
m := make(localMap)
|
|
idx := 1
|
|
for _, p := range fn.Params {
|
|
m[strings.ToUpper(p.Name)] = idx
|
|
idx++
|
|
}
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
for _, v := range vd.Vars {
|
|
m[strings.ToUpper(v.Name)] = idx
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// scanBodyLocals recursively scans statements for LOCAL declarations,
|
|
// adding them to the local map with pre-assigned indices.
|
|
// This ensures mid-function LOCALs are known at compile time.
|
|
func scanBodyLocals(stmts []ast.Stmt, m localMap, idx *int) {
|
|
for _, s := range stmts {
|
|
switch st := s.(type) {
|
|
case *ast.VarDecl:
|
|
if st.Scope == ast.ScopeLocal {
|
|
for _, v := range st.Vars {
|
|
name := strings.ToUpper(v.Name)
|
|
if _, exists := m[name]; !exists {
|
|
m[name] = *idx
|
|
(*idx)++
|
|
}
|
|
}
|
|
}
|
|
case *ast.IfStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
for _, ei := range st.ElseIfs {
|
|
scanBodyLocals(ei.Body, m, idx)
|
|
}
|
|
scanBodyLocals(st.ElseBody, m, idx)
|
|
case *ast.DoWhileStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.ForStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.ForEachStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.SwitchStmt:
|
|
for _, c := range st.Cases {
|
|
scanBodyLocals(c.Body, m, idx)
|
|
}
|
|
scanBodyLocals(st.Otherwise, m, idx)
|
|
case *ast.SeqStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
scanBodyLocals(st.RecoverBody, m, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func (g *Generator) emitExpr(expr ast.Expr) {
|
|
switch e := expr.(type) {
|
|
case *ast.LiteralExpr:
|
|
g.emitLiteral(e)
|
|
case *ast.IdentExpr:
|
|
g.emitIdent(e)
|
|
case *ast.BinaryExpr:
|
|
// Compile-time constant folding. Fold the whole subtree so
|
|
// `(2*3) + 5` collapses to PushInt(11) and so string-concat
|
|
// tails like `x + "b" + "c"` reassociate into `x + "bc"`.
|
|
// Mutates the AST in place — safe because the generator owns
|
|
// the tree post-parse.
|
|
switch folded := foldLiteralTree(e).(type) {
|
|
case *ast.LiteralExpr:
|
|
g.emitLiteral(folded)
|
|
return
|
|
case *ast.BinaryExpr:
|
|
e = folded
|
|
}
|
|
// Short-circuit AND/OR: Harbour evaluates right operand only if needed.
|
|
// With a literal LHS we can skip the PushBool/PopLogical roundtrip
|
|
// entirely — .T. .AND. B folds to B, .F. .AND. B folds to false,
|
|
// and symmetrically for OR.
|
|
if e.Op == token.AND {
|
|
if v, ok := boolLiteralValue(e.Left); ok {
|
|
if v {
|
|
g.emitExpr(e.Right)
|
|
} else {
|
|
g.writeln("t.PushBool(false)")
|
|
}
|
|
break
|
|
}
|
|
g.emitExpr(e.Left)
|
|
g.writeln("if !t.PopLogical() {")
|
|
g.writeln("t.PushBool(false)")
|
|
g.writeln("} else {")
|
|
g.emitExpr(e.Right)
|
|
g.writeln("}")
|
|
} else if e.Op == token.OR {
|
|
if v, ok := boolLiteralValue(e.Left); ok {
|
|
if v {
|
|
g.writeln("t.PushBool(true)")
|
|
} else {
|
|
g.emitExpr(e.Right)
|
|
}
|
|
break
|
|
}
|
|
g.emitExpr(e.Left)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.writeln("t.PushBool(true)")
|
|
g.writeln("} else {")
|
|
g.emitExpr(e.Right)
|
|
g.writeln("}")
|
|
} else {
|
|
g.emitExpr(e.Left)
|
|
g.emitExpr(e.Right)
|
|
g.emitBinaryOp(e.Op)
|
|
}
|
|
case *ast.UnaryExpr:
|
|
// Fold `-<literal>` to a negative literal at compile time so
|
|
// `-42` becomes PushInt(-42) instead of PushInt(42) + Negate().
|
|
if e.Op == token.MINUS {
|
|
if lit, ok := e.X.(*ast.LiteralExpr); ok {
|
|
if neg, ok := negateLiteral(lit); ok {
|
|
g.emitLiteral(neg)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Fold `.NOT. .T.` → .F. and `.NOT. .F.` → .T. at compile time.
|
|
if e.Op == token.NOT {
|
|
if v, ok := boolLiteralValue(e.X); ok {
|
|
if v {
|
|
g.writeln("t.PushBool(false)")
|
|
} else {
|
|
g.writeln("t.PushBool(true)")
|
|
}
|
|
break
|
|
}
|
|
}
|
|
g.emitExpr(e.X)
|
|
g.emitUnaryOp(e.Op)
|
|
case *ast.AssignExpr:
|
|
// Handle compound assignment (+=, -=, :=) in expression context.
|
|
// Needed for code block bodies like {|x| nSum += x}.
|
|
g.emitAssignExpr(e)
|
|
case *ast.CallExpr:
|
|
g.emitCall(e)
|
|
case *ast.DotExpr:
|
|
// pkg.Member as value (rare — usually inside CallExpr)
|
|
g.writeln(fmt.Sprintf("t.PushValue(hbrt.WrapGo(%s.%s))", g.dotPkgName(e), e.Member))
|
|
case *ast.SendExpr:
|
|
g.emitSendExpr(e)
|
|
case *ast.IndexExpr:
|
|
g.emitExpr(e.X)
|
|
g.emitExpr(e.Index)
|
|
g.writeln("t.ArrayPush()")
|
|
case *ast.SelfExpr:
|
|
g.writeln("t.PushSelf()") // :: alone, rare
|
|
case *ast.ArrayLitExpr:
|
|
for _, item := range e.Items {
|
|
g.emitExpr(item)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(e.Items)))
|
|
case *ast.HashLitExpr:
|
|
for i := range e.Keys {
|
|
g.emitExpr(e.Keys[i])
|
|
g.emitExpr(e.Values[i])
|
|
}
|
|
g.writeln(fmt.Sprintf("t.HashGen(%d)", len(e.Keys)))
|
|
case *ast.BlockExpr:
|
|
g.emitBlock(e)
|
|
case *ast.SeqExpr:
|
|
// Comma-separated expr list (`{|| e1, e2, e3 }`): emit each in
|
|
// order; pop the result of every expr except the last so only
|
|
// the final value remains on the stack as the seq's value.
|
|
for i, item := range e.Items {
|
|
g.emitExpr(item)
|
|
if i < len(e.Items)-1 {
|
|
g.writeln("t.Pop2()")
|
|
}
|
|
}
|
|
case *ast.SliceExpr:
|
|
// a[low:high] → hbrt.ArraySlice(array, low, high)
|
|
g.emitExpr(e.X)
|
|
if e.Low != nil {
|
|
g.emitExpr(e.Low)
|
|
} else {
|
|
g.writeln("t.PushInt(1)") // default: from start (1-based)
|
|
}
|
|
if e.High != nil {
|
|
g.emitExpr(e.High)
|
|
} else {
|
|
g.writeln("t.PushInt(-1)") // default: to end (-1 = all)
|
|
}
|
|
g.writeln("t.ArraySlice()")
|
|
case *ast.NilSafeExpr:
|
|
// obj?:Method() → if not nil, call; else push NIL
|
|
g.emitExpr(e.X)
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_ns := t.Pop2()")
|
|
g.writeln("if _ns.IsNil() {")
|
|
g.indent++
|
|
g.writeln("t.PushNil()")
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.writeln("t.PushValue(_ns)")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
case *ast.InterpolatedString:
|
|
// Already converted to fmt.Sprintf CallExpr by parser
|
|
g.emitExpr(e.Parts[0]) // shouldn't reach here normally
|
|
case *ast.MacroExpr:
|
|
// &ident / &(expr) — evaluate the inner expression to get the
|
|
// source string, then MacroPush compiles and runs it via the
|
|
// hbrtl-installed evaluator hook.
|
|
g.emitExpr(e.Expr)
|
|
g.writeln("t.MacroPush()")
|
|
case *ast.AliasExpr:
|
|
g.emitAliasExpr(e)
|
|
case *ast.RefExpr:
|
|
// @variable — pass by reference
|
|
// In Five, we push a ByRef wrapper that holds the local index
|
|
if ident, ok := e.X.(*ast.IdentExpr); ok {
|
|
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocalRef(%d)", idx))
|
|
} else {
|
|
g.emitExpr(e.X) // fallback: push value
|
|
}
|
|
} else {
|
|
g.emitExpr(e.X)
|
|
}
|
|
case *ast.IIfExpr:
|
|
g.emitExpr(e.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
g.emitExpr(e.True)
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.emitExpr(e.False)
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
case *ast.PostfixExpr:
|
|
// `x++` / `x--` as a sub-expression (e.g. `y := x++`): push the
|
|
// ORIGINAL value for the surrounding expression to consume, then
|
|
// mutate the underlying storage so the next read sees the new
|
|
// value. The earlier Dup+Inc+Pop form only incremented a copy
|
|
// on the stack — the variable itself was never updated, so
|
|
// `y := x++` silently left x unchanged and `x++ + x++` read the
|
|
// same value twice. Matches Clipper / C post-increment
|
|
// semantics.
|
|
delta := "1"
|
|
if e.Op == token.DEC {
|
|
delta = "-1"
|
|
}
|
|
if ident, ok := e.X.(*ast.IdentExpr); ok {
|
|
upper := strings.ToUpper(ident.Name)
|
|
if idx, found := g.curLocals[upper]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, %s)", idx, delta))
|
|
return
|
|
}
|
|
if goVar, found := g.staticVars[upper]; found {
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
|
|
return
|
|
}
|
|
}
|
|
if send, ok := e.X.(*ast.SendExpr); ok {
|
|
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
|
|
fieldName := strings.ToUpper(send.Method)
|
|
// Push old value first (for outer expression),
|
|
// then read-modify-write the field.
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
if e.Op == token.INC {
|
|
g.writeln("t.PushInt(1)")
|
|
g.writeln("t.Plus()")
|
|
} else {
|
|
g.writeln("t.PushInt(1)")
|
|
g.writeln("t.Minus()")
|
|
}
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
return
|
|
}
|
|
}
|
|
// Fallback: expression has no assignable target (e.g. a
|
|
// parenthesised constant) — emit a no-op modification so the
|
|
// surrounding expression still sees the value.
|
|
g.emitExpr(e.X)
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unhandled expr %T", expr))
|
|
}
|
|
}
|
|
|
|
// emitIndexKeyExpr emits Go code that evaluates an INDEX ON key expression.
|
|
// Unlike emitExpr, bare identifiers (IdentExpr) are treated as DBF FIELD
|
|
// names — not local variables — because INDEX ON operates in field context.
|
|
// Function calls, UPPER/LOWER wrappers, and binary ops delegate to emitExpr
|
|
// (which handles them identically regardless of context).
|
|
func (g *Generator) emitIndexKeyExpr(expr ast.Expr) {
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
// Bare identifier in INDEX = field name → runtime FieldGet by name.
|
|
// The emitted code uses strings.ToUpper, so flag the import here
|
|
// — without this the deferred-import patcher leaves a stub.
|
|
g.imports["strings"] = true
|
|
fieldName := strings.ToUpper(e.Name)
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { for _fi := 0; _fi < _a.FieldCount(); _fi++ { if strings.ToUpper(_a.GetFieldInfo(_fi).Name) == %q { _v, _ := _a.GetValue(_fi); t.PushValue(_v); break } } } }`, fieldName))
|
|
case *ast.BinaryExpr:
|
|
// Recurse with field-aware emitter for both sides
|
|
g.emitIndexKeyExpr(e.Left)
|
|
g.emitIndexKeyExpr(e.Right)
|
|
g.emitBinaryOp(e.Op)
|
|
case *ast.CallExpr:
|
|
// Function call: emit normally (symbol + args + Function)
|
|
// But args might contain field refs, so use indexKeyExpr for args
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
upper := strings.ToUpper(ident.Name)
|
|
// Inline UPPER/LOWER for single-arg calls on fields
|
|
if (upper == "UPPER" || upper == "LOWER") && len(e.Args) == 1 {
|
|
g.emitIndexKeyExpr(e.Args[0])
|
|
if upper == "UPPER" {
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToUpper(_s)) }")
|
|
} else {
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToLower(_s)) }")
|
|
}
|
|
return
|
|
}
|
|
g.emitPushSymbol(upper)
|
|
} else {
|
|
g.emitExpr(e.Func)
|
|
}
|
|
g.writeln("t.PushNil()")
|
|
for _, arg := range e.Args {
|
|
g.emitIndexKeyExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Function(%d)", len(e.Args)))
|
|
case *ast.AliasExpr:
|
|
// FIELD->NAME or alias->field — delegate to standard emitter
|
|
g.emitExpr(expr)
|
|
default:
|
|
// Literals, etc. — standard emitter works fine
|
|
g.emitExpr(expr)
|
|
}
|
|
}
|
|
|
|
// exprToString extracts a string representation from an AST expression.
|
|
// containsCall reports whether an expression contains any function call.
|
|
// Used to distinguish runtime-evaluated INDEX ON filenames
|
|
// (`TO ( Lower(cTable) + "_pk.ntx" )`) from static ones (`TO test.ntx`).
|
|
func containsCall(expr ast.Expr) bool {
|
|
if expr == nil {
|
|
return false
|
|
}
|
|
switch e := expr.(type) {
|
|
case *ast.CallExpr:
|
|
return true
|
|
case *ast.BinaryExpr:
|
|
return containsCall(e.Left) || containsCall(e.Right)
|
|
case *ast.UnaryExpr:
|
|
return containsCall(e.X)
|
|
case *ast.SendExpr:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Used for INDEX ON key and filename, where idents are field/file names, not variables.
|
|
func exprToString(expr ast.Expr) string {
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
return e.Name
|
|
case *ast.LiteralExpr:
|
|
if e.Kind == token.STRING {
|
|
return `"` + e.Value + `"`
|
|
}
|
|
return e.Value
|
|
case *ast.BinaryExpr:
|
|
left := exprToString(e.Left)
|
|
right := exprToString(e.Right)
|
|
opStr := ""
|
|
switch e.Op {
|
|
case token.PLUS:
|
|
opStr = "+"
|
|
case token.MINUS:
|
|
opStr = "-"
|
|
case token.EQ:
|
|
opStr = "="
|
|
case token.EXEQ:
|
|
opStr = "=="
|
|
case token.NEQ:
|
|
opStr = "!="
|
|
case token.LT:
|
|
opStr = "<"
|
|
case token.GT:
|
|
opStr = ">"
|
|
case token.LTE:
|
|
opStr = "<="
|
|
case token.GTE:
|
|
opStr = ">="
|
|
case token.AND:
|
|
opStr = ".AND."
|
|
case token.OR:
|
|
opStr = ".OR."
|
|
}
|
|
if opStr != "" {
|
|
return left + " " + opStr + " " + right
|
|
}
|
|
case *ast.UnaryExpr:
|
|
if e.Op == token.NOT {
|
|
return "!" + exprToString(e.X)
|
|
}
|
|
case *ast.CallExpr:
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
args := ""
|
|
for i, a := range e.Args {
|
|
if i > 0 {
|
|
args += ","
|
|
}
|
|
args += exprToString(a)
|
|
}
|
|
return ident.Name + "(" + args + ")"
|
|
}
|
|
case *ast.AliasExpr:
|
|
// FIELD->NAME, M->VAR, ALIAS->FIELD
|
|
alias := exprToString(e.Alias)
|
|
field := exprToString(e.Field)
|
|
return alias + "->" + field
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *Generator) emitLiteral(e *ast.LiteralExpr) {
|
|
switch e.Kind {
|
|
case token.INT:
|
|
g.writeln(fmt.Sprintf("t.PushInt(%s)", e.Value))
|
|
case token.DOUBLE:
|
|
g.writeln(fmt.Sprintf("t.PushDouble(%s, 255, 255)", e.Value))
|
|
case token.STRING:
|
|
g.writeln(fmt.Sprintf("t.PushString(%q)", e.Value))
|
|
case token.TRUE:
|
|
g.writeln("t.PushBool(true)")
|
|
case token.FALSE:
|
|
g.writeln("t.PushBool(false)")
|
|
case token.NIL_LIT:
|
|
g.writeln("t.PushNil()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unknown literal kind %v", e.Kind))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitIdent(e *ast.IdentExpr) {
|
|
upper := strings.ToUpper(e.Name)
|
|
|
|
// Special: Self keyword → PushSelf
|
|
if upper == "SELF" {
|
|
g.writeln("t.PushSelf()")
|
|
return
|
|
}
|
|
|
|
// Constant-propagate literal-init LOCALs that collectConstLocals
|
|
// proved are never reassigned or passed by reference. Emitting the
|
|
// literal inline lets downstream folding (dead IF, AND/OR, FOR
|
|
// fused ops) fire on what was a variable reference.
|
|
if lit, ok := g.constLocals[upper]; ok {
|
|
g.emitLiteral(lit)
|
|
return
|
|
}
|
|
|
|
if idx, found := g.curLocals[upper]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
} else if goVar, found := g.staticVars[upper]; found {
|
|
// Module-level STATIC variable
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
} else {
|
|
// Unresolved at compile time — fall back to runtime memvar lookup.
|
|
// Harbour semantics: undefined identifiers try PRIVATE/PUBLIC memvar
|
|
// tables at runtime; missing → NIL. Prior behavior was PushLocal(0)
|
|
// which crashed any code path that actually reached the reference
|
|
// (e.g. missing #include, typo'd constant). PushMemvar returns NIL
|
|
// for missing names, matching Harbour's forgiving semantics.
|
|
g.writeln(fmt.Sprintf("t.PushMemvar(%q) // unresolved: fall through to memvar", e.Name))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitCall(e *ast.CallExpr) {
|
|
// Check for Go package call: pkg.Func(args)
|
|
if dot, ok := e.Func.(*ast.DotExpr); ok {
|
|
if g.isGoPackage(dot) {
|
|
g.emitGoPackageCall(dot, e.Args)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Inline hot-path RTL functions — skip Frame/EndProc/VM dispatch entirely
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
if g.tryEmitInlineRTL(ident.Name, e.Args) {
|
|
return
|
|
}
|
|
upper := strings.ToUpper(ident.Name)
|
|
// Guard: reserved words must never be emitted as function calls.
|
|
// This catches PRG bugs like stray ENDIF/ENDDO/NEXT from bad IF nesting.
|
|
if isReservedWord(upper) {
|
|
g.writeln(fmt.Sprintf("// WARN: reserved word %q used as function call — skipped", upper))
|
|
g.writeln("t.PushNil()")
|
|
return
|
|
}
|
|
g.emitPushSymbol(upper)
|
|
} else {
|
|
g.emitExpr(e.Func)
|
|
}
|
|
g.writeln("t.PushNil()")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Function(%d)", len(e.Args)))
|
|
}
|
|
|
|
// emitAssignExpr handles := / += / -= in expression context (e.g. code block body).
|
|
func (g *Generator) emitAssignExpr(e *ast.AssignExpr) {
|
|
// alias->field := value in expression context
|
|
if aliasExpr, ok := e.Left.(*ast.AliasExpr); ok {
|
|
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
|
|
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
|
|
upper := strings.ToUpper(aliasIdent.Name)
|
|
if upper == "M" || upper == "MEMVAR" {
|
|
// Memvar write — leaves a copy on the stack for the
|
|
// enclosing expression to pick up (matches Dup+SetAlias
|
|
// path).
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if ident, ok := e.Left.(*ast.IdentExpr); ok {
|
|
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
|
switch e.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
case token.PLUSEQ:
|
|
g.emitExpr(e.Right)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
case token.MINUSEQ:
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Negate()")
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(e.Right)
|
|
g.emitBinaryOp(e.Op)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// Fallback: unknown target
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln("// WARN: compound assignment — unknown target")
|
|
}
|
|
|
|
// isReservedWord returns true if the name is a Harbour reserved keyword
|
|
// that must never be emitted as a function call.
|
|
func isReservedWord(name string) bool {
|
|
switch name {
|
|
case "IF", "ELSE", "ELSEIF", "ENDIF",
|
|
"WHILE", "ENDDO",
|
|
"FOR", "NEXT", "TO", "STEP",
|
|
"RETURN", "FUNCTION", "PROCEDURE",
|
|
"LOCAL", "STATIC", "PRIVATE", "PUBLIC",
|
|
"BEGIN", "SEQUENCE", "RECOVER", "END",
|
|
"SWITCH", "CASE", "OTHERWISE", "ENDCASE",
|
|
"EXIT", "LOOP",
|
|
"CLASS", "ENDCLASS", "METHOD", "DATA",
|
|
"WITH", "OBJECT",
|
|
"NIL", "TRUE", "FALSE",
|
|
"AND", "OR", "NOT", "IN":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tryEmitInlineRTL emits direct Go code for known RTL functions.
|
|
// Returns true if handled (no VM dispatch needed).
|
|
// This eliminates Frame/EndProc/symbol lookup for hot-path functions.
|
|
func (g *Generator) tryEmitInlineRTL(name string, args []ast.Expr) bool {
|
|
upper := strings.ToUpper(name)
|
|
switch upper {
|
|
case "LTRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimLeft(_s, \" \")) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "RTRIM", "TRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimRight(_s, \" \")) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "ALLTRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimSpace(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "UPPER":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToUpper(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "LOWER":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToLower(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "LEN":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _v := t.Pop2(); if _v.IsString() { t.PushInt(len(_v.AsString())) } else if _v.IsArray() { t.PushInt(len(_v.AsArray().Items)) } else if _v.IsHash() { t.PushInt(len(_v.AsHash().Keys)) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "EMPTY":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _v := t.Pop2(); t.PushBool(_v.IsNil() || (_v.IsString() && len(strings.TrimSpace(_v.AsString())) == 0) || (_v.IsNumeric() && _v.AsNumDouble() == 0) || (_v.IsLogical() && !_v.AsBool()) || (_v.IsDate() && _v.AsJulian() == 0)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "CHR":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("t.PushString(string(byte(t.Pop2().AsNumInt())))")
|
|
return true
|
|
}
|
|
case "ASC":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); if len(_s)>0 { t.PushInt(int(_s[0])) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "EOF":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.EOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.EOF()) } else { t.PushBool(true) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "BOF":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.BOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.BOF()) } else { t.PushBool(true) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "FOUND":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Found())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Found()) } else { t.PushBool(false) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "RECNO":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("if %s != nil { t.PushInt(int(%s.RecNo())) } else { t.PushInt(0) }", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushInt(int(_a.RecNo())) } else { t.PushInt(0) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "DELETED":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Deleted())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Deleted()) } else { t.PushBool(false) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "RECCOUNT":
|
|
if len(args) == 0 {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { _rc, _ := _a.RecCount(); t.PushInt(int(_rc)) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "STR":
|
|
if len(args) >= 1 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _sv := t.Pop2()")
|
|
if len(args) >= 2 {
|
|
g.emitExpr(args[1])
|
|
g.writeln("_sw := int(t.Pop2().AsNumInt())")
|
|
} else {
|
|
g.writeln("_sw := 10")
|
|
}
|
|
if len(args) >= 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("_sd := int(t.Pop2().AsNumInt())")
|
|
} else {
|
|
g.writeln("_sd := 0")
|
|
}
|
|
g.writeln("_ss := fmt.Sprintf(\"%*.*f\", _sw, _sd, _sv.AsNumDouble())")
|
|
g.writeln("if len(_ss) > _sw && _sw > 0 { _ss = strings.Repeat(\"*\", _sw) }")
|
|
g.writeln("t.PushString(_ss) }")
|
|
g.imports["fmt"] = true
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "PADR":
|
|
if len(args) >= 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[:_pn])")
|
|
g.writeln("} else { t.PushString(_ps + hbrtl.Spaces(_pn - len(_ps))) } }")
|
|
return true
|
|
}
|
|
case "PADL":
|
|
if len(args) >= 2 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("{ _pf := t.Pop2().AsString(); _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
|
|
g.writeln("} else { t.PushString(strings.Repeat(_pf[:1], _pn-len(_ps)) + _ps) } }")
|
|
g.imports["strings"] = true
|
|
} else {
|
|
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
|
|
g.writeln("} else { t.PushString(hbrtl.Spaces(_pn - len(_ps)) + _ps) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "SUBSTR", "SUBSTRING":
|
|
if len(args) >= 2 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("{ _sl := int(t.Pop2().AsNumInt()); _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString()")
|
|
} else {
|
|
g.writeln("{ _sl := 0; _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString(); _sl = len(_ss) - _sp")
|
|
}
|
|
g.writeln("if _sp < 0 { _sp = 0 }; if _sp > len(_ss) { _sp = len(_ss) }")
|
|
g.writeln("if _sp+_sl > len(_ss) { _sl = len(_ss) - _sp }")
|
|
g.writeln("t.PushString(_ss[_sp:_sp+_sl]) }")
|
|
return true
|
|
}
|
|
case "LEFT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _ln := int(t.Pop2().AsNumInt()); _ls := t.Pop2().AsString()")
|
|
g.writeln("if _ln >= len(_ls) { t.PushString(_ls) } else if _ln <= 0 { t.PushString(\"\") } else { t.PushString(_ls[:_ln]) } }")
|
|
return true
|
|
}
|
|
case "RIGHT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _rn := int(t.Pop2().AsNumInt()); _rs := t.Pop2().AsString()")
|
|
g.writeln("if _rn >= len(_rs) { t.PushString(_rs) } else if _rn <= 0 { t.PushString(\"\") } else { t.PushString(_rs[len(_rs)-_rn:]) } }")
|
|
return true
|
|
}
|
|
case "AT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _as := t.Pop2().AsString(); _ak := t.Pop2().AsString()")
|
|
g.writeln("_ai := strings.Index(_as, _ak)")
|
|
g.writeln("if _ai >= 0 { t.PushInt(_ai+1) } else { t.PushInt(0) } }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "IIF":
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("if t.Pop2().AsBool() {")
|
|
g.indent++
|
|
g.emitExpr(args[1])
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.emitExpr(args[2])
|
|
g.indent--
|
|
g.writeln("}")
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Spaces is exported for use by generated code.
|
|
func init() {
|
|
// hbrtl.Spaces is available to generated code via import
|
|
}
|
|
|
|
// isGoPackage checks if a DotExpr refers to an imported Go package.
|
|
func (g *Generator) isGoPackage(dot *ast.DotExpr) bool {
|
|
if ident, ok := dot.X.(*ast.IdentExpr); ok {
|
|
pkgName := ident.Name
|
|
// Check against imported package names
|
|
for path := range g.imports {
|
|
// "database/sql" → last segment "sql"
|
|
parts := strings.Split(path, "/")
|
|
name := parts[len(parts)-1]
|
|
if alias, ok := g.importAlias[path]; ok && alias != "_" && alias != "" {
|
|
name = alias
|
|
}
|
|
if name == pkgName {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dotPkgName extracts the package identifier from a DotExpr.
|
|
func (g *Generator) dotPkgName(dot *ast.DotExpr) string {
|
|
if ident, ok := dot.X.(*ast.IdentExpr); ok {
|
|
return ident.Name
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// emitGoPackageCall generates direct Go function call with auto type bridging.
|
|
// PRG: result := sql.Open("sqlite", ":memory:")
|
|
// Go: { _r := hbrt.GoCallFunc(sql.Open, args...); t.PushValue(_r[0]) }
|
|
func (g *Generator) emitGoPackageCall(dot *ast.DotExpr, args []ast.Expr) {
|
|
pkg := g.dotPkgName(dot)
|
|
fn := dot.Member
|
|
qualName := pkg + "." + fn
|
|
regName := pkg + "_" + fn // safe Go variable name
|
|
|
|
// Register FastFunc in init block
|
|
g.goFastFuncs = append(g.goFastFuncs, goFastEntry{regName: regName, qualName: qualName})
|
|
|
|
// Build arg list
|
|
g.writeln("{")
|
|
g.indent++
|
|
|
|
argNames := make([]string, len(args))
|
|
for i, arg := range args {
|
|
argName := fmt.Sprintf("_a%d", i)
|
|
argNames[i] = argName
|
|
g.emitExpr(arg)
|
|
g.writeln(fmt.Sprintf("%s := t.Pop2()", argName))
|
|
}
|
|
|
|
argsStr := ""
|
|
for i, name := range argNames {
|
|
if i > 0 {
|
|
argsStr += ", "
|
|
}
|
|
argsStr += name
|
|
}
|
|
|
|
// Use FastPath (type-specialized, 3-11x faster than reflect)
|
|
g.writeln(fmt.Sprintf("_results := hbrt.GoCallFast(_ff_%s, %s)", regName, argsStr))
|
|
g.writeln("if len(_results) > 0 { t.PushValue(_results[0]) } else { t.PushNil() }")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
type goFastEntry struct {
|
|
regName string // Go variable: strings_ToUpper
|
|
qualName string // Go call: strings.ToUpper
|
|
}
|
|
|
|
|
|
func (g *Generator) emitBinaryOp(op token.Kind) {
|
|
switch op {
|
|
case token.PLUS:
|
|
g.writeln("t.Plus()")
|
|
case token.MINUS:
|
|
g.writeln("t.Minus()")
|
|
case token.STAR:
|
|
g.writeln("t.Mult()")
|
|
case token.SLASH:
|
|
g.writeln("t.Divide()")
|
|
case token.PERCENT:
|
|
g.writeln("t.Modulus()")
|
|
case token.POWER:
|
|
g.writeln("t.Power()")
|
|
case token.EQ, token.EXEQ:
|
|
g.writeln("t.Equal()")
|
|
case token.NEQ:
|
|
g.writeln("t.NotEqual()")
|
|
case token.LT:
|
|
g.writeln("t.Less()")
|
|
case token.GT:
|
|
g.writeln("t.Greater()")
|
|
case token.LTE:
|
|
g.writeln("t.LessEqual()")
|
|
case token.GTE:
|
|
g.writeln("t.GreaterEqual()")
|
|
case token.AND:
|
|
g.writeln("t.And()")
|
|
case token.OR:
|
|
g.writeln("t.Or()")
|
|
case token.DOLLAR:
|
|
g.writeln("t.InString()") // $ operator
|
|
// Compound assign ops (shouldn't reach here normally)
|
|
case token.PLUSEQ:
|
|
g.writeln("t.Plus()")
|
|
case token.MINUSEQ:
|
|
g.writeln("t.Minus()")
|
|
case token.STAREQ:
|
|
g.writeln("t.Mult()")
|
|
case token.SLASHEQ:
|
|
g.writeln("t.Divide()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled binary op %v", op))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitUnaryOp(op token.Kind) {
|
|
switch op {
|
|
case token.MINUS:
|
|
g.writeln("t.Negate()")
|
|
case token.NOT:
|
|
g.writeln("t.Not()")
|
|
case token.INC:
|
|
g.writeln("t.Inc()")
|
|
case token.DEC:
|
|
g.writeln("t.Dec()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled unary op %v", op))
|
|
}
|
|
}
|