feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix

Major changes since last commit:
- FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS
- 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.)
- @byref pass-by-reference via RefCell pattern
- Mutable closure capture (EnsureLocalRef + RefCell sharing)
- RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8)
- DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display)
- Reserved word guard (39 keywords blocked from function calls)
- AEval arg order fix (element before index)
- Closure capture redecl fix (unique _cap_ names per block)
- Hash/string indexing in ArrayPush/ArrayPop
- Harbour compat test suite: 51/51
- 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 11:35:37 +09:00
parent d451b836a6
commit 486e466592
129 changed files with 35248 additions and 241 deletions

View File

@@ -767,10 +767,13 @@ func (s *LoopStmt) stmtNode() {}
// UseCmd represents USE [file] [VIA driver] [ALIAS name] [EXCLUSIVE|SHARED]
type UseCmd struct {
UsePos token.Position
File Expr // filename expression (nil = close current)
Via string // RDD driver name
Alias string // alias name
UsePos token.Position
File Expr // filename expression (nil = close current)
Via string // RDD driver name
Alias string // alias name (static)
AliasExpr Expr // alias expression for ALIAS (expr) — dynamic alias
Shared bool // SHARED flag
ReadOnly bool // READONLY flag
}
func (s *UseCmd) Pos() token.Position { return s.UsePos }

View File

@@ -117,7 +117,7 @@ func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
localMap := make(localMap)
idx := 1
for _, p := range md.Params {
localMap[p.Name] = idx
localMap[strings.ToUpper(p.Name)] = idx
idx++
}
for _, d := range md.Decls {
@@ -125,9 +125,9 @@ func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
for _, v := range vd.Vars {
if v.Init != nil {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
localMap[v.Name] = idx
localMap[strings.ToUpper(v.Name)] = idx
idx++
}
}

View File

@@ -34,10 +34,24 @@ func (g *Generator) emitUseCmd(s *ast.UseCmd, locals localMap) {
if s.Via != "" {
via = s.Via
}
alias := s.Alias
shared := "false"
if s.Shared {
shared = "true"
}
readOnly := "false"
if s.ReadOnly {
readOnly = "true"
}
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, %q, false, false)", via, alias))
g.writeln("if _err != nil { panic(_err) }")
if s.AliasExpr != nil {
// Dynamic alias: ALIAS (expr)
g.emitExpr(s.AliasExpr)
g.writeln("_alias := t.Pop2().AsString()")
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, _alias, %s, %s)", via, shared, readOnly))
} else {
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, %q, %s, %s)", via, s.Alias, shared, readOnly))
}
g.writeln("if _err != nil { panic(&hbrt.HbError{Description: _err.Error(), Operation: \"USE\", SubSystem: \"BASE\"}) }")
g.indent--
g.writeln("}")
}
@@ -344,7 +358,7 @@ func (g *Generator) emitReadCmd(s *ast.ReadCmd, locals localMap) {
// Uses captured frame base + local index to access the outer variable correctly
// even when the block is called from a different call depth (e.g., Eval inside GetNew).
func (g *Generator) emitGetSetBlock(varExpr ast.Expr, varName string, locals localMap) {
if idx, found := locals[varName]; found {
if idx, found := locals[strings.ToUpper(varName)]; found {
// Capture the frame's localBase and index at block creation time
g.writeln(fmt.Sprintf("{ // GET/SET block for %s", varName))
g.indent++
@@ -379,8 +393,8 @@ func (g *Generator) emitGetSetBlock(varExpr ast.Expr, varName string, locals loc
// emitIdentByName pushes a variable by name onto the stack
func (g *Generator) emitIdentByName(name string, locals localMap) {
if idx, found := locals[name]; found {
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
if idx, found := locals[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
} else if goVar, found := g.staticVars[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
} else {
@@ -390,8 +404,8 @@ func (g *Generator) emitIdentByName(name string, locals localMap) {
// emitPopByName pops stack into a variable by name
func (g *Generator) emitPopByName(name string, locals localMap) {
if idx, found := locals[name]; found {
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
if idx, found := locals[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
} else if goVar, found := g.staticVars[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
} else {

View File

@@ -8,12 +8,15 @@ import "five/compiler/ast"
// hasXBaseCommands checks if the file contains any xBase commands.
func hasXBaseCommands(file *ast.File) bool {
for _, d := range file.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok {
continue
}
if scanStmtsForXBase(fn.Body) {
return true
switch decl := d.(type) {
case *ast.FuncDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
case *ast.MethodDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
}
}
return false

View File

@@ -41,6 +41,9 @@ type Generator struct {
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
}
type symbolEntry struct {
@@ -122,7 +125,28 @@ func doGenerate(file *ast.File, debug, library bool) string {
g.emitMain()
}
return g.buf.String()
// 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 ---
@@ -156,7 +180,7 @@ func (g *Generator) emitHeader() {
g.writeln("package main")
g.writeln("")
// Imports
// Imports (deferred placeholder for imports discovered during body emission)
g.writeln("import (")
g.indent++
for imp := range g.imports {
@@ -166,6 +190,7 @@ func (g *Generator) emitHeader() {
g.writeln(fmt.Sprintf("%q", imp))
}
}
g.writeln("/*DEFERRED_IMPORTS*/")
g.indent--
g.writeln(")")
g.writeln("")
@@ -176,12 +201,11 @@ func (g *Generator) emitHeader() {
g.writeln("var _ = hbrdd.NewWorkAreaManager")
g.writeln("var _ dbf.DBFDriver")
}
if g.imports["strings"] {
g.writeln("var _ = strings.TrimLeft")
}
if g.imports["fmt"] {
g.writeln("var _ = fmt.Sprintf")
}
// 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("")
}
@@ -355,7 +379,7 @@ func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
for _, v := range vd.Vars {
if v.Init != nil {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", localIdx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", localIdx))
}
localIdx++
}
@@ -377,13 +401,13 @@ func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap {
m := make(localMap)
idx := 1
for _, p := range fn.Params {
m[p.Name] = idx
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[v.Name] = idx
m[strings.ToUpper(v.Name)] = idx
idx++
}
}
@@ -391,6 +415,46 @@ func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap {
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) {
@@ -441,7 +505,12 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
g.writeln("break")
case *ast.LoopStmt:
g.writeln("continue")
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)
@@ -627,13 +696,9 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
}
func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
// LOCAL declared in mid-function: allocate new local slots dynamically
// For now, emit as local variable with initialization
for _, v := range s.Vars {
// Find or assign local index
idx, found := locals[v.Name]
idx, found := locals[strings.ToUpper(v.Name)]
if !found {
// Assign next available slot
maxIdx := 0
for _, i := range locals {
if i > maxIdx {
@@ -641,11 +706,11 @@ func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
}
}
idx = maxIdx + 1
locals[v.Name] = idx
locals[strings.ToUpper(v.Name)] = idx
}
if v.Init != nil {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
}
}
@@ -676,7 +741,7 @@ func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
}
// 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[ident.Name]; !found {
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", strings.ToUpper(ident.Name)))
g.writeln("t.PushNil()")
g.writeln("t.Do(0)")
@@ -687,7 +752,8 @@ func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
if pf, ok := s.X.(*ast.PostfixExpr); ok {
// Local variable: n++
if ident, ok := pf.X.(*ast.IdentExpr); ok {
if idx, found := locals[ident.Name]; found {
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 {
@@ -695,6 +761,15 @@ func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
}
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 {
@@ -778,11 +853,11 @@ func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
}
if ident, ok := a.Left.(*ast.IdentExpr); ok {
if idx, found := locals[ident.Name]; found {
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
case token.PLUSEQ:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
@@ -792,10 +867,10 @@ func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
default:
// General compound: push local, push right, op, pop local
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
return
}
@@ -1040,7 +1115,7 @@ func hasAppendInBody(stmts []ast.Stmt) bool {
}
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
idx, found := locals[s.Var]
idx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR variable not found in locals")
return
@@ -1048,7 +1123,7 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
// i := start
g.emitExpr(s.Start)
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
// Detect step direction for comparison
isNegStep := false
@@ -1095,7 +1170,7 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
}
} else {
// General case: stack-based comparison
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(s.To)
if isNegStep {
g.writeln("t.GreaterEqual()")
@@ -1105,11 +1180,29 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
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)
@@ -1118,6 +1211,7 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
}
g.curForLabel = prevForLabel
g.indent--
g.writeln("}")
@@ -1129,6 +1223,69 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
}
}
// 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) {
g.emitExpr(s.Expr)
g.writeln("_sw := t.Pop2()")
@@ -1193,8 +1350,8 @@ func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
g.writeln("if _seqErr != nil {")
g.indent++
if s.RecoverVar != "" {
if idx, found := locals[s.RecoverVar]; found {
g.writeln(fmt.Sprintf("t.SetLocal(%d, hbrt.MakeString(_seqErr.Error()))", idx))
if idx, found := locals[strings.ToUpper(s.RecoverVar)]; found {
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, hbrt.MakeString(_seqErr.Error()))", idx))
}
}
for _, stmt := range s.RecoverBody {
@@ -1202,6 +1359,8 @@ func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
}
g.indent--
g.writeln("}")
} else {
g.writeln("_ = _seqErr")
}
g.indent--
@@ -1209,7 +1368,7 @@ func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
}
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
varIdx, found := locals[s.Var]
varIdx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR EACH variable not in locals")
return
@@ -1223,7 +1382,7 @@ func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
g.writeln("_feItems := _feArr.AsArray().Items")
g.writeln("for _feI := 0; _feI < len(_feItems); _feI++ {")
g.indent++
g.writeln(fmt.Sprintf("t.SetLocal(%d, _feItems[_feI])", varIdx))
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _feItems[_feI])", varIdx))
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
@@ -1255,7 +1414,7 @@ func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
}
idx := locals[strings.ToUpper(name)]
if idx > 0 {
g.writeln(fmt.Sprintf("if %d < len(_arr.Items) { t.SetLocal(%d, _arr.Items[%d]) }", i, idx, i))
g.writeln(fmt.Sprintf("if %d < len(_arr.Items) { t.SetLocalFast(%d, _arr.Items[%d]) }", i, idx, i))
}
}
g.indent--
@@ -1265,7 +1424,7 @@ func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
if s.Targets[0] != "_" {
idx := locals[strings.ToUpper(s.Targets[0])]
if idx > 0 {
g.writeln(fmt.Sprintf("t.SetLocal(%d, _mr)", idx))
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mr)", idx))
}
}
g.indent--
@@ -1285,7 +1444,7 @@ func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
}
idx := locals[strings.ToUpper(name)]
if idx > 0 {
g.writeln(fmt.Sprintf("t.SetLocal(%d, _mv%d)", idx, i))
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mv%d)", idx, i))
}
}
}
@@ -1308,16 +1467,33 @@ func (g *Generator) emitExpr(expr ast.Expr) {
case *ast.IdentExpr:
g.emitIdent(e)
case *ast.BinaryExpr:
g.emitExpr(e.Left)
g.emitExpr(e.Right)
g.emitBinaryOp(e.Op)
// Short-circuit AND/OR: Harbour evaluates right operand only if needed
if e.Op == token.AND {
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 {
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:
g.emitExpr(e.X)
g.emitUnaryOp(e.Op)
case *ast.AssignExpr:
g.emitExpr(e.Right)
g.writeln("t.Dup()")
g.writeln("// WARN: compound assignment — value on stack")
// 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:
@@ -1391,7 +1567,7 @@ func (g *Generator) emitExpr(expr ast.Expr) {
// @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[ident.Name]; found {
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
@@ -1519,8 +1695,8 @@ func (g *Generator) emitIdent(e *ast.IdentExpr) {
return
}
if idx, found := g.curLocals[e.Name]; found {
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
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))
@@ -1545,6 +1721,13 @@ func (g *Generator) emitCall(e *ast.CallExpr) {
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
}
if g.symCache != nil {
if varName, ok := g.symCache[upper]; ok {
g.writeln(fmt.Sprintf("t.PushSymbol(%s)", varName))
@@ -1564,6 +1747,61 @@ func (g *Generator) emitCall(e *ast.CallExpr) {
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) {
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",
"DO", "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.
@@ -1866,16 +2104,42 @@ type goFastEntry struct {
}
func (g *Generator) emitAliasExpr(e *ast.AliasExpr) {
// alias->field or (expr)->field
// Push alias, then field name, call runtime FieldGet by name
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
// Static alias: customers->name
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, g.fieldName(e.Field)))
} else {
// Dynamic: (cAlias)->field
g.emitExpr(e.Alias)
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, g.fieldName(e.Field)))
fieldIdent, isFieldIdent := e.Field.(*ast.IdentExpr)
// Case 1: alias->field (static alias, simple field name)
if ident, ok := e.Alias.(*ast.IdentExpr); ok && isFieldIdent {
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 {
@@ -1947,30 +2211,132 @@ func (g *Generator) emitSendExpr(e *ast.SendExpr) {
func (g *Generator) emitBlock(e *ast.BlockExpr) {
// Code block: {|params| body}
// The block function receives the SAME thread (t), not a new one.
// Block params are passed via Frame() from Eval/AEval.
nParams := len(e.Params)
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
g.indent++
g.writeln(fmt.Sprintf("t.Frame(%d, 0)", nParams))
g.writeln("defer t.EndProc()")
// Build local map for block params
oldLocals := g.curLocals
// 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[p] = i + 1
blockLocals[strings.ToUpper(p)] = i + 1
}
g.curLocals = blockLocals
// 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 = oldLocals
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:

View File

@@ -54,8 +54,8 @@ func TestGenerateArithmetic(t *testing.T) {
`)
assertContains(t, code, "t.Frame(0, 1)")
assertContains(t, code, "t.PushInt(10)")
assertContains(t, code, "t.PopLocal(1)")
assertContains(t, code, "t.PushLocal(1)") // n
assertContains(t, code, "t.PopLocalFast(1)")
assertContains(t, code, "t.PushLocalFast(1)") // n
assertContains(t, code, "t.PushInt(5)")
assertContains(t, code, "t.Plus()")
assertContains(t, code, "t.RetValue()")
@@ -129,7 +129,7 @@ func TestGenerateStringConcat(t *testing.T) {
RETURN NIL
`)
assertContains(t, code, `t.PushString("Hello, ")`)
assertContains(t, code, "t.PushLocal(1)")
assertContains(t, code, "t.PushLocalFast(1)")
assertContains(t, code, "t.Plus()")
assertContains(t, code, `t.PushString("!")`)
}

View File

@@ -591,6 +591,11 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl {
p.advance() // skip keyword
p.advance() // skip :
p.skipNewlines()
} else if upper == "CLASSDATA" || upper == "CLASSVAR" {
// CLASSDATA / CLASSVAR — class-level variable (treat as DATA)
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
members = append(members, p.parseDataDecl())
} else if upper == "CLASS" {
// CLASS VAR — class-level variable
p.advance() // skip CLASS
@@ -1511,6 +1516,8 @@ func (p *Parser) parseUse() *ast.UseCmd {
pos := p.expect(token.USE).Pos
var file ast.Expr
var via, alias string
var aliasExprNode ast.Expr
var shared, readOnly bool
// USE without args = close
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
@@ -1520,8 +1527,22 @@ func (p *Parser) parseUse() *ast.UseCmd {
p.expectEndOfStmt()
return &ast.UseCmd{UsePos: pos}
}
file = p.parseExpr()
p.consumeFileExtension(file)
// Bare ident as filename: USE myfile / USE myfile.dbf / USE myfile NEW
// In Harbour, USE <name> treats name as a filename string, not a variable.
// Only use parseExpr for parenthesized (USE (expr)) or string literal (USE "file").
if p.at(token.IDENT) {
// Check if it's a bare filename (ident optionally followed by .ext)
name := p.advance().Literal
if p.at(token.DOT) && (p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
p.advance() // skip DOT
ext := p.advance().Literal
name = name + "." + ext
}
file = &ast.LiteralExpr{ValuePos: pos, Kind: token.STRING, Value: name}
} else {
file = p.parseExpr()
p.consumeFileExtension(file)
}
}
// Parse optional clauses: VIA, ALIAS, EXCLUSIVE, SHARED, NEW, READONLY
@@ -1530,20 +1551,44 @@ func (p *Parser) parseUse() *ast.UseCmd {
upper := p.currentUpper()
if upper == "VIA" {
p.advance()
via = p.expectMethodName().Literal
if p.at(token.STRING) {
via = p.current.Literal
p.advance()
} else {
via = p.expectMethodName().Literal
}
continue
}
if upper == "ALIAS" {
p.advance()
if p.at(token.AMPERSAND) {
p.parseMacro() // macro alias — skip
} else if p.at(token.LPAREN) {
// ALIAS ( expr ) — parenthesized alias expression (runtime)
p.advance() // skip (
aliasExpr := p.parseExpr()
p.expect(token.RPAREN)
if lit, ok := aliasExpr.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
alias = lit.Value // constant string — store directly
} else {
aliasExprNode = aliasExpr // dynamic — evaluate at runtime
}
} else {
alias = p.expectMethodName().Literal
}
continue
}
if upper == "EXCLUSIVE" || upper == "SHARED" || upper == "NEW" || upper == "READONLY" ||
upper == "ADDITIVE" {
if upper == "SHARED" {
shared = true
p.advance()
continue
}
if upper == "READONLY" {
readOnly = true
p.advance()
continue
}
if upper == "EXCLUSIVE" || upper == "NEW" || upper == "ADDITIVE" {
p.advance()
continue
}
@@ -1557,6 +1602,16 @@ func (p *Parser) parseUse() *ast.UseCmd {
p.advance()
if p.at(token.AMPERSAND) {
p.parseMacro()
} else if p.at(token.LPAREN) {
// ALIAS ( expr ) — parenthesized alias expression
p.advance()
ae := p.parseExpr()
p.expect(token.RPAREN)
if lit, ok := ae.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
alias = lit.Value
} else {
aliasExprNode = ae
}
} else {
alias = p.expectMethodName().Literal
}
@@ -1571,7 +1626,7 @@ func (p *Parser) parseUse() *ast.UseCmd {
}
p.expectEndOfStmt()
return &ast.UseCmd{UsePos: pos, File: file, Via: via, Alias: alias}
return &ast.UseCmd{UsePos: pos, File: file, Via: via, Alias: alias, AliasExpr: aliasExprNode, Shared: shared, ReadOnly: readOnly}
}
func (p *Parser) parseSelect() *ast.SelectCmd {