Two bugs blocked Five's own inline-Go feature: 1. Inline Go blocks placed mid-file couldn't carry an `import` list because Go rejects declarations before imports in the same file. examples/godump_demo.prg and friends (real Five demos) hit "syntax error: imports must appear before other declarations" during compile of the generated Go. hoistGoImports parses the raw dump body for `import (...)` blocks and single-form `import "path"` lines, registers each path into the generator's imports map, and returns the body with those directives stripped. The top-of-file import block then carries everything the dump needs. 2. HB_FUNC() calls inside the inline block's init() enqueue registrations into hbrt.dynamicFuncs, but the VM only promotes them to its symbol table when RegisterLibModules() is called. gengo's generated main() skipped that step, so dispatch on the inline-defined names panicked with "no function symbol for call". Emit vm.RegisterLibModules() after RegisterModule(symbols). Verified: examples/godump_demo.prg builds and runs; the inline GoUpper / GoFib / GoGCD / GoSplit / GoSquare / GoTypeOf functions all dispatch. Matches the feature's original design intent. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3484 lines
97 KiB
Go
3484 lines
97 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 HB_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: "HB_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: "HB_" + 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: "HB_" + 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))
|
|
}
|
|
|
|
// negateLiteral produces a new literal that represents -lit. Handles
|
|
// INT and DOUBLE (as a textual prefix). Returns (nil, false) for
|
|
// non-numeric literals or an already-negative INT whose negation would
|
|
// overflow int64.
|
|
func negateLiteral(lit *ast.LiteralExpr) (*ast.LiteralExpr, bool) {
|
|
switch lit.Kind {
|
|
case token.INT:
|
|
n, err := strconv.ParseInt(lit.Value, 10, 64)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
// Guard: math.MinInt64 has no positive twin — let the VM's
|
|
// runtime coerce-to-double path handle it.
|
|
if n == -1<<63 {
|
|
return nil, false
|
|
}
|
|
return &ast.LiteralExpr{
|
|
ValuePos: lit.ValuePos,
|
|
Kind: token.INT,
|
|
Value: strconv.FormatInt(-n, 10),
|
|
}, true
|
|
case token.DOUBLE:
|
|
// Syntactically prefix `-` or flip an existing leading `-`.
|
|
if strings.HasPrefix(lit.Value, "-") {
|
|
return &ast.LiteralExpr{
|
|
ValuePos: lit.ValuePos,
|
|
Kind: token.DOUBLE,
|
|
Value: lit.Value[1:],
|
|
}, true
|
|
}
|
|
return &ast.LiteralExpr{
|
|
ValuePos: lit.ValuePos,
|
|
Kind: token.DOUBLE,
|
|
Value: "-" + lit.Value,
|
|
}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// foldLiteralTree recursively folds BinaryExpr subtrees into LiteralExpr
|
|
// where both operands eventually collapse to literals. Non-foldable
|
|
// subtrees come back unchanged. Used as a preorder pre-pass so the
|
|
// caller can look at a flat LITERAL + LITERAL pair.
|
|
//
|
|
// For left-associative string-concat chains like "a" + x + "b" + "c",
|
|
// the parser builds (((("a" + x) + "b") + "c")) and no pair is
|
|
// literal+literal. We reassociate: if the LHS is `Y + strlit` and the
|
|
// RHS is a string literal, rewrite as `Y + (strlit+rhslit)` so the
|
|
// tail literals collapse. Only safe for STRING+STRING (numeric `+`
|
|
// cares about types / overflow).
|
|
func foldLiteralTree(e ast.Expr) ast.Expr {
|
|
be, ok := e.(*ast.BinaryExpr)
|
|
if !ok {
|
|
return e
|
|
}
|
|
be.Left = foldLiteralTree(be.Left)
|
|
be.Right = foldLiteralTree(be.Right)
|
|
if folded, ok := tryFoldBinary(be); ok {
|
|
return folded
|
|
}
|
|
// String-concat reassociation for left-leaning chains.
|
|
if be.Op == token.PLUS {
|
|
if rLit, ok := be.Right.(*ast.LiteralExpr); ok && rLit.Kind == token.STRING {
|
|
if lBin, ok := be.Left.(*ast.BinaryExpr); ok && lBin.Op == token.PLUS {
|
|
if mLit, ok := lBin.Right.(*ast.LiteralExpr); ok && mLit.Kind == token.STRING {
|
|
fused := &ast.LiteralExpr{
|
|
ValuePos: mLit.ValuePos,
|
|
Kind: token.STRING,
|
|
Value: mLit.Value + rLit.Value,
|
|
}
|
|
return &ast.BinaryExpr{
|
|
OpPos: be.OpPos,
|
|
Op: token.PLUS,
|
|
Left: lBin.Left,
|
|
Right: fused,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return be
|
|
}
|
|
|
|
// tryFoldBinary returns a synthetic LiteralExpr when both operands of a
|
|
// BinaryExpr are themselves literals and the operator is one the
|
|
// folder recognises. INT+INT stays INT (with overflow falling through
|
|
// to the VM path), mixed numeric falls to double, STRING+STRING
|
|
// concatenates. Non-literal operands or unsupported op → (nil, false).
|
|
func tryFoldBinary(e *ast.BinaryExpr) (*ast.LiteralExpr, bool) {
|
|
l, lok := e.Left.(*ast.LiteralExpr)
|
|
r, rok := e.Right.(*ast.LiteralExpr)
|
|
if !lok || !rok {
|
|
return nil, false
|
|
}
|
|
switch e.Op {
|
|
case token.PLUS, token.MINUS, token.STAR, token.SLASH:
|
|
default:
|
|
return nil, false
|
|
}
|
|
// INT + INT — keep int exact result.
|
|
if l.Kind == token.INT && r.Kind == token.INT {
|
|
li, errL := strconv.ParseInt(l.Value, 10, 64)
|
|
ri, errR := strconv.ParseInt(r.Value, 10, 64)
|
|
if errL != nil || errR != nil {
|
|
return nil, false
|
|
}
|
|
var result int64
|
|
var overflowed bool
|
|
switch e.Op {
|
|
case token.PLUS:
|
|
result = li + ri
|
|
// Harbour overflow discipline: fall through to VM on overflow
|
|
if (ri >= 0 && result < li) || (ri < 0 && result > li) {
|
|
overflowed = true
|
|
}
|
|
case token.MINUS:
|
|
result = li - ri
|
|
if (ri <= 0 && result < li) || (ri > 0 && result > li) {
|
|
overflowed = true
|
|
}
|
|
case token.STAR:
|
|
if li == 0 || ri == 0 {
|
|
result = 0
|
|
} else {
|
|
result = li * ri
|
|
if result/li != ri {
|
|
overflowed = true
|
|
}
|
|
}
|
|
case token.SLASH:
|
|
// Harbour SLASH always yields double even for int inputs.
|
|
return nil, false
|
|
}
|
|
if overflowed {
|
|
return nil, false
|
|
}
|
|
return &ast.LiteralExpr{
|
|
ValuePos: l.ValuePos,
|
|
Kind: token.INT,
|
|
Value: strconv.FormatInt(result, 10),
|
|
}, true
|
|
}
|
|
// STRING + STRING — concatenate. Preserves the quoting style of the
|
|
// left literal so DateExpr and other quoting-sensitive kinds don't
|
|
// change shape.
|
|
if e.Op == token.PLUS && l.Kind == token.STRING && r.Kind == token.STRING {
|
|
return &ast.LiteralExpr{
|
|
ValuePos: l.ValuePos,
|
|
Kind: token.STRING,
|
|
Value: l.Value + r.Value,
|
|
}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// collectConstLocals returns a map of LOCAL names (uppercase) whose
|
|
// only assignment is a literal initializer — these can be propagated
|
|
// inline. Any reassignment, ++/--, += family, @byref, MultiAssignStmt
|
|
// target, FOR/FOREACH loop var, or AtGet target disqualifies the name.
|
|
//
|
|
// The walker is bounded: if it encounters a macro expansion or any
|
|
// AST node it doesn't recognise, it aborts and returns an empty map.
|
|
// Correctness trumps coverage — an unrecognised node might hide a
|
|
// write, so we refuse to propagate.
|
|
func collectConstLocals(fn *ast.FuncDecl) map[string]*ast.LiteralExpr {
|
|
v := &constLocalVisitor{
|
|
candidates: map[string]*ast.LiteralExpr{},
|
|
}
|
|
// Seed candidates from top-level LOCAL decls with literal init.
|
|
for _, d := range fn.Decls {
|
|
vd, ok := d.(*ast.VarDecl)
|
|
if !ok || vd.Scope != ast.ScopeLocal {
|
|
continue
|
|
}
|
|
for _, vi := range vd.Vars {
|
|
if vi.Init == nil {
|
|
continue
|
|
}
|
|
if lit, ok := vi.Init.(*ast.LiteralExpr); ok {
|
|
v.candidates[strings.ToUpper(vi.Name)] = lit
|
|
}
|
|
}
|
|
}
|
|
if len(v.candidates) == 0 {
|
|
return nil
|
|
}
|
|
// Params are writable even without explicit assignment (by-value
|
|
// but reassignable) — disqualify any candidate that shadows a param.
|
|
// Params come from a separate slot but guard in case of odd decls.
|
|
for _, p := range fn.Params {
|
|
delete(v.candidates, strings.ToUpper(p.Name))
|
|
}
|
|
for _, st := range fn.Body {
|
|
v.stmt(st)
|
|
if v.aborted {
|
|
return nil
|
|
}
|
|
}
|
|
if len(v.candidates) == 0 {
|
|
return nil
|
|
}
|
|
return v.candidates
|
|
}
|
|
|
|
type constLocalVisitor struct {
|
|
candidates map[string]*ast.LiteralExpr
|
|
aborted bool
|
|
}
|
|
|
|
func (v *constLocalVisitor) abort() {
|
|
v.aborted = true
|
|
v.candidates = nil
|
|
}
|
|
|
|
func (v *constLocalVisitor) writeIdent(e ast.Expr) {
|
|
if id, ok := e.(*ast.IdentExpr); ok {
|
|
delete(v.candidates, strings.ToUpper(id.Name))
|
|
}
|
|
}
|
|
|
|
func (v *constLocalVisitor) writeName(name string) {
|
|
delete(v.candidates, strings.ToUpper(name))
|
|
}
|
|
|
|
func (v *constLocalVisitor) exprs(es []ast.Expr) {
|
|
for _, e := range es {
|
|
v.expr(e)
|
|
}
|
|
}
|
|
|
|
func (v *constLocalVisitor) stmts(ss []ast.Stmt) {
|
|
for _, s := range ss {
|
|
v.stmt(s)
|
|
}
|
|
}
|
|
|
|
func (v *constLocalVisitor) expr(e ast.Expr) {
|
|
if v.aborted || e == nil {
|
|
return
|
|
}
|
|
switch x := e.(type) {
|
|
case *ast.LiteralExpr, *ast.IdentExpr, *ast.SelfExpr:
|
|
// leaf; reads don't disqualify
|
|
case *ast.BinaryExpr:
|
|
v.expr(x.Left)
|
|
v.expr(x.Right)
|
|
case *ast.UnaryExpr:
|
|
if x.Op == token.INC || x.Op == token.DEC {
|
|
v.writeIdent(x.X)
|
|
}
|
|
v.expr(x.X)
|
|
case *ast.PostfixExpr:
|
|
v.writeIdent(x.X)
|
|
v.expr(x.X)
|
|
case *ast.AssignExpr:
|
|
// All assign ops (:= += -= *= /= %= ^=) are writes to Left's
|
|
// outer ident. Compound assigns also read, but disqualification
|
|
// is based on being written at all.
|
|
v.writeIdent(x.Left)
|
|
// Still walk Left in case of indexing: arr[i] := v — the ident
|
|
// arr is read (and we don't want to accidentally treat it as a
|
|
// write since writeIdent only triggers on a bare IdentExpr).
|
|
if _, isIdent := x.Left.(*ast.IdentExpr); !isIdent {
|
|
v.expr(x.Left)
|
|
}
|
|
v.expr(x.Right)
|
|
case *ast.CallExpr:
|
|
v.expr(x.Func)
|
|
v.exprs(x.Args)
|
|
case *ast.DotExpr:
|
|
v.expr(x.X)
|
|
case *ast.SendExpr:
|
|
v.expr(x.Object)
|
|
if x.MacroMethod != nil {
|
|
v.expr(x.MacroMethod)
|
|
}
|
|
v.exprs(x.Args)
|
|
case *ast.IndexExpr:
|
|
v.expr(x.X)
|
|
v.expr(x.Index)
|
|
case *ast.AliasExpr:
|
|
v.expr(x.Alias)
|
|
v.expr(x.Field)
|
|
case *ast.MacroExpr:
|
|
// Macros can expand to any name including writes. Bail.
|
|
v.abort()
|
|
case *ast.BlockExpr:
|
|
v.expr(x.Body)
|
|
case *ast.ArrayLitExpr:
|
|
v.exprs(x.Items)
|
|
case *ast.HashLitExpr:
|
|
v.exprs(x.Keys)
|
|
v.exprs(x.Values)
|
|
case *ast.IIfExpr:
|
|
v.expr(x.Cond)
|
|
v.expr(x.True)
|
|
v.expr(x.False)
|
|
case *ast.RefExpr:
|
|
// @ident — passes by reference; callee may mutate.
|
|
v.writeIdent(x.X)
|
|
v.expr(x.X)
|
|
case *ast.SliceExpr:
|
|
v.expr(x.X)
|
|
v.expr(x.Low)
|
|
v.expr(x.High)
|
|
case *ast.NilSafeExpr:
|
|
v.expr(x.X)
|
|
case *ast.InterpolatedString:
|
|
v.exprs(x.Parts)
|
|
default:
|
|
v.abort()
|
|
}
|
|
}
|
|
|
|
func (v *constLocalVisitor) stmt(s ast.Stmt) {
|
|
if v.aborted || s == nil {
|
|
return
|
|
}
|
|
switch x := s.(type) {
|
|
case *ast.ExprStmt:
|
|
v.expr(x.X)
|
|
case *ast.ReturnStmt:
|
|
v.expr(x.Value)
|
|
case *ast.QOutStmt:
|
|
v.exprs(x.Exprs)
|
|
case *ast.IfStmt:
|
|
v.expr(x.Cond)
|
|
v.stmts(x.Body)
|
|
for _, ei := range x.ElseIfs {
|
|
v.expr(ei.Cond)
|
|
v.stmts(ei.Body)
|
|
}
|
|
v.stmts(x.ElseBody)
|
|
case *ast.DoWhileStmt:
|
|
v.expr(x.Cond)
|
|
v.stmts(x.Body)
|
|
case *ast.ForStmt:
|
|
v.writeName(x.Var)
|
|
v.expr(x.Start)
|
|
v.expr(x.To)
|
|
v.expr(x.Step)
|
|
v.stmts(x.Body)
|
|
case *ast.ForEachStmt:
|
|
v.writeName(x.Var)
|
|
v.expr(x.Collection)
|
|
v.stmts(x.Body)
|
|
case *ast.SwitchStmt:
|
|
v.expr(x.Expr)
|
|
for _, c := range x.Cases {
|
|
v.expr(c.Value)
|
|
v.stmts(c.Body)
|
|
}
|
|
v.stmts(x.Otherwise)
|
|
case *ast.SeqStmt:
|
|
v.stmts(x.Body)
|
|
if x.RecoverVar != "" {
|
|
v.writeName(x.RecoverVar)
|
|
}
|
|
v.stmts(x.RecoverBody)
|
|
case *ast.MultiAssignStmt:
|
|
for _, t := range x.Targets {
|
|
v.writeName(t)
|
|
}
|
|
v.exprs(x.Values)
|
|
case *ast.VarDecl:
|
|
// Init exprs are reads. The LOCAL name itself was already
|
|
// collected as a candidate by collectConstLocals; we don't
|
|
// treat its own init as a reassignment.
|
|
for _, vi := range x.Vars {
|
|
v.expr(vi.Init)
|
|
}
|
|
case *ast.DeferStmt:
|
|
v.expr(x.Call)
|
|
case *ast.ExitStmt, *ast.LoopStmt:
|
|
// no expression
|
|
case *ast.SkipCmd:
|
|
v.expr(x.Count)
|
|
case *ast.GoCmd:
|
|
v.expr(x.RecNo)
|
|
case *ast.SeekCmd:
|
|
v.expr(x.Key)
|
|
case *ast.UseCmd:
|
|
v.expr(x.File)
|
|
v.expr(x.AliasExpr)
|
|
case *ast.SelectCmd:
|
|
v.expr(x.Area)
|
|
case *ast.ReplaceCmd:
|
|
for _, f := range x.Fields {
|
|
v.expr(f.Field)
|
|
v.expr(f.Value)
|
|
}
|
|
case *ast.AppendCmd, *ast.DeleteCmd, *ast.ReadCmd:
|
|
// no expressions
|
|
case *ast.IndexCmd:
|
|
v.expr(x.KeyExpr)
|
|
v.expr(x.File)
|
|
v.expr(x.ForCond)
|
|
case *ast.SetCmd:
|
|
v.expr(x.Expr)
|
|
case *ast.AtSayCmd:
|
|
v.expr(x.Row)
|
|
v.expr(x.Col)
|
|
v.expr(x.SayExpr)
|
|
v.expr(x.Picture)
|
|
case *ast.AtGetCmd:
|
|
// @ GET var writes to Var at READ time.
|
|
v.writeIdent(x.Var)
|
|
if x.VarName != "" {
|
|
v.writeName(x.VarName)
|
|
}
|
|
v.expr(x.Row)
|
|
v.expr(x.Col)
|
|
v.expr(x.Picture)
|
|
v.expr(x.Valid)
|
|
v.expr(x.When)
|
|
case *ast.AtSayGetCmd:
|
|
v.writeIdent(x.Var)
|
|
if x.VarName != "" {
|
|
v.writeName(x.VarName)
|
|
}
|
|
v.expr(x.Row)
|
|
v.expr(x.Col)
|
|
v.expr(x.SayExpr)
|
|
v.expr(x.Picture)
|
|
v.expr(x.Valid)
|
|
v.expr(x.When)
|
|
default:
|
|
v.abort()
|
|
}
|
|
}
|
|
|
|
// 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 := "HB_" + 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)
|
|
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)
|
|
}
|
|
}
|
|
// Count mid-function LOCAL declarations in Body
|
|
for _, s := range fn.Body {
|
|
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
nLocals += len(vd.Vars)
|
|
}
|
|
}
|
|
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)
|
|
// 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
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Statement emission ---
|
|
|
|
func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
|
|
// Emit debug line hook
|
|
if g.Debug && stmt.Pos().Line > 0 {
|
|
g.writeln(fmt.Sprintf("t.DebugLine(%q, %d)", g.file.Name, stmt.Pos().Line))
|
|
}
|
|
|
|
switch s := stmt.(type) {
|
|
case *ast.ReturnStmt:
|
|
if len(s.Values) > 1 {
|
|
// Multi-return: RETURN a, b, c → push array of values
|
|
for _, v := range s.Values {
|
|
g.emitExpr(v)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(s.Values)))
|
|
g.writeln("t.RetValue()")
|
|
} else if s.Value != nil {
|
|
g.emitExpr(s.Value)
|
|
g.writeln("t.RetValue()")
|
|
} else {
|
|
g.writeln("t.RetNil()")
|
|
}
|
|
g.writeln("return") // Go return to exit function immediately
|
|
|
|
case *ast.QOutStmt:
|
|
g.emitQOut(s, locals)
|
|
|
|
case *ast.ExprStmt:
|
|
g.emitExprStmt(s, locals)
|
|
|
|
case *ast.IfStmt:
|
|
g.emitIf(s, locals)
|
|
|
|
case *ast.SwitchStmt:
|
|
g.emitSwitch(s, locals)
|
|
|
|
case *ast.DoWhileStmt:
|
|
g.emitDoWhile(s, locals)
|
|
|
|
case *ast.ForStmt:
|
|
g.emitFor(s, locals)
|
|
|
|
case *ast.ForEachStmt:
|
|
g.emitForEach(s, locals)
|
|
|
|
case *ast.ExitStmt:
|
|
g.writeln("break")
|
|
|
|
case *ast.LoopStmt:
|
|
if g.curForLabel != "" {
|
|
// Inside FOR..NEXT: goto label before increment (continue would skip it)
|
|
g.writeln("goto " + g.curForLabel)
|
|
} else {
|
|
g.writeln("continue")
|
|
}
|
|
|
|
case *ast.MultiAssignStmt:
|
|
g.emitMultiAssign(s, locals)
|
|
|
|
case *ast.DeferStmt:
|
|
g.emitDefer(s, locals)
|
|
|
|
case *ast.VarDecl:
|
|
// LOCAL in mid-function or PRIVATE/PUBLIC
|
|
g.emitMidVarDecl(s, locals)
|
|
|
|
// xBase commands — generate calls to hbrdd WorkAreaManager
|
|
case *ast.UseCmd:
|
|
g.emitUseCmd(s, locals)
|
|
case *ast.GoCmd:
|
|
g.emitGoCmd(s)
|
|
case *ast.SkipCmd:
|
|
g.emitSkipCmd(s, locals)
|
|
case *ast.SeekCmd:
|
|
g.emitSeekCmd(s, locals)
|
|
case *ast.ReplaceCmd:
|
|
g.emitReplaceCmd(s, locals)
|
|
case *ast.AppendCmd:
|
|
if g.hoistedFields != nil {
|
|
// Use hoisted area variable
|
|
g.writeln("if _rarea != nil { _rarea.Append() }")
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := _wa.Current(); _area != nil { _area.Append() } }")
|
|
}
|
|
case *ast.DeleteCmd:
|
|
if g.hoistedDW || g.hoistedFields != nil {
|
|
g.writeln(fmt.Sprintf("if %s != nil { %s.Delete() }", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := _wa.Current(); _area != nil { _area.Delete() } }")
|
|
}
|
|
case *ast.SelectCmd:
|
|
g.emitExpr(s.Area)
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); _v := t.Pop2()")
|
|
g.writeln("if _v.IsNumeric() { _wa.Select(int(_v.AsNumInt())) } else { _wa.Select(_v.AsString()) } }")
|
|
case *ast.IndexCmd:
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if area := wa.Current(); area != nil {")
|
|
g.indent++
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
keyStr := exprToString(s.KeyExpr)
|
|
g.writeln(fmt.Sprintf("_keyExpr := %q", keyStr))
|
|
|
|
// File expression: if it contains a function call, evaluate at
|
|
// runtime — Harbour `INDEX ON ... TO ( cExpr )` semantics. Prior
|
|
// behavior was static exprToString which serialized calls like
|
|
// `Lower(cTable) + "_pk.ntx"` into the literal filename string.
|
|
// Detect via containsCall; preserve static path for simple
|
|
// `test.ntx` style identifiers.
|
|
if containsCall(s.File) {
|
|
g.emitExpr(s.File)
|
|
g.writeln("_file := t.Pop2().AsString()")
|
|
} else {
|
|
fileStr := exprToString(s.File)
|
|
g.writeln(fmt.Sprintf("_file := %q", fileStr))
|
|
}
|
|
forExpr := `""`
|
|
if s.ForCond != nil {
|
|
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
|
|
}
|
|
|
|
// Emit compiled key evaluator as Go closure.
|
|
// This inlines the AST of the key expression into native Go code,
|
|
// eliminating per-record MacroEval string parsing + symbol lookup.
|
|
// In INDEX context, bare identifiers are FIELD names (not locals).
|
|
g.writeln("_keyFunc := func() hbrt.Value {")
|
|
g.indent++
|
|
g.emitIndexKeyExpr(s.KeyExpr)
|
|
g.writeln("return t.Pop2()")
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
// Still set MacroEval fallback for evalKeyExprInner (used for keyLen sampling)
|
|
g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }")
|
|
g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, TagName: %q, Unique: %v, Descending: %v, KeyFunc: _keyFunc})",
|
|
forExpr, s.TagName, s.Unique, s.Descending))
|
|
g.writeln("dbf.KeyEvalFunc = nil")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
case *ast.SetCmd:
|
|
upper := strings.ToUpper(s.Setting)
|
|
|
|
// Boolean SET toggles — call RTL Set function, no workarea needed
|
|
setFuncMap := map[string]string{
|
|
"DELETED": "SETDELETED",
|
|
"EXACT": "SETEXACT",
|
|
"SOFTSEEK": "SETSOFTSEEK",
|
|
"EXCLUSIVE": "SETEXCLUSIVE",
|
|
"FIXED": "SETFIXED",
|
|
"CANCEL": "SETCANCEL",
|
|
"BELL": "SETBELL",
|
|
"CONFIRM": "SETCONFIRM",
|
|
"INSERT": "SETINSERT",
|
|
"ESCAPE": "SETESCAPE",
|
|
"WRAP": "SETWRAP",
|
|
}
|
|
if funcName, ok := setFuncMap[upper]; ok {
|
|
onOff := strings.ToUpper(s.Extra)
|
|
if onOff == "ON" || onOff == "OFF" {
|
|
val := "true"
|
|
if onOff == "OFF" {
|
|
val = "false"
|
|
}
|
|
g.emitPushSymbol(funcName)
|
|
g.writeln("t.PushNil()")
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
|
|
g.writeln("t.Do(1)")
|
|
}
|
|
break
|
|
}
|
|
|
|
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
|
|
valueFuncMap := map[string]string{
|
|
"DATE": "__SETDATEFORMAT",
|
|
"DECIMALS": "SETDECIMALS",
|
|
"EPOCH": "SETEPOCH",
|
|
}
|
|
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
|
|
g.emitPushSymbol(funcName)
|
|
g.writeln("t.PushNil()")
|
|
g.emitExpr(s.Expr)
|
|
g.writeln("t.Do(1)")
|
|
break
|
|
}
|
|
|
|
// Workarea-specific SET commands
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if area := wa.Current(); area != nil {")
|
|
g.indent++
|
|
switch upper {
|
|
case "FILTER":
|
|
if s.Expr != nil {
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
|
|
} else {
|
|
g.writeln("area.ClearFilter()")
|
|
}
|
|
case "ORDER":
|
|
if s.Expr != nil {
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`{ _ov := t.Pop2(); var _os string; if _ov.IsNumeric() { _os = hbrt.NtoS(_ov.AsNumInt()) } else { _os = _ov.AsString() }; idx.OrderListFocus(_os) }`)
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
case "INDEX":
|
|
if s.Expr != nil {
|
|
fileStr := exprToString(s.Expr)
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
if fileStr != "" {
|
|
// SET INDEX TO a, b, c — split comma-separated file names
|
|
// and call OrderListAdd for each. Harbour loads all NTX
|
|
// files into the active index list.
|
|
clean := fileStr
|
|
if len(clean) >= 2 && clean[0] == '"' && clean[len(clean)-1] == '"' {
|
|
clean = clean[1 : len(clean)-1]
|
|
}
|
|
parts := strings.Split(clean, ",")
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, p))
|
|
}
|
|
}
|
|
} else {
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok { idx.OrderListClear() }")
|
|
}
|
|
default:
|
|
g.writeln(fmt.Sprintf("// SET %s: not yet implemented", upper))
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
case *ast.SeqStmt:
|
|
g.emitBeginSequence(s, locals)
|
|
|
|
case *ast.AtSayCmd:
|
|
g.emitAtSayCmd(s)
|
|
case *ast.AtGetCmd:
|
|
g.emitAtGetCmd(s, locals)
|
|
case *ast.AtSayGetCmd:
|
|
g.emitAtSayGetCmd(s, locals)
|
|
case *ast.ReadCmd:
|
|
g.emitReadCmd(s, locals)
|
|
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled statement type %T — skipped", stmt))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
|
|
for _, v := range s.Vars {
|
|
idx, found := locals[strings.ToUpper(v.Name)]
|
|
if !found {
|
|
maxIdx := 0
|
|
for _, i := range locals {
|
|
if i > maxIdx {
|
|
maxIdx = i
|
|
}
|
|
}
|
|
idx = maxIdx + 1
|
|
locals[strings.ToUpper(v.Name)] = idx
|
|
}
|
|
if v.Init != nil {
|
|
if _, isConst := g.constLocals[strings.ToUpper(v.Name)]; !isConst {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitQOut(s *ast.QOutStmt, locals localMap) {
|
|
sym := "QOUT"
|
|
if s.IsQQ {
|
|
sym = "QQOUT"
|
|
}
|
|
g.emitPushSymbol(sym)
|
|
g.writeln("t.PushNil()")
|
|
for _, expr := range s.Exprs {
|
|
g.emitExpr(expr)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Function(%d)", len(s.Exprs)))
|
|
}
|
|
|
|
func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
|
|
// Check if it's an assignment
|
|
if assign, ok := s.X.(*ast.AssignExpr); ok {
|
|
g.emitAssign(assign, locals)
|
|
return
|
|
}
|
|
// Check if it's a function call (discard result)
|
|
if call, ok := s.X.(*ast.CallExpr); ok {
|
|
g.emitCallAsStmt(call, locals)
|
|
return
|
|
}
|
|
// Bare identifier as statement (e.g., CLS, CLEAR) — treat as zero-arg function call
|
|
if ident, ok := s.X.(*ast.IdentExpr); ok {
|
|
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
|
|
g.emitPushSymbol(strings.ToUpper(ident.Name))
|
|
g.writeln("t.PushNil()")
|
|
g.writeln("t.Do(0)")
|
|
return
|
|
}
|
|
}
|
|
// Postfix ++/--
|
|
if pf, ok := s.X.(*ast.PostfixExpr); ok {
|
|
// Local variable: n++
|
|
if ident, ok := pf.X.(*ast.IdentExpr); ok {
|
|
upper := strings.ToUpper(ident.Name)
|
|
if idx, found := locals[upper]; found {
|
|
if pf.Op == token.INC {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, -1)", idx))
|
|
}
|
|
return
|
|
}
|
|
// STATIC variable: s_nPass++
|
|
if goVar, found := g.staticVars[upper]; found {
|
|
delta := "1"
|
|
if pf.Op == token.DEC {
|
|
delta = "-1"
|
|
}
|
|
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
|
|
return
|
|
}
|
|
}
|
|
// Self field: ::field++
|
|
if send, ok := pf.X.(*ast.SendExpr); ok {
|
|
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
|
|
fieldName := strings.ToUpper(send.Method)
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
if pf.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
|
|
}
|
|
}
|
|
}
|
|
// General expression (result on stack, pop it)
|
|
g.emitExpr(s.X)
|
|
g.writeln("t.Pop()")
|
|
}
|
|
|
|
func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
|
|
// Check for arr[idx] := value (array index assignment)
|
|
if idx, ok := a.Left.(*ast.IndexExpr); ok {
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(idx.X) // array
|
|
g.emitExpr(idx.Index) // index
|
|
g.emitExpr(a.Right) // value
|
|
g.writeln("t.ArrayPop()") // set array[index] = value
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check for obj:field := value (object field assignment)
|
|
if send, ok := a.Left.(*ast.SendExpr); ok {
|
|
_, isSelf := send.Object.(*ast.SelfExpr)
|
|
|
|
if isSelf {
|
|
fieldName := strings.ToUpper(send.Method)
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.PLUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Plus()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.MINUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Minus()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.STAREQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Mult()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.SLASHEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Divide()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
default:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Non-self: obj:field := value → obj:_FIELD(value)
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(send.Object)
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.Send(%q, 1)", "_"+strings.ToUpper(send.Method)))
|
|
g.writeln("t.Pop() // discard setter result")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check for alias->field := value (FIELD->NAME := value)
|
|
if aliasExpr, ok := a.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)
|
|
// `M->name := v` / `MEMVAR->name := v` are memvar writes,
|
|
// not workarea field writes.
|
|
if upper == "M" || upper == "MEMVAR" {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if ident, ok := a.Left.(*ast.IdentExpr); ok {
|
|
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
// Peephole: `x := x + <expr>` / `x := x - <expr>` →
|
|
// LocalAdd. Same result as `x += <expr>` but lets the
|
|
// PRG side use the explicit form without penalty.
|
|
if be, ok := a.Right.(*ast.BinaryExpr); ok &&
|
|
(be.Op == token.PLUS || be.Op == token.MINUS) {
|
|
if lid, isIdent := be.Left.(*ast.IdentExpr); isIdent {
|
|
if selfIdx, found := locals[strings.ToUpper(lid.Name)]; found && selfIdx == idx {
|
|
g.emitExpr(be.Right)
|
|
if be.Op == token.MINUS {
|
|
g.writeln("t.Negate()")
|
|
}
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
case token.PLUSEQ:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
case token.MINUSEQ:
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Negate()")
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
default:
|
|
// General compound: push local, push right, op, pop local
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
return
|
|
}
|
|
// Check module-level or function-level STATIC variable
|
|
upper := strings.ToUpper(ident.Name)
|
|
if goVar, found := g.staticVars[upper]; found {
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
case token.PLUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Plus()")
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
case token.MINUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Minus()")
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
case token.STAREQ:
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Mult()")
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
case token.SLASHEQ:
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Divide()")
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// Fallback: general assignment via stack
|
|
g.emitExpr(a.Right)
|
|
g.writeln("// WARN: complex assignment target — simplified")
|
|
g.writeln("t.Pop()")
|
|
}
|
|
|
|
func (g *Generator) emitCallAsStmt(call *ast.CallExpr, locals localMap) {
|
|
if ident, ok := call.Func.(*ast.IdentExpr); ok {
|
|
g.emitPushSymbol(strings.ToUpper(ident.Name))
|
|
} else {
|
|
g.emitExpr(call.Func)
|
|
}
|
|
g.writeln("t.PushNil()")
|
|
for _, arg := range call.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Do(%d)", len(call.Args)))
|
|
}
|
|
|
|
// boolLiteralValue returns (value, true) if e reduces to a .T./.F.
|
|
// literal at compile time. Sees through an outer `.NOT.` so expressions
|
|
// like `!.F.` also collapse. Used by emitIf to skip dead branches and
|
|
// by the AND/OR short-circuit emitter.
|
|
func boolLiteralValue(e ast.Expr) (bool, bool) {
|
|
if u, ok := e.(*ast.UnaryExpr); ok && u.Op == token.NOT {
|
|
if v, ok := boolLiteralValue(u.X); ok {
|
|
return !v, true
|
|
}
|
|
return false, false
|
|
}
|
|
lit, ok := e.(*ast.LiteralExpr)
|
|
if !ok {
|
|
return false, false
|
|
}
|
|
switch lit.Kind {
|
|
case token.TRUE:
|
|
return true, true
|
|
case token.FALSE:
|
|
return false, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func (g *Generator) emitIf(s *ast.IfStmt, locals localMap) {
|
|
// Dead-branch elimination for literal conditions. An IF .T. collapses
|
|
// to its body; an IF .F. collapses to its first live ELSEIF/ELSE.
|
|
// We resolve the main Cond here and recurse on the remainder if it
|
|
// turns into a new IF chain.
|
|
if v, ok := boolLiteralValue(s.Cond); ok {
|
|
if v {
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
return
|
|
}
|
|
// IF .F. — scan ElseIfs for first non-.F. branch.
|
|
for i, ei := range s.ElseIfs {
|
|
if v2, ok2 := boolLiteralValue(ei.Cond); ok2 {
|
|
if v2 {
|
|
for _, stmt := range ei.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
return
|
|
}
|
|
continue // ELSEIF .F. — dead, skip
|
|
}
|
|
// Non-literal ELSEIF becomes the new IF head.
|
|
newIf := &ast.IfStmt{
|
|
IfPos: ei.ElseIfPos,
|
|
Cond: ei.Cond,
|
|
Body: ei.Body,
|
|
ElseIfs: s.ElseIfs[i+1:],
|
|
ElseBody: s.ElseBody,
|
|
}
|
|
g.emitIf(newIf, locals)
|
|
return
|
|
}
|
|
// All ElseIfs were .F. — only ELSE body remains.
|
|
for _, stmt := range s.ElseBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Main cond is dynamic. Still filter dead ELSEIFs (.F. removed;
|
|
// an ELSEIF .T. truncates the chain and becomes the ELSE).
|
|
elseIfs := s.ElseIfs
|
|
elseBody := s.ElseBody
|
|
if len(elseIfs) > 0 {
|
|
filtered := make([]*ast.ElseIfClause, 0, len(elseIfs))
|
|
for _, ei := range elseIfs {
|
|
if v, ok := boolLiteralValue(ei.Cond); ok {
|
|
if v {
|
|
// ELSEIF .T. — chain stops here; body becomes ELSE.
|
|
elseBody = ei.Body
|
|
elseIfs = filtered
|
|
goto emit
|
|
}
|
|
continue // ELSEIF .F. — dead
|
|
}
|
|
filtered = append(filtered, ei)
|
|
}
|
|
elseIfs = filtered
|
|
}
|
|
|
|
emit:
|
|
g.emitExpr(s.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
|
|
for _, ei := range elseIfs {
|
|
g.writeIndent()
|
|
g.write("} else {\n")
|
|
g.indent++
|
|
g.emitExpr(ei.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
for _, stmt := range ei.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
|
|
if len(elseBody) > 0 {
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
for _, stmt := range elseBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
|
|
g.writeln("}")
|
|
|
|
// Close nested elseif braces
|
|
for range elseIfs {
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitDoWhile(s *ast.DoWhileStmt, locals localMap) {
|
|
// DO WHILE .F. — body is unreachable; emit nothing.
|
|
if v, ok := boolLiteralValue(s.Cond); ok && !v {
|
|
return
|
|
}
|
|
|
|
// Detect RDD commands in body for WA hoisting
|
|
hasRDD := hasRDDCommands(s.Body)
|
|
safeToHoist := hasRDD && !hasWorkareaChange(s.Body)
|
|
|
|
if safeToHoist && g.hoistedFields == nil {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_dwa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("_darea := _dwa.Current()")
|
|
g.hoistedDW = true
|
|
}
|
|
|
|
g.writeln("for {")
|
|
g.indent++
|
|
// DO WHILE .T. — the idiomatic infinite loop. Skip the per-iteration
|
|
// PushBool/PopLogical; exit only through EXIT / LOOP / RETURN.
|
|
if v, ok := boolLiteralValue(s.Cond); !ok || !v {
|
|
g.emitExpr(s.Cond)
|
|
g.writeln("if !t.PopLogical() { break }")
|
|
}
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
if safeToHoist && g.hoistedDW {
|
|
g.hoistedDW = false
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
// hasRDDCommands checks if any statement is an RDD operation.
|
|
func hasRDDCommands(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
switch s.(type) {
|
|
case *ast.SkipCmd, *ast.GoCmd, *ast.SeekCmd,
|
|
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasWorkareaChange checks for USE/SELECT that would invalidate cached area.
|
|
func hasWorkareaChange(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.UseCmd, *ast.SelectCmd:
|
|
return true
|
|
case *ast.IfStmt:
|
|
if hasWorkareaChange(v.Body) || hasWorkareaChange(v.ElseBody) {
|
|
return true
|
|
}
|
|
case *ast.DoWhileStmt:
|
|
if hasWorkareaChange(v.Body) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// collectSymbols scans AST for all symbol names referenced by function calls.
|
|
// Returns unique names for hoisting FindSymbol to function prologue.
|
|
func collectSymbols(stmts []ast.Stmt) []string {
|
|
seen := map[string]bool{}
|
|
var names []string
|
|
var walk func([]ast.Stmt)
|
|
var walkExpr func(ast.Expr)
|
|
|
|
walkExpr = func(e ast.Expr) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
switch v := e.(type) {
|
|
case *ast.CallExpr:
|
|
if ident, ok := v.Func.(*ast.IdentExpr); ok {
|
|
name := strings.ToUpper(ident.Name)
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
for _, a := range v.Args {
|
|
walkExpr(a)
|
|
}
|
|
case *ast.BinaryExpr:
|
|
walkExpr(v.Left)
|
|
walkExpr(v.Right)
|
|
case *ast.UnaryExpr:
|
|
walkExpr(v.X)
|
|
}
|
|
}
|
|
|
|
walk = func(stmts []ast.Stmt) {
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.ExprStmt:
|
|
walkExpr(v.X)
|
|
case *ast.ReturnStmt:
|
|
if v.Value != nil {
|
|
walkExpr(v.Value)
|
|
}
|
|
case *ast.IfStmt:
|
|
walkExpr(v.Cond)
|
|
walk(v.Body)
|
|
walk(v.ElseBody)
|
|
case *ast.ForStmt:
|
|
walk(v.Body)
|
|
case *ast.ForEachStmt:
|
|
walk(v.Body)
|
|
case *ast.DoWhileStmt:
|
|
walkExpr(v.Cond)
|
|
walk(v.Body)
|
|
case *ast.SeqStmt:
|
|
walk(v.Body)
|
|
walk(v.RecoverBody)
|
|
case *ast.SwitchStmt:
|
|
for _, c := range v.Cases {
|
|
walk(c.Body)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(stmts)
|
|
return names
|
|
}
|
|
|
|
// collectReplaceFields scans statements for REPLACE field names.
|
|
// Returns nil if unsafe to hoist (USE/SELECT/CLOSE found).
|
|
func collectReplaceFields(stmts []ast.Stmt) []string {
|
|
seen := map[string]bool{}
|
|
var fields []string
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.ReplaceCmd:
|
|
for _, rf := range v.Fields {
|
|
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
|
|
name := ident.Name
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
fields = append(fields, name)
|
|
}
|
|
}
|
|
}
|
|
case *ast.UseCmd, *ast.SelectCmd:
|
|
return nil // workarea may change — unsafe to hoist
|
|
case *ast.IfStmt:
|
|
// Check nested blocks
|
|
if sub := collectReplaceFields(v.Body); sub == nil {
|
|
return nil
|
|
}
|
|
if sub := collectReplaceFields(v.ElseBody); sub == nil {
|
|
return nil
|
|
}
|
|
case *ast.DoWhileStmt:
|
|
if sub := collectReplaceFields(v.Body); sub == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// hasAppendInBody checks if any APPEND command exists in the statements.
|
|
func hasAppendInBody(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
if _, ok := s.(*ast.AppendCmd); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
|
|
idx, found := locals[strings.ToUpper(s.Var)]
|
|
if !found {
|
|
g.writeln("// ERROR: FOR variable not found in locals")
|
|
return
|
|
}
|
|
|
|
// i := start
|
|
g.emitExpr(s.Start)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
|
|
// Detect step direction for comparison
|
|
isNegStep := false
|
|
if s.Step != nil {
|
|
if lit, ok := s.Step.(*ast.LiteralExpr); ok {
|
|
if lit.Kind == token.INT && len(lit.Value) > 0 && lit.Value[0] == '-' {
|
|
isNegStep = true
|
|
}
|
|
}
|
|
if un, ok := s.Step.(*ast.UnaryExpr); ok && un.Op == token.MINUS {
|
|
isNegStep = true
|
|
}
|
|
}
|
|
|
|
// Optimization: hoist WA/FieldIndex lookups outside FOR loop
|
|
// if body contains REPLACE and no USE/SELECT (safe to cache).
|
|
rddFields := collectReplaceFields(s.Body)
|
|
hoistRDD := len(rddFields) > 0 && hasAppendInBody(s.Body)
|
|
|
|
if hoistRDD {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_rwa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("_rarea := _rwa.Current()")
|
|
g.writeln("var _rdbf *dbf.DBFArea")
|
|
g.writeln("if _rarea != nil { _rdbf, _ = _rarea.(*dbf.DBFArea) }")
|
|
// Pre-compute field indexes
|
|
for i, fname := range rddFields {
|
|
g.writeln(fmt.Sprintf("var _rfi%d int = -1", i))
|
|
g.writeln(fmt.Sprintf("if _rdbf != nil { _rfi%d = _rdbf.FieldIndex(%q) }", i, fname))
|
|
}
|
|
g.hoistedFields = rddFields // store for emitReplaceCmdHoisted
|
|
}
|
|
|
|
g.writeln("for {")
|
|
g.indent++
|
|
|
|
// Comparison: fused opcode when limit is literal int (most common).
|
|
// Also see through const-propagated LOCALs: `LOCAL n := 100; FOR i := 1
|
|
// TO n` should hit the same fast path as a bare literal.
|
|
toLit, _ := s.To.(*ast.LiteralExpr)
|
|
if toLit == nil {
|
|
if id, ok := s.To.(*ast.IdentExpr); ok {
|
|
if l, ok2 := g.constLocals[strings.ToUpper(id.Name)]; ok2 {
|
|
toLit = l
|
|
}
|
|
}
|
|
}
|
|
if lit := toLit; lit != nil && lit.Kind == token.INT {
|
|
if isNegStep {
|
|
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("if !t.LocalLessEqualInt(%d, %s) { break }", idx, lit.Value))
|
|
}
|
|
} else {
|
|
// General case: stack-based comparison
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(s.To)
|
|
if isNegStep {
|
|
g.writeln("t.GreaterEqual()")
|
|
} else {
|
|
g.writeln("t.LessEqual()")
|
|
}
|
|
g.writeln("if !t.PopLogical() { break }")
|
|
}
|
|
|
|
// Track FOR loop depth so LOOP can use goto instead of continue.
|
|
// Only emit label if LOOP is present in the body (Go rejects unused labels).
|
|
hasLoop := bodyHasLoop(s.Body)
|
|
forLabel := ""
|
|
prevForLabel := g.curForLabel
|
|
if hasLoop {
|
|
forLabel = fmt.Sprintf("_for_next_%d", g.forLabelSeq)
|
|
g.forLabelSeq++
|
|
g.curForLabel = forLabel
|
|
} else {
|
|
g.curForLabel = ""
|
|
}
|
|
|
|
// body
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
// Label for LOOP to jump to (skipping continue which would miss increment)
|
|
if hasLoop {
|
|
g.writeln(forLabel + ":")
|
|
}
|
|
|
|
// i += step (default 1)
|
|
if s.Step != nil {
|
|
g.emitExpr(s.Step)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
|
|
}
|
|
|
|
g.curForLabel = prevForLabel
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
// Close hoisting block
|
|
if hoistRDD {
|
|
g.hoistedFields = nil
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
// bodyHasLoop checks if any statement in the body is a LOOP.
|
|
// Only checks the immediate level — LOOP inside nested FOR/DO WHILE is irrelevant.
|
|
func bodyHasLoop(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
if hasLoopStmt(s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasLoopStmt(s ast.Stmt) bool {
|
|
switch s := s.(type) {
|
|
case *ast.LoopStmt:
|
|
return true
|
|
case *ast.IfStmt:
|
|
for _, st := range s.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
for _, ei := range s.ElseIfs {
|
|
for _, st := range ei.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for _, st := range s.ElseBody {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
case *ast.SeqStmt:
|
|
for _, st := range s.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
for _, st := range s.RecoverBody {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
case *ast.SwitchStmt:
|
|
for _, c := range s.Cases {
|
|
for _, st := range c.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for _, st := range s.Otherwise {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
// Do NOT recurse into ForStmt/DoWhileStmt — nested LOOP is for the inner loop
|
|
}
|
|
return false
|
|
}
|
|
|
|
|
|
func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
|
|
// Wrap the whole thing in a one-iteration `for` so:
|
|
// 1. `_sw` stays scoped to the switch.
|
|
// 2. `EXIT` inside a CASE emits `break` and targets this loop,
|
|
// matching Harbour SWITCH semantics (EXIT terminates SWITCH).
|
|
// 3. Empty SWITCH (`SWITCH x ENDSWITCH`, common in conditional-
|
|
// compile test files) stays valid Go.
|
|
g.writeln("for {")
|
|
g.indent++
|
|
g.emitExpr(s.Expr)
|
|
g.writeln("_sw := t.Pop2()")
|
|
g.writeln("_ = _sw") // silence unused-var warning when no cases reference it
|
|
first := true
|
|
for _, c := range s.Cases {
|
|
if first {
|
|
g.emitExpr(c.Value)
|
|
g.writeln("if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
|
|
first = false
|
|
} else {
|
|
g.emitExpr(c.Value)
|
|
g.writeln("} else if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
|
|
}
|
|
g.indent++
|
|
for _, stmt := range c.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
if len(s.Otherwise) > 0 {
|
|
if first {
|
|
// No CASE arms — emit the OTHERWISE body as-is, no if/else.
|
|
for _, stmt := range s.Otherwise {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
} else {
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
for _, stmt := range s.Otherwise {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
} else if !first {
|
|
// Had CASE arms, no OTHERWISE — close the if/else-if chain.
|
|
g.writeln("}")
|
|
}
|
|
// Always break out of our one-iteration `for` wrapper, regardless
|
|
// of which (or no) case ran.
|
|
g.writeln("break")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
|
|
// BEGIN SEQUENCE → Go's panic/recover.
|
|
// Catches both *HbError (runtime errors) and BreakValue (Break() calls).
|
|
// BreakValue is defined in hbrtl, but we detect it via duck typing
|
|
// to avoid import cycles.
|
|
g.writeln("{ // BEGIN SEQUENCE")
|
|
g.indent++
|
|
g.writeln("_seqErr := func() (_recoverVal interface{}) {")
|
|
g.indent++
|
|
g.writeln("defer func() {")
|
|
g.indent++
|
|
g.writeln("if r := recover(); r != nil {")
|
|
g.indent++
|
|
g.writeln("_recoverVal = r")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
|
|
// Body
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
g.writeln("return nil")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
|
|
// RECOVER
|
|
if len(s.RecoverBody) > 0 {
|
|
g.writeln("if _seqErr != nil {")
|
|
g.indent++
|
|
if s.RecoverVar != "" {
|
|
if idx, found := locals[strings.ToUpper(s.RecoverVar)]; found {
|
|
// Extract the value from the recovered panic:
|
|
// *HbError → error description string
|
|
// BreakValue (has .Value field) → the Break() argument
|
|
// other → string representation
|
|
g.writeln(fmt.Sprintf(`{ // RECOVER USING %s`, s.RecoverVar))
|
|
g.indent++
|
|
g.writeln(`switch _sv := _seqErr.(type) {`)
|
|
g.writeln(`case *hbrt.HbError:`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString(_sv.Error()))`, idx))
|
|
g.writeln(`default:`)
|
|
// For BreakValue, use reflection-free approach: check if
|
|
// the type has a Value field via a local interface.
|
|
g.writeln(` type hasValue interface{ GetValue() hbrt.Value }`)
|
|
g.writeln(` if bv, ok := _sv.(hasValue); ok {`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, bv.GetValue())`, idx))
|
|
g.writeln(` } else {`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString("error"))`, idx))
|
|
g.writeln(` }`)
|
|
g.writeln(`}`)
|
|
g.indent--
|
|
g.writeln(`}`)
|
|
}
|
|
}
|
|
for _, stmt := range s.RecoverBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
g.writeln("_ = _seqErr")
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("} // END SEQUENCE")
|
|
}
|
|
|
|
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
|
|
varIdx, found := locals[strings.ToUpper(s.Var)]
|
|
if !found {
|
|
g.writeln("// ERROR: FOR EACH variable not in locals")
|
|
return
|
|
}
|
|
|
|
// Evaluate collection
|
|
g.emitExpr(s.Collection)
|
|
g.writeln("{ _feArr := t.Pop2()")
|
|
g.writeln("if _feArr.IsArray() {")
|
|
g.indent++
|
|
g.writeln("_feItems := _feArr.AsArray().Items")
|
|
g.writeln("for _feI := 0; _feI < len(_feItems); _feI++ {")
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _feItems[_feI])", varIdx))
|
|
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("} }")
|
|
}
|
|
|
|
// --- Expression emission ---
|
|
// Each emitExpr leaves one value on the stack.
|
|
|
|
// emitMultiAssign: a, b := Func() or a, b := x, y
|
|
func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
|
|
if len(s.Values) == 1 {
|
|
// Single RHS: a, b := Func() → call function, unpack array result
|
|
g.emitExpr(s.Values[0])
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_mr := t.Pop2()")
|
|
g.writeln("if _mr.IsArray() {")
|
|
g.indent++
|
|
g.writeln("_arr := _mr.AsArray()")
|
|
for i, name := range s.Targets {
|
|
if name == "_" {
|
|
continue
|
|
}
|
|
idx := locals[strings.ToUpper(name)]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("if %d < len(_arr.Items) { t.SetLocalFast(%d, _arr.Items[%d]) }", i, idx, i))
|
|
}
|
|
}
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
// Not array — assign first target, rest get NIL
|
|
if s.Targets[0] != "_" {
|
|
idx := locals[strings.ToUpper(s.Targets[0])]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mr)", idx))
|
|
}
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
// Multiple RHS: a, b := x, y (parallel assign)
|
|
// Evaluate all RHS first, then assign
|
|
for i, val := range s.Values {
|
|
g.emitExpr(val)
|
|
g.writeln(fmt.Sprintf("_mv%d := t.Pop2()", i))
|
|
}
|
|
for i, name := range s.Targets {
|
|
if name == "_" || i >= len(s.Values) {
|
|
continue
|
|
}
|
|
idx := locals[strings.ToUpper(name)]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mv%d)", idx, i))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// emitDefer: DEFER expr → Go defer
|
|
func (g *Generator) emitDefer(s *ast.DeferStmt, locals localMap) {
|
|
g.writeln("defer func() {")
|
|
g.indent++
|
|
g.emitExpr(s.Call)
|
|
g.writeln("t.Pop() // discard defer result")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
}
|
|
|
|
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.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:
|
|
g.emitExpr(e.X)
|
|
g.writeln("t.Dup()")
|
|
if e.Op == token.INC {
|
|
g.writeln("t.Inc()")
|
|
} else {
|
|
g.writeln("t.Dec()")
|
|
}
|
|
g.writeln("t.Pop() // keep original for postfix")
|
|
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
|
|
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) emitAliasExpr(e *ast.AliasExpr) {
|
|
fieldIdent, isFieldIdent := e.Field.(*ast.IdentExpr)
|
|
|
|
// Case 1: alias->field (static alias, simple field name)
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok && isFieldIdent {
|
|
upper := strings.ToUpper(ident.Name)
|
|
// `M->name` / `MEMVAR->name` access the memvar namespace, not
|
|
// a database workarea. Harbour reserves both aliases for this.
|
|
if upper == "M" || upper == "MEMVAR" {
|
|
g.writeln(fmt.Sprintf(`t.PushMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 2: (expr)->field (dynamic alias, simple field name)
|
|
if isFieldIdent {
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 3: alias->(expr) or (expr)->(expr) — workarea context expression
|
|
// Harbour: save current WA, select new WA, evaluate expr, restore WA
|
|
// Example: (nArea)->(Used()) → evaluate Used() in workarea nArea
|
|
// Example: CUSTOMERS->(RecCount()) → evaluate RecCount() in CUSTOMERS workarea
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
|
|
_, isLocal := g.curLocals[strings.ToUpper(ident.Name)]
|
|
if isLocal {
|
|
// Local variable: emit value (numeric area number)
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
} else {
|
|
// Static alias name: resolve by alias string
|
|
g.writeln(fmt.Sprintf(`t.WASaveAndSelectAlias(%q)`, ident.Name))
|
|
}
|
|
} else {
|
|
// Dynamic: numeric area from expression
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
}
|
|
g.emitExpr(e.Field)
|
|
g.writeln(`t.WARestore()`)
|
|
}
|
|
|
|
func (g *Generator) fieldName(expr ast.Expr) string {
|
|
if ident, ok := expr.(*ast.IdentExpr); ok {
|
|
return ident.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *Generator) emitSendExpr(e *ast.SendExpr) {
|
|
// ::super:Method(args) — dispatch to parent class. The parse tree
|
|
// is nested: outer SendExpr.Object is itself a SendExpr whose
|
|
// Object is ::SELF and Method is "super". Detect that shape and
|
|
// route through SendSuper, which keeps Self bound to the child
|
|
// instance but looks the method up on Parent.
|
|
if sup, ok := e.Object.(*ast.SendExpr); ok {
|
|
if _, isSelf := sup.Object.(*ast.SelfExpr); isSelf &&
|
|
strings.EqualFold(sup.Method, "super") {
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
// Emit defining-class name so runtime walks the right Parent
|
|
// chain — Self's class alone would infinite-loop on 3+ level
|
|
// hierarchies (Grand→Child→Base). See SendSuper comment.
|
|
g.writeln(fmt.Sprintf("t.SendSuper(%q, %q, %d)",
|
|
g.curMethodClass, e.Method, len(e.Args)))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Self access: ::field (no parens) → PushSelfField
|
|
// Self method: ::method() (has parens) → Send on Self
|
|
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {
|
|
if !e.HasParens && len(e.Args) == 0 {
|
|
// ::field (getter, no parentheses)
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", strings.ToUpper(e.Method)))
|
|
return
|
|
}
|
|
// ::method() or ::method(args) — method call on Self
|
|
g.writeln("t.PushSelf()")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
return
|
|
}
|
|
|
|
// General: obj:method(args) or obj:field
|
|
// Check at runtime: if Go object → GoCall, else Harbour Send
|
|
g.emitExpr(e.Object)
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_obj := t.Pop2()")
|
|
|
|
// Push args and capture them
|
|
argNames := make([]string, len(e.Args))
|
|
for i, arg := range e.Args {
|
|
argNames[i] = fmt.Sprintf("_sa%d", i)
|
|
g.emitExpr(arg)
|
|
g.writeln(fmt.Sprintf("%s := t.Pop2()", argNames[i]))
|
|
}
|
|
|
|
g.writeln("if hbrt.IsGoObject(_obj) {")
|
|
g.indent++
|
|
// Go object: use reflect bridge
|
|
argsStr := ""
|
|
for i, name := range argNames {
|
|
if i > 0 {
|
|
argsStr += ", "
|
|
}
|
|
argsStr += name
|
|
}
|
|
g.writeln(fmt.Sprintf("_gr := hbrt.GoCallCached(_obj, %q, %s)", e.Method, argsStr))
|
|
g.writeln("if len(_gr) > 0 { t.PushValue(_gr[0]) } else { t.PushNil() }")
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
// Harbour object: use Send
|
|
g.writeln("t.PushValue(_obj)")
|
|
for _, name := range argNames {
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", name))
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitBlock(e *ast.BlockExpr) {
|
|
// Code block: {|params| body}
|
|
// Block params are passed via Frame() from Eval/AEval.
|
|
nParams := len(e.Params)
|
|
|
|
// Collect free variables in the block body that reference outer locals.
|
|
// These need to be captured via Go closure variables.
|
|
outerLocals := g.curLocals
|
|
blockLocals := make(localMap)
|
|
for i, p := range e.Params {
|
|
blockLocals[strings.ToUpper(p)] = i + 1
|
|
}
|
|
|
|
// Find all idents in block body that are in outerLocals but NOT in blockLocals
|
|
freeVars := g.collectFreeVars(e.Body, blockLocals, outerLocals)
|
|
|
|
// Harbour: closures share outer locals via RefCell (mutable capture).
|
|
// Convert each captured outer local to a RefCell, then pass the RefCell
|
|
// into the block. Both outer function and block read/write through it.
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
// Ensure outer local is a RefCell (PushLocalRef creates one if needed,
|
|
// but we do it inline to avoid stack ops).
|
|
g.writeln(fmt.Sprintf("t.EnsureLocalRef(%d) // share %s via RefCell", outerIdx, fv))
|
|
}
|
|
|
|
// Capture the RefCell values with unique names to avoid Go scope issues.
|
|
capSeq := g.blockSeq
|
|
g.blockSeq++
|
|
capNames := make(map[string]string) // fv → Go var name
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
capName := fmt.Sprintf("_cap_%s_%d", fv, capSeq)
|
|
g.writeln(fmt.Sprintf("%s := t.LocalRaw(%d) // capture RefCell %s", capName, outerIdx, fv))
|
|
capNames[fv] = capName
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
|
|
g.indent++
|
|
nLocals := len(freeVars)
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
|
|
// Inject RefCell values directly into block locals — reads/writes go through RefCell
|
|
for i, fv := range freeVars {
|
|
localIdx := nParams + i + 1
|
|
blockLocals[fv] = localIdx
|
|
g.writeln(fmt.Sprintf("t.SetLocalRaw(%d, %s) // inject shared RefCell %s", localIdx, capNames[fv], fv))
|
|
}
|
|
|
|
g.curLocals = blockLocals
|
|
g.emitExpr(e.Body)
|
|
g.writeln("t.RetValue()")
|
|
|
|
g.curLocals = outerLocals
|
|
g.indent--
|
|
g.writeln(fmt.Sprintf("}, %d)", 0))
|
|
}
|
|
|
|
// collectFreeVars finds identifier names in expr that exist in outerLocals but not blockLocals.
|
|
func (g *Generator) collectFreeVars(expr ast.Expr, blockLocals, outerLocals localMap) []string {
|
|
var result []string
|
|
seen := map[string]bool{}
|
|
g.walkExprIdents(expr, func(name string) {
|
|
upper := strings.ToUpper(name)
|
|
if seen[upper] {
|
|
return
|
|
}
|
|
if _, inBlock := blockLocals[upper]; inBlock {
|
|
return
|
|
}
|
|
if _, inOuter := outerLocals[upper]; inOuter {
|
|
seen[upper] = true
|
|
result = append(result, upper)
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
// walkExprIdents calls fn for each IdentExpr in the expression tree.
|
|
func (g *Generator) walkExprIdents(expr ast.Expr, fn func(string)) {
|
|
if expr == nil {
|
|
return
|
|
}
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
fn(e.Name)
|
|
case *ast.BinaryExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.UnaryExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.PostfixExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.CallExpr:
|
|
g.walkExprIdents(e.Func, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.IndexExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
g.walkExprIdents(e.Index, fn)
|
|
case *ast.DotExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.AssignExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.ArrayLitExpr:
|
|
for _, item := range e.Items {
|
|
g.walkExprIdents(item, fn)
|
|
}
|
|
case *ast.IIfExpr:
|
|
g.walkExprIdents(e.Cond, fn)
|
|
g.walkExprIdents(e.True, fn)
|
|
g.walkExprIdents(e.False, fn)
|
|
case *ast.SendExpr:
|
|
g.walkExprIdents(e.Object, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.AliasExpr:
|
|
g.walkExprIdents(e.Alias, fn)
|
|
g.walkExprIdents(e.Field, fn)
|
|
case *ast.BlockExpr:
|
|
g.walkExprIdents(e.Body, fn)
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|