// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Block, alias, and method-send emission. // // Groups three related emitters that all cross the "ordinary local vs // externally-addressable" boundary: // // - emitAliasExpr: workarea aliasing (`ALIAS->field`, `(expr)->(...)`, // MEMVAR->name), including the save/select/restore dance used when // an aliased expression switches the current workarea. // - emitSendExpr: method dispatch (`obj:method()`, `::field`, // `::super:method()`, Go-object reflect-bridge fallback). // - emitBlock: code blocks `{|params| body}`, including // RefCell-based mutable capture of outer locals. // // collectFreeVars / walkExprIdents are the shared walker that emitBlock // uses to decide which outer locals to capture into the block. package gengo import ( "five/compiler/ast" "fmt" "strings" ) 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) case *ast.SeqExpr: // Comma-separated expressions inside a code block — recurse so // every sub-expr's free variables are picked up for closure // capture. Otherwise the second/third comma-statements would // see uncaptured outer locals. for _, item := range e.Items { g.walkExprIdents(item, fn) } } }