Files
five/compiler/genpc/genpc.go
CharlesKWON 29ca02e1bc fix(genpc,parser,pcinterp): pcode wider regression sweep (Tier 1 #3)
Six more silent miscompiles in the pcode path, all uncovered by a
new pcode regression sweep that exercises the full PRG surface a
dynamic FrbCompile body could legitimately use.

  * **xBase-keyword shadowing of variable names.** parseIdentStmt
    and parseExprStmt's fallback switches consumed an entire line
    when the leading IDENT matched LABEL / REPORT / ACCEPT / INPUT
    / NOTE / etc. Those words are also extremely common LOCAL /
    PRIVATE names — `LOCAL label ; label := "x"` had the
    assignment swallowed because the switch didn't peek at the
    next token. Both switches now look at peek(1): an assignment
    operator, [], (, -, ++, --, or `.` means it's a variable /
    call / member access, not the xBase command, and we fall
    through to expression parsing. Real silent bug — bit
    test_frb_pcode_sweep's `LOCAL label` declaration.

  * **`arr[i]` indexing not implemented in genpc.** ast.IndexExpr
    fell through to the default PushNil path, so any indexed read
    in a pcode-mode body returned NIL. New case emits the array,
    the index, and PcOpArrayPush (the get-op; PcOpArrayPop is the
    set-op — naming follows Harbour convention). Hashes go
    through the same opcode, which already special-cases
    IsHash() in ops_collection.go.

  * **Hash literals not implemented in genpc + dispatch missing
    in pcinterp.** `{ "k" => v, ... }` fell to PushNil. Added
    HashLitExpr emit (Push key, Push value pairs, then PcOpHashGen
    with count). Also wired up the PcOpHashGen dispatch in
    execPcodeBody — it had been declared in pcode.go since the
    initial design but the case statement was never added, so
    even hand-written modules couldn't use hashes.

  * **`x++` / `x--` postfix were silent no-ops.** PostfixExpr fell
    to PushNil and the surrounding ExprStmt then popped the NIL.
    DO WHILE loops with `n--` couldn't terminate; FOR loops with
    `i++` in the body were broken too. New case: PushLocal +
    LocalAddInt(±1).

  * **BlockExpr (`{|p| body }`) wasn't compiled.** Eval(b, n)
    inside a pcode body returned NIL. Added: build the body in a
    sub-codebuffer with the block's params occupying its locals,
    emit PcOpRetValue at the end, then PushBlock with the
    serialized bytes. Format extended with a uint16 nParams field
    so the runtime's PcOpPushBlock dispatch can set
    PcodeFunc.Params correctly — without it, ExecPcode's
    Frame(0, 0) pulled none of Eval's args and the block saw
    every parameter as NIL.

  * **All g.locals accesses were case-sensitive.** PRG is case-
    insensitive, but the pcode generator stored block params via
    strings.ToUpper while every other lookup site (function decl,
    mid-decl, ForStmt, IdentExpr read, AssignExpr write,
    PostfixExpr) used the raw .Name. So `{|x| x*x }` stored "X"
    but read "x" and missed. Normalized: all insertions and all
    lookups now go through strings.ToUpper.

  * **SeqExpr in pcode** — added the matching emit for comma-
    separated expression lists in code blocks (`{|| a, b, c }`).
    Same shape as the gengo SeqExpr case from Wave 1.

Test fixture: tests/frb/test_frb_pcode_sweep.prg covers 14 shapes
(string ops, arithmetic, comparison chains, array indexing, DO
WHILE with postfix, nested IF, IIf, hash literal + indexing,
block + Eval, character iteration). All 14 pass. Wired into the
FRB runner — suite now stands at 7/7.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 15/15
  FRB suite          : 7/7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:32:38 +09:00

794 lines
20 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// genpc — Five pcode generator. Compiles AST to bytecode for FRB interpreter mode.
// Mirrors gengo's logic but emits bytecode opcodes instead of Go source code.
package genpc
import (
"encoding/binary"
"five/compiler/ast"
"five/compiler/token"
"five/hbrt"
"math"
"strconv"
"strings"
)
// Generate compiles an AST file to a PcodeModule.
func Generate(file *ast.File) *hbrt.PcodeModule {
g := &generator{
mod: &hbrt.PcodeModule{
Name: file.Name,
Funcs: make(map[string]*hbrt.PcodeFunc),
},
}
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
g.emitFunc(decl)
}
}
return g.mod
}
// CompileExpr compiles a single expression AST to a standalone PcodeFunc
// that, when executed, leaves the expression's value on the stack as a
// return value. Used by FiveSql2 for prepared-statement-style caching:
// compile WHERE / SELECT expressions once per query, execute per row.
//
// The returned function takes zero parameters and zero locals.
// Caller provides field access context via the current workarea.
func CompileExpr(expr ast.Expr) *hbrt.PcodeFunc {
g := &generator{
mod: &hbrt.PcodeModule{Funcs: make(map[string]*hbrt.PcodeFunc)},
locals: make(map[string]int),
}
// Note: ExecPcode emits its own Frame/EndProc around this code.
// We just emit the expression evaluation + RetValue.
g.emitExpr(expr)
g.emit(hbrt.PcOpRetValue)
return &hbrt.PcodeFunc{
Name: "_EXPR",
Code: g.code,
Params: 0,
Locals: 0,
}
}
type generator struct {
mod *hbrt.PcodeModule
code []byte
locals map[string]int
}
func (g *generator) emit(b ...byte) {
g.code = append(g.code, b...)
}
func (g *generator) emitU16(v uint16) {
var buf [2]byte
binary.LittleEndian.PutUint16(buf[:], v)
g.code = append(g.code, buf[:]...)
}
func (g *generator) emitI32(v int32) {
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], uint32(v))
g.code = append(g.code, buf[:]...)
}
func (g *generator) emitI64(v int64) {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(v))
g.code = append(g.code, buf[:]...)
}
func (g *generator) emitF64(v float64) {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], math.Float64bits(v))
g.code = append(g.code, buf[:]...)
}
func (g *generator) emitString(op byte, s string) {
g.emit(op)
g.emitU16(uint16(len(s)))
g.code = append(g.code, []byte(s)...)
}
func (g *generator) pc() int {
return len(g.code)
}
// placeholder for jump offset, returns position to patch
func (g *generator) emitJumpPlaceholder(op byte) int {
g.emit(op)
pos := g.pc()
g.emitI32(0) // placeholder
return pos
}
func (g *generator) patchJump(pos int) {
offset := int32(g.pc() - pos - 4) // relative to after the offset bytes
binary.LittleEndian.PutUint32(g.code[pos:], uint32(offset))
}
// --- Function ---
func (g *generator) emitFunc(fn *ast.FuncDecl) {
g.code = nil
g.locals = make(map[string]int)
// Build local map. PRG is case-insensitive so all keys are
// uppercased here; every lookup site below must mirror this.
idx := 1
for _, p := range fn.Params {
g.locals[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 {
g.locals[strings.ToUpper(v.Name)] = idx
idx++
}
}
}
for _, s := range fn.Body {
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
for _, v := range vd.Vars {
g.locals[strings.ToUpper(v.Name)] = idx
idx++
}
}
}
nLocals := idx - 1 - len(fn.Params)
// Emit LOCAL initializers
localIdx := len(fn.Params) + 1
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 {
g.emitExpr(v.Init)
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(localIdx))
}
localIdx++
}
}
// Emit body
for _, s := range fn.Body {
g.emitStmt(s)
}
// Implicit return NIL
g.emit(hbrt.PcOpPushNil)
g.emit(hbrt.PcOpRetValue)
pf := &hbrt.PcodeFunc{
Name: fn.Name,
Code: make([]byte, len(g.code)),
Params: len(fn.Params),
Locals: nLocals,
}
copy(pf.Code, g.code)
g.mod.Funcs[strings.ToUpper(fn.Name)] = pf
}
// --- Statements ---
func (g *generator) emitStmt(stmt ast.Stmt) {
switch s := stmt.(type) {
case *ast.ReturnStmt:
if s.Value != nil {
g.emitExpr(s.Value)
g.emit(hbrt.PcOpRetValue)
} else {
g.emit(hbrt.PcOpPushNil)
g.emit(hbrt.PcOpRetValue)
}
case *ast.ExprStmt:
if assign, ok := s.X.(*ast.AssignExpr); ok {
g.emitAssign(assign)
} else if call, ok := s.X.(*ast.CallExpr); ok {
g.emitCallStmt(call)
} else {
g.emitExpr(s.X)
g.emit(hbrt.PcOpPop)
}
case *ast.IfStmt:
g.emitIf(s)
case *ast.DoWhileStmt:
g.emitDoWhile(s)
case *ast.ForStmt:
g.emitFor(s)
case *ast.ExitStmt:
// handled by loop
g.emit(hbrt.PcOpHalt) // placeholder
case *ast.QOutStmt:
g.emitQOut(s)
case *ast.VarDecl:
// Mid-function LOCAL
for _, v := range s.Vars {
if v.Init != nil {
g.emitExpr(v.Init)
if idx, ok := g.locals[strings.ToUpper(v.Name)]; ok {
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(idx))
} else {
g.emit(hbrt.PcOpPop)
}
}
}
default:
// Unsupported statement — skip
}
}
func (g *generator) emitIf(s *ast.IfStmt) {
g.emitExpr(s.Cond)
jumpFalse := g.emitJumpPlaceholder(hbrt.PcOpJumpFalse)
for _, stmt := range s.Body {
g.emitStmt(stmt)
}
if len(s.ElseIfs) > 0 || len(s.ElseBody) > 0 {
jumpEnd := g.emitJumpPlaceholder(hbrt.PcOpJump)
g.patchJump(jumpFalse)
for _, elif := range s.ElseIfs {
g.emitExpr(elif.Cond)
nextJump := g.emitJumpPlaceholder(hbrt.PcOpJumpFalse)
for _, stmt := range elif.Body {
g.emitStmt(stmt)
}
jumpEnd2 := g.emitJumpPlaceholder(hbrt.PcOpJump)
g.patchJump(nextJump)
_ = jumpEnd2 // will be patched by end
}
for _, stmt := range s.ElseBody {
g.emitStmt(stmt)
}
g.patchJump(jumpEnd)
} else {
g.patchJump(jumpFalse)
}
}
func (g *generator) emitDoWhile(s *ast.DoWhileStmt) {
loopStart := g.pc()
for _, stmt := range s.Body {
g.emitStmt(stmt)
}
g.emitExpr(s.Cond)
// Jump back if true
g.emit(hbrt.PcOpJumpTrue)
offset := int32(loopStart - g.pc() - 4)
g.emitI32(offset)
}
func (g *generator) emitFor(s *ast.ForStmt) {
idx, ok := g.locals[strings.ToUpper(s.Var)]
if !ok {
return
}
// Init: var := start
g.emitExpr(s.Start)
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(idx))
// Detect step direction statically (matches gengo's emitFor):
// * no Step → +1, ascending
// * literal -N → descending
// * unary MINUS → descending
// Anything else (variable, expression) defaults to ascending.
// Without this we always emitted `var <= to`, which made `FOR
// 5 TO 1 STEP -1` exit on the first iteration; and we always
// stepped by hardcoded +1, which made `FOR i := 1 TO 10 STEP
// 2` summed 1+2+...+10 (55) instead of 1+3+5+7+9 (25).
negStep := 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] == '-' {
negStep = true
}
}
if un, ok := s.Step.(*ast.UnaryExpr); ok && un.Op == token.MINUS {
negStep = true
}
}
loopStart := g.pc()
// Comparison: ascending → var <= to; descending → var >= to.
g.emit(hbrt.PcOpPushLocal)
g.emitU16(uint16(idx))
g.emitExpr(s.To)
if negStep {
g.emit(hbrt.PcOpGreaterEq)
} else {
g.emit(hbrt.PcOpLessEq)
}
jumpOut := g.emitJumpPlaceholder(hbrt.PcOpJumpFalse)
// Body
for _, stmt := range s.Body {
g.emitStmt(stmt)
}
// Increment: var := var + step (re-evaluating step per iter is
// fine; constant-folding can hoist it later). Push var, push
// step, add, store back.
g.emit(hbrt.PcOpPushLocal)
g.emitU16(uint16(idx))
if s.Step != nil {
g.emitExpr(s.Step)
} else {
g.emit(hbrt.PcOpPushInt)
g.emitI64(1)
}
g.emit(hbrt.PcOpPlus)
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(idx))
// Jump back to comparison
g.emit(hbrt.PcOpJump)
g.emitI32(int32(loopStart - g.pc() - 4))
g.patchJump(jumpOut)
}
func (g *generator) emitQOut(s *ast.QOutStmt) {
sym := "QOUT"
if s.IsQQ {
sym = "QQOUT"
}
g.emitString(hbrt.PcOpPushSymbol, sym)
g.emit(hbrt.PcOpPushNil)
for _, expr := range s.Exprs {
g.emitExpr(expr)
}
g.emit(hbrt.PcOpFunction)
g.emitU16(uint16(len(s.Exprs)))
}
// --- Expressions ---
func (g *generator) emitExpr(expr ast.Expr) {
switch e := expr.(type) {
case *ast.LiteralExpr:
switch e.Kind {
case token.INT:
g.emit(hbrt.PcOpPushInt)
v := parseInt64(e.Value)
g.emitI64(v)
case token.DOUBLE:
g.emit(hbrt.PcOpPushDouble)
v := parseFloat64(e.Value)
g.emitF64(v)
case token.STRING:
g.emitString(hbrt.PcOpPushString, e.Value)
case token.TRUE:
g.emit(hbrt.PcOpPushTrue)
case token.FALSE:
g.emit(hbrt.PcOpPushFalse)
case token.NIL_LIT:
g.emit(hbrt.PcOpPushNil)
}
case *ast.IdentExpr:
upper := strings.ToUpper(e.Name)
if upper == "SELF" {
g.emit(hbrt.PcOpPushSelf)
return
}
// Locals are keyed case-insensitively. Look up via uppercase
// (also covers blocks: their params are stored ToUpper). The
// previous raw `e.Name` lookup missed any caller that wrote
// the identifier in different case from the declaration —
// `{|x| x * x }` invoked via Eval(b, 7) silently saw x=NIL.
if idx, ok := g.locals[upper]; ok {
g.emit(hbrt.PcOpPushLocal)
g.emitU16(uint16(idx))
} else {
// Unknown at compile time → runtime memvar lookup. This
// makes `&(expr)` and the debugger's `p` see PRIVATEs
// (including the frame-local injection the debugger does).
g.emitString(hbrt.PcOpPushMemvar, upper)
}
case *ast.BinaryExpr:
g.emitExpr(e.Left)
g.emitExpr(e.Right)
g.emitBinaryOp(e.Op)
case *ast.UnaryExpr:
g.emitExpr(e.X)
switch e.Op {
case token.MINUS:
g.emit(hbrt.PcOpNegate)
case token.NOT:
g.emit(hbrt.PcOpNot)
}
case *ast.CallExpr:
g.emitCall(e)
case *ast.IIfExpr:
g.emitExpr(e.Cond)
jumpFalse := g.emitJumpPlaceholder(hbrt.PcOpJumpFalse)
g.emitExpr(e.True)
jumpEnd := g.emitJumpPlaceholder(hbrt.PcOpJump)
g.patchJump(jumpFalse)
g.emitExpr(e.False)
g.patchJump(jumpEnd)
case *ast.SelfExpr:
g.emit(hbrt.PcOpPushSelf)
case *ast.SendExpr:
g.emitExpr(e.Object)
if e.HasParens {
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.emitString(hbrt.PcOpSend, strings.ToUpper(e.Method))
g.emitU16(uint16(len(e.Args)))
} else {
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {
// Replace with PushSelfField (pop the self we pushed)
g.code = g.code[:len(g.code)] // keep self on stack... actually use dedicated op
g.emit(hbrt.PcOpPop) // remove self
g.emitString(hbrt.PcOpPushSelfField, strings.ToUpper(e.Method))
}
}
case *ast.ArrayLitExpr:
for _, item := range e.Items {
g.emitExpr(item)
}
g.emit(hbrt.PcOpArrayGen)
g.emitU16(uint16(len(e.Items)))
case *ast.BlockExpr:
// `{|p| body }` — compile body to its own pcode buffer with
// the block's params occupying locals 1..len(Params), then
// emit PcOpPushBlock + length + body bytes + nDetached (zero
// — closure capture isn't wired up in pcode mode yet, so
// blocks see their declared params and any module-local
// symbol but no caller locals).
// Without this case, BlockExpr fell through to the generic
// PushNil and Eval(NIL, ...) returned NIL — silently
// breaking every higher-order function (Eval / AEval /
// SqlScan predicate compile / etc.) inside a pcode body.
savedCode := g.code
savedLocals := g.locals
g.code = nil
g.locals = make(map[string]int, len(e.Params))
for i, p := range e.Params {
g.locals[strings.ToUpper(p)] = i + 1
}
g.emitExpr(e.Body)
g.emit(hbrt.PcOpRetValue)
body := g.code
g.code = savedCode
g.locals = savedLocals
g.emit(hbrt.PcOpPushBlock)
g.emitI32(int32(len(body)))
g.code = append(g.code, body...)
g.emitU16(uint16(len(e.Params))) // nParams
g.emitU16(0) // nDetached — no closure capture yet
case *ast.SeqExpr:
// Comma-separated expression list inside a code block:
// `{|| e1, e2, e3 }`. Evaluate each in order, pop intermediate
// results so only the last value remains. Same semantics as
// gengo's SeqExpr handler.
for i, item := range e.Items {
g.emitExpr(item)
if i < len(e.Items)-1 {
g.emit(hbrt.PcOpPop)
}
}
case *ast.HashLitExpr:
// `{ "k" => 1, ... }` — push each key+value pair, HashGen
// builds the hash from the top-N stack pairs. Without this
// case, the hash literal silently fell through to PushNil
// and any subsequent `h[key]` panicked at ArrayPush with
// "argument error (op: [])".
for i, k := range e.Keys {
g.emitExpr(k)
g.emitExpr(e.Values[i])
}
g.emit(hbrt.PcOpHashGen)
g.emitU16(uint16(len(e.Keys)))
case *ast.IndexExpr:
// arr[idx] — push array, push index, ArrayPush reads element.
// (ArrayPush is the "get" op; ArrayPop is the "set" op — names
// kept to match the Harbour stack-machine convention.)
// Without this case, indexed reads in pcode silently emitted
// PushNil via the default fallback, so `arr[i]` always
// returned NIL and `n + arr[i]` panicked at the +.
g.emitExpr(e.X)
g.emitExpr(e.Index)
g.emit(hbrt.PcOpArrayPush)
case *ast.PostfixExpr:
// `x++` / `x--` — read current value (becomes the expression
// result), apply Inc/Dec to the LOCAL slot, leave the
// pre-modification value on the stack so it round-trips
// correctly when used as an expression. As a statement the
// caller does Pop afterward.
// Without this case, postfix on pcode-mode silently emitted
// PushNil → `n++` was a no-op, breaking DO WHILE / FOR
// patterns that mutate the loop counter.
if id, isIdent := e.X.(*ast.IdentExpr); isIdent {
if idx, found := g.locals[strings.ToUpper(id.Name)]; found {
g.emit(hbrt.PcOpPushLocal)
g.emitU16(uint16(idx))
delta := int64(1)
if e.Op == token.DEC {
delta = -1
}
g.emit(hbrt.PcOpLocalAddInt)
g.emitU16(uint16(idx))
g.emitI32(int32(delta))
return
}
}
// Anything else (memvar, alias->field, arr[i]) — emit the
// expression as a no-op for now and document the gap.
g.emitExpr(e.X)
case *ast.AliasExpr:
// Pcode mode: only the M-> / MEMVAR-> namespace (memvar
// access) is wired up. The general workarea-alias form
// (`FOO->bar`, `(expr)->(body)`) needs new opcodes for
// alias dispatch + workarea context save/restore — until
// then it falls through to the generic NIL fallback so
// callers see "missing data" rather than crash.
if aliasIdent, ok1 := e.Alias.(*ast.IdentExpr); ok1 {
if fieldIdent, ok2 := e.Field.(*ast.IdentExpr); ok2 {
upper := strings.ToUpper(aliasIdent.Name)
if upper == "M" || upper == "MEMVAR" {
g.emitString(hbrt.PcOpPushMemvar, fieldIdent.Name)
return
}
}
}
g.emit(hbrt.PcOpPushNil)
default:
g.emit(hbrt.PcOpPushNil) // fallback
}
}
func (g *generator) emitBinaryOp(op token.Kind) {
switch op {
case token.PLUS:
g.emit(hbrt.PcOpPlus)
case token.MINUS:
g.emit(hbrt.PcOpMinus)
case token.STAR:
g.emit(hbrt.PcOpMult)
case token.SLASH:
g.emit(hbrt.PcOpDivide)
case token.PERCENT:
g.emit(hbrt.PcOpMod)
case token.POWER:
g.emit(hbrt.PcOpPower)
case token.EQ, token.EXEQ:
g.emit(hbrt.PcOpEqual)
case token.NEQ:
g.emit(hbrt.PcOpNotEqual)
case token.LT:
g.emit(hbrt.PcOpLess)
case token.GT:
g.emit(hbrt.PcOpGreater)
case token.LTE:
g.emit(hbrt.PcOpLessEq)
case token.GTE:
g.emit(hbrt.PcOpGreaterEq)
case token.AND:
g.emit(hbrt.PcOpAnd)
case token.OR:
g.emit(hbrt.PcOpOr)
case token.DOLLAR:
g.emit(hbrt.PcOpInString)
}
}
func (g *generator) emitCall(e *ast.CallExpr) {
if ident, ok := e.Func.(*ast.IdentExpr); ok {
// Peephole: FieldGet(<int literal>) → PcOpFieldGet <idx>.
// Skips the entire PushSymbol + Function + Frame + RTL path in
// favor of a direct workarea field access. Huge win for WHERE
// predicates on scan loops where this is the per-row hot op.
if strings.EqualFold(ident.Name, "FieldGet") && len(e.Args) == 1 {
if lit, ok := e.Args[0].(*ast.LiteralExpr); ok && lit.Kind == token.INT {
if n, err := strconv.Atoi(lit.Value); err == nil && n > 0 && n <= 0xFFFF {
g.emit(hbrt.PcOpFieldGet)
g.emitU16(uint16(n))
return
}
}
}
// Peephole: AllTrim(FieldGet(<int literal>)) → PcOpFieldTrim <idx>.
// Fuses the character-field CHAR-trim normalization that
// SqlExprToPrg auto-wraps into one opcode, saving one Function
// dispatch + one intermediate string allocation per row.
if strings.EqualFold(ident.Name, "AllTrim") && len(e.Args) == 1 {
if inner, ok := e.Args[0].(*ast.CallExpr); ok {
if innerIdent, ok := inner.Func.(*ast.IdentExpr); ok &&
strings.EqualFold(innerIdent.Name, "FieldGet") &&
len(inner.Args) == 1 {
if lit, ok := inner.Args[0].(*ast.LiteralExpr); ok && lit.Kind == token.INT {
if n, err := strconv.Atoi(lit.Value); err == nil && n > 0 && n <= 0xFFFF {
g.emit(hbrt.PcOpFieldTrim)
g.emitU16(uint16(n))
return
}
}
}
}
}
g.emitString(hbrt.PcOpPushSymbol, strings.ToUpper(ident.Name))
g.emit(hbrt.PcOpPushNil)
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.emit(hbrt.PcOpFunction)
g.emitU16(uint16(len(e.Args)))
} else {
g.emitExpr(e.Func)
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.emit(hbrt.PcOpDo)
g.emitU16(uint16(len(e.Args)))
}
}
func (g *generator) emitCallStmt(e *ast.CallExpr) {
if ident, ok := e.Func.(*ast.IdentExpr); ok {
g.emitString(hbrt.PcOpPushSymbol, strings.ToUpper(ident.Name))
g.emit(hbrt.PcOpPushNil)
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.emit(hbrt.PcOpDo)
g.emitU16(uint16(len(e.Args)))
} else {
g.emitExpr(e.Func)
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.emit(hbrt.PcOpDo)
g.emitU16(uint16(len(e.Args)))
}
}
func (g *generator) emitAssign(a *ast.AssignExpr) {
// Compound operators (+=, -=, *=, /=, %=, ^=) need to fold the
// existing left-hand value with the right. Without this they got
// emitted as plain `:=`, dropping the accumulator: `n += i`
// behaved as `n := i`. So the FOR loop reduce idiom (e.g.
// `n := 0 ; FOR i := 1 TO 10 ; n += i ; NEXT`) returned only
// the LAST iteration's increment.
if a.Op != token.ASSIGN {
op, ok := compoundBinOp(a.Op)
if ok {
if ident, isIdent := a.Left.(*ast.IdentExpr); isIdent {
if idx, found := g.locals[strings.ToUpper(ident.Name)]; found {
g.emit(hbrt.PcOpPushLocal)
g.emitU16(uint16(idx))
g.emitExpr(a.Right)
g.emit(op)
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(idx))
return
}
}
}
}
if ident, ok := a.Left.(*ast.IdentExpr); ok {
if idx, found := g.locals[strings.ToUpper(ident.Name)]; found {
g.emitExpr(a.Right)
g.emit(hbrt.PcOpPopLocal)
g.emitU16(uint16(idx))
return
}
}
// Self field assignment
if send, ok := a.Left.(*ast.SendExpr); ok {
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
g.emitExpr(a.Right)
g.emitString(hbrt.PcOpSetSelfField, strings.ToUpper(send.Method))
return
}
}
g.emitExpr(a.Right)
g.emit(hbrt.PcOpPop)
}
// compoundBinOp maps an `<op>=` token to the binary opcode it
// produces against the left-hand value. Returns false for ASSIGN
// (the caller should take the plain-store path).
func compoundBinOp(k token.Kind) (byte, bool) {
switch k {
case token.PLUSEQ:
return hbrt.PcOpPlus, true
case token.MINUSEQ:
return hbrt.PcOpMinus, true
case token.STAREQ:
return hbrt.PcOpMult, true
case token.SLASHEQ:
return hbrt.PcOpDivide, true
case token.PERCENTEQ:
return hbrt.PcOpMod, true
case token.POWEREQ:
return hbrt.PcOpPower, true
}
return 0, false
}
func parseInt64(s string) int64 {
var v int64
for _, c := range s {
if c >= '0' && c <= '9' {
v = v*10 + int64(c-'0')
}
}
if len(s) > 0 && s[0] == '-' {
v = -v
}
return v
}
func parseFloat64(s string) float64 {
var v float64
var dec float64
inDec := false
for _, c := range s {
if c == '.' {
inDec = true
dec = 0.1
continue
}
if c >= '0' && c <= '9' {
if inDec {
v += float64(c-'0') * dec
dec *= 0.1
} else {
v = v*10 + float64(c-'0')
}
}
}
if len(s) > 0 && s[0] == '-' {
v = -v
}
return v
}