Files
zzg a22c99344e
ci / test (push) Canceled after 0s
initial commit
2026-09-25 22:12:55 -07:00

296 lines
8.0 KiB
Go

// Package codegen compiles an expression tree to x86-64 assembly.
package codegen
import (
"fmt"
"sort"
"github.com/zg/asm-calc/lexer"
"github.com/zg/asm-calc/parser"
"github.com/zg/asm-calc/x64"
)
// Compiler turns expression trees into instructions.
type Compiler struct {
prog x64.Program
// vars maps a variable name to its frame offset. Variables live on the
// stack frame rather than in registers, because the number of them is
// unbounded while registers are not.
vars map[string]int
nextOffset int
}
// New returns a compiler with no variables defined.
func New() *Compiler {
return &Compiler{vars: map[string]int{}, nextOffset: -8}
}
// Variables returns the defined variable names, sorted.
func (c *Compiler) Variables() []string {
out := make([]string, 0, len(c.vars))
for k := range c.vars {
out = append(out, k)
}
sort.Strings(out)
return out
}
// Offset returns a variable's frame offset.
func (c *Compiler) Offset(name string) (int, bool) {
off, ok := c.vars[name]
return off, ok
}
// Compile emits code leaving the expression's value in RAX.
func (c *Compiler) Compile(node parser.Node) (x64.Program, error) {
c.prog = nil
if err := c.emit(node); err != nil {
return nil, err
}
return c.prog, nil
}
func (c *Compiler) add(op x64.Op, comment string, operands ...x64.Operand) {
c.prog = append(c.prog, x64.Instruction{Op: op, Operands: operands, Comment: comment})
}
func (c *Compiler) emit(node parser.Node) error {
switch n := node.(type) {
case *parser.Number:
c.add(x64.MOV, fmt.Sprintf("literal %d", n.Value), x64.R(x64.RAX), x64.I(n.Value))
return nil
case *parser.Variable:
off, ok := c.vars[n.Name]
if !ok {
return fmt.Errorf("undefined variable %q", n.Name)
}
c.add(x64.MOV, "load "+n.Name, x64.R(x64.RAX), x64.M(x64.RBP, off))
return nil
case *parser.Assignment:
if err := c.emit(n.Value); err != nil {
return err
}
off, ok := c.vars[n.Name]
if !ok {
off = c.nextOffset
c.vars[n.Name] = off
c.nextOffset -= 8
}
c.add(x64.MOV, "store "+n.Name, x64.M(x64.RBP, off), x64.R(x64.RAX))
return nil
case *parser.UnaryOp:
if err := c.emit(n.Operand); err != nil {
return err
}
if n.Op == lexer.MINUS {
c.add(x64.NEG, "negate", x64.R(x64.RAX))
return nil
}
return fmt.Errorf("unsupported unary operator %s", n.Op)
case *parser.BinaryOp:
return c.emitBinary(n)
}
return fmt.Errorf("unsupported node %T", node)
}
// emitBinary compiles a binary operation.
//
// The strategy is: evaluate the left side into RAX, push it, evaluate the
// right side into RAX, then pop the left into RCX. Using the stack rather
// than a second register means arbitrarily deep expressions compile without
// running out of registers — at the cost of a push and pop per operation.
func (c *Compiler) emitBinary(n *parser.BinaryOp) error {
if n.Op == lexer.CARET {
return c.emitPower(n)
}
if err := c.emit(n.Left); err != nil {
return err
}
c.add(x64.PUSH, "save left operand", x64.R(x64.RAX))
if err := c.emit(n.Right); err != nil {
return err
}
// The right operand is in RAX and the left is on the stack, so the
// operands are the wrong way round for a non-commutative operation.
// Move the right out of the way, then pop the left into RAX.
c.add(x64.MOV, "right operand", x64.R(x64.RCX), x64.R(x64.RAX))
c.add(x64.POP, "restore left operand", x64.R(x64.RAX))
switch n.Op {
case lexer.PLUS:
c.add(x64.ADD, "add", x64.R(x64.RAX), x64.R(x64.RCX))
case lexer.MINUS:
c.add(x64.SUB, "subtract", x64.R(x64.RAX), x64.R(x64.RCX))
case lexer.STAR:
c.add(x64.IMUL, "multiply", x64.R(x64.RAX), x64.R(x64.RCX))
case lexer.SLASH:
// idiv divides the 128-bit RDX:RAX by its operand, so RDX must
// hold the sign extension of RAX first. Omitting cqo gives wrong
// answers for negative dividends rather than an obvious failure.
c.add(x64.CQO, "sign-extend rax into rdx:rax", nil...)
c.add(x64.IDIV, "divide, quotient in rax", x64.R(x64.RCX))
case lexer.PERCENT:
c.add(x64.CQO, "sign-extend rax into rdx:rax", nil...)
c.add(x64.IDIV, "divide, remainder in rdx", x64.R(x64.RCX))
c.add(x64.MOV, "take the remainder", x64.R(x64.RAX), x64.R(x64.RDX))
default:
return fmt.Errorf("unsupported binary operator %s", n.Op)
}
return nil
}
// constFold evaluates a node at compile time when it contains nothing but
// literals and arithmetic, reporting false when it depends on a variable.
//
// This is what lets 2^(3^2) and 2^(1+2) compile: the exponent must be known
// to unroll the multiplication, but it need not be written as a literal.
func constFold(node parser.Node) (int64, bool) {
switch n := node.(type) {
case *parser.Number:
return n.Value, true
case *parser.UnaryOp:
v, ok := constFold(n.Operand)
if !ok {
return 0, false
}
if n.Op == lexer.MINUS {
return -v, true
}
return 0, false
case *parser.BinaryOp:
l, ok := constFold(n.Left)
if !ok {
return 0, false
}
r, ok := constFold(n.Right)
if !ok {
return 0, false
}
switch n.Op {
case lexer.PLUS:
return l + r, true
case lexer.MINUS:
return l - r, true
case lexer.STAR:
return l * r, true
case lexer.SLASH:
// A constant division by zero is a compile-time error, not a
// value; refuse to fold it rather than panicking here.
if r == 0 {
return 0, false
}
return l / r, true
case lexer.PERCENT:
if r == 0 {
return 0, false
}
return l % r, true
case lexer.CARET:
if r < 0 || r > 64 {
return 0, false
}
result := int64(1)
for range r {
result *= l
}
return result, true
}
}
return 0, false
}
// emitPower expands x^n by repeated multiplication. The exponent must be
// known at compile time, since the instruction subset has no jumps to build
// a loop from.
func (c *Compiler) emitPower(n *parser.BinaryOp) error {
expValue, ok := constFold(n.Right)
if !ok {
return fmt.Errorf("exponent must be a constant expression")
}
exp := &parser.Number{Value: expValue}
if exp.Value < 0 {
// Integer arithmetic has no fractions, so a negative exponent
// would silently truncate to zero.
return fmt.Errorf("negative exponent %d has no integer result", exp.Value)
}
if exp.Value > 64 {
return fmt.Errorf("exponent %d is too large", exp.Value)
}
if exp.Value == 0 {
c.add(x64.MOV, "anything to the power 0 is 1", x64.R(x64.RAX), x64.I(1))
return nil
}
if err := c.emit(n.Left); err != nil {
return err
}
if exp.Value == 1 {
return nil
}
// Keep the base in RCX and multiply RAX by it repeatedly. Unrolling
// avoids needing jumps, which this instruction subset does not have.
c.add(x64.MOV, "keep the base", x64.R(x64.RCX), x64.R(x64.RAX))
for range exp.Value - 1 {
c.add(x64.IMUL, "raise to the power", x64.R(x64.RAX), x64.R(x64.RCX))
}
return nil
}
// Assemble wraps a program in a complete, assemblable NASM source file with
// a _start entry point that exits with the result.
func Assemble(prog x64.Program, frameSize int) string {
// Round the frame up to a multiple of 16: the ABI requires the stack
// stay 16-byte aligned, and an unaligned call crashes on any function
// using SSE.
if frameSize%16 != 0 {
frameSize += 16 - frameSize%16
}
var sb = &sbuilder{}
sb.line("; generated by asm-calc")
sb.line("bits 64")
sb.line("default rel")
sb.line("")
sb.line("section .text")
sb.line("global _start")
sb.line("")
sb.line("_start:")
sb.line(" push rbp")
sb.line(" mov rbp, rsp")
if frameSize > 0 {
sb.line(fmt.Sprintf(" sub rsp, %d", frameSize))
}
sb.line("")
sb.raw(prog.String())
sb.line("")
sb.line(" ; exit(rax & 0xff)")
sb.line(" mov rdi, rax")
sb.line(" mov rax, 60")
sb.line(" syscall")
return sb.String()
}
type sbuilder struct{ parts []string }
func (s *sbuilder) line(l string) { s.parts = append(s.parts, l+"\n") }
func (s *sbuilder) raw(r string) { s.parts = append(s.parts, r) }
func (s *sbuilder) String() string {
out := ""
for _, p := range s.parts {
out += p
}
return out
}
// FrameSize returns the bytes of stack the compiled variables need.
func (c *Compiler) FrameSize() int { return len(c.vars) * 8 }