[NEW] Parse assignment statements. Added template and test code.

This commit is contained in:
sjs 2009-05-13 23:20:51 -07:00
parent ff2b68a8f2
commit 268c6f6c29
5 changed files with 191 additions and 66 deletions

View file

@ -1,7 +1,5 @@
build: build:
# ruby test.rb '5-5+3-2-1' ruby test.rb test.code
# ruby test.rb '5*3-5*2+3-9/3-1*1-8/2'
ruby test.rb '5*(3-5)*2+2-9/3-8/2-4*(5+5+5)'
nasm -f elf -g -o test.o test.asm nasm -f elf -g -o test.o test.asm
ld -o test test.o ld -o test test.o
# $? indicates success as per unix convention # $? indicates success as per unix convention

View file

@ -9,19 +9,31 @@
class ParseError < StandardError; end class ParseError < StandardError; end
class Compiler class Compiler
def initialize(input=STDIN, output=STDOUT) attr_reader :data, :bss, :code
def initialize(input=STDIN)
@look = '' # next lookahead char @look = '' # next lookahead char
@input = input # stream to read from @input = input # stream to read from
@output = output # stream to write to @data = '' # data section
@bss = '' # bss section
@code = '' # code section
# seed the lexer # seed the lexer
get_char get_char
end end
def parse
statement until eof?
[@data, @bss, @code]
end
# Read the next character from the input stream # Read the next character from the input stream
def get_char def get_char
@look = @input.getc @look = if @input.eof?
@look = @look.chr if @look nil
else
@input.readbyte.chr
end
end end
# Report error and halt # Report error and halt
@ -34,7 +46,7 @@ class Compiler
if eof? if eof?
raise ParseError, "Premature end of file, expected: #{what}." raise ParseError, "Premature end of file, expected: #{what}."
else else
raise ParseError, "Expected: #{what}, got: #{@look}." raise ParseError, "Expected: #{what}, got: #{@look} (##{@look[0]})."
end end
end end
@ -48,18 +60,18 @@ class Compiler
end end
# Recognize an alphabetical character # Recognize an alphabetical character
def is_alpha(char) def alpha?(char)
('A'..'Z') === char.upcase ('A'..'Z') === char.upcase
end end
# Recognize a decimal digit # Recognize a decimal digit
def is_digit(char) def digit?(char)
('0'..'9') === char ('0'..'9') === char
end end
# Get an identifier # Get an identifier
def get_name def get_name
expected('identifier') unless is_alpha(@look) expected('identifier') unless alpha?(@look)
c = @look c = @look
get_char get_char
return c return c
@ -67,29 +79,60 @@ class Compiler
# Get a number # Get a number
def get_num def get_num
expected('integer') unless is_digit(@look) expected('integer') unless digit?(@look)
c = @look c = @look
get_char get_char
return c return c
end end
# Print a tab followed by a string and a newline # Define a constant in the .data section.
def equ(name, value)
@data << "#{name}\tequ #{value}"
end
# Define a variable with the given name and size (in dwords).
def var(name, dwords=1)
@bss << "#{name}: resd #{dwords}\n"
end
# Emit a line of code wrapped between a tab and a newline.
def emit(s) def emit(s)
@output.puts("\t#{s}") @code << "\t#{s}\n"
end
# Parse and translate an identifier or function call.
def identifier
name = get_name
if @look == '('
# function call
match('(')
match(')')
call(name)
else
# variable access
mov("eax", "dword [#{name}]")
end
end end
# Parse and translate a single factor. Result is in eax. # Parse and translate a single factor. Result is in eax.
def factor def factor
if @look == '(' case
when @look == '('
match('(') match('(')
expression expression
match(')') match(')')
when alpha?(@look)
identifier
when digit?(@look)
mov("eax", get_num)
else else
emit("mov eax, #{get_num}") expected("a number, identifier, or an expression wrapped in parens")
end end
end end
# Parse and translate a single term. Result is in eax. # Parse and translate a single term (factor or mulop). Result is in
# eax.
def term def term
factor # Result in eax. factor # Result in eax.
while mulop? while mulop?
@ -97,25 +140,25 @@ class Compiler
# multiply & divide. Because they leave their results in eax # multiply & divide. Because they leave their results in eax
# associativity works. Each interim result is pushed on the # associativity works. Each interim result is pushed on the
# stack here. # stack here.
emit("push eax") push("eax")
case @look if @look == '*'
when '*': multiply multiply
when '/': divide
else else
expected('Multiplication or division operator (* or /)') divide
end end
emit("add esp, 4") # Remove the 1st factor from the stack.
add("esp", 4) # Remove the 1st factor from the stack.
end end
end end
# Parse and translate a mathematical expression of terms. Result is # Parse and translate a general expression of terms. Result is
# in eax. # in eax.
def expression def expression
if addop? if addop?
# Clear eax simulating a zero before unary plus and minus # Clear eax simulating a zero before unary plus and minus
# operations. # operations.
emit("xor eax, eax") xor("eax", "eax")
else else
term # Result is in eax. term # Result is in eax.
end end
@ -125,24 +168,49 @@ class Compiler
# subtract. Because they leave their results in eax # subtract. Because they leave their results in eax
# associativity works. Each interim result is pushed on the # associativity works. Each interim result is pushed on the
# stack here. # stack here.
emit("push eax") push("eax")
case @look if @look == '+'
when '+': add add
when '-': subtract
else else
expected('Addition or subtraction operator (+ or -)') subtract
end end
emit("add esp, 4") # Remove 1st term (a) from the stack.
add("esp", 4) # Remove 1st term (a) from the stack.
end end
end end
# Parse an assignment statement. Value is in eax.
def assignment
name = get_name
match('=')
expression
var(name)
mov("dword [#{name}]", "eax")
end
# Parse one or more newlines.
def newline
if @look == "\n" || @look == "\r"
get_char while @look == "\n" || @look == "\r"
else
expected('newline')
end
end
# Parse an assignment expression followed by a newline.
def statement
assignment
newline
end
# Parse an addition operator and the 2nd term (b). The result is # Parse an addition operator and the 2nd term (b). The result is
# left in eax. The 1st term (a) is expected on the stack. # left in eax. The 1st term (a) is expected on the stack.
def add def add
match('+') match('+')
term # Result is in eax. term # Result is in eax.
emit("add eax, [esp]") # Add a to b. add('eax', '[esp]') # Add a to b.
end end
# Parse a subtraction operator and the 2nd term (b). The result is # Parse a subtraction operator and the 2nd term (b). The result is
@ -150,8 +218,8 @@ class Compiler
def subtract def subtract
match('-') match('-')
term # Result is in eax. term # Result is in eax.
emit("sub eax, [esp]") # Subtract a from b (this is backwards). sub('eax', '[esp]') # Subtract a from b (this is backwards).
emit("neg eax") # Fix things up. -(b-a) == a-b neg('eax') # Fix things up. -(b-a) == a-b
end end
# Parse an addition operator and the 2nd term (b). The result is # Parse an addition operator and the 2nd term (b). The result is
@ -159,7 +227,7 @@ class Compiler
def multiply def multiply
match('*') match('*')
factor # Result is in eax. factor # Result is in eax.
emit("imul dword [esp]") # Multiply a by b. imul('dword [esp]') # Multiply a by b.
end end
# Parse a division operator and the divisor (b). The result is # Parse a division operator and the divisor (b). The result is
@ -167,11 +235,17 @@ class Compiler
def divide def divide
match('/') match('/')
factor # Result is in eax. factor # Result is in eax.
emit("xchg eax, [esp]") # Swap the divisor and dividend into xchg('eax', '[esp]') # Swap the divisor and dividend into
# the correct places. # the correct places.
emit("idiv dword [esp]") # Divide a (eax) by b ([esp]). idiv('dword [esp]') # Divide a (eax) by b ([esp]).
end end
#######
private
#######
def eof? def eof?
@input.eof? && @look.nil? @input.eof? && @look.nil?
end end
@ -183,4 +257,43 @@ class Compiler
def mulop? def mulop?
@look == '*' || @look == '/' @look == '*' || @look == '/'
end end
# Some asm methods for convenience and arity checks.
def mov(dest, src)
emit("mov #{dest}, #{src}")
end
def add(dest, src)
emit("add #{dest}, #{src}")
end
def sub(dest, src)
emit("sub #{dest}, #{src}")
end
def imul(op)
emit("imul #{op}")
end
def idiv(op)
emit("idiv #{op}")
end
def push(reg)
emit("push #{reg}")
end
def call(label)
emit("call #{label}")
end
def neg(reg)
emit("neg #{reg}")
end
def xchg(op1, op2)
emit("xchg #{op1}, #{op2}")
end
end end

12
template.asm Normal file
View file

@ -0,0 +1,12 @@
GLOBAL _start
SECTION .data
{data}
SECTION .bss
{bss}
SECTION .text
_start:
{code}
;; The result in eax is the exit code, move it to ebx.
mov ebx, eax
mov eax, 1 ; _exit syscall
int 0x80 ; call Linux

5
test.code Normal file
View file

@ -0,0 +1,5 @@
a=9
5*(3-5)*2+2-9/3-8/2-4*(5+5+5)
a-1
x()+1
-1

53
test.rb
View file

@ -1,39 +1,36 @@
require 'compiler' require 'compiler'
require 'stringio' require 'stringio'
MaxRetries = 1
def error(msg) STDERR.puts(msg) end def error(msg) STDERR.puts(msg) end
# Main program def parse(input)
def main compiler = Compiler.new(input)
retries = 0 compiler.parse # tuple of [data, bss, code]
input = StringIO.new(ARGV[0] || '5-5')
output = StringIO.new rescue ParseError => e
parse = Compiler.new(input, output)
until parse.eof?
begin
parse.expression
rescue ParseError => e
error("[error] #{e.message}") error("[error] #{e.message}")
if retries < MaxRetries
retries += 1
error("Skipping token...")
parse.get_char
retry
else
error("Aborting!") error("Aborting!")
break exit(1)
end
end
end
output.string
end end
code = main def interpolate(template, data)
File.open("test.asm", "w") do |f| data.inject(template) do |template, mapping|
f.puts(File.readlines("prologue.asm")) token, replacement = *mapping
f.puts(code) template.sub("{#{token}}", replacement)
f.puts(File.readlines("epilogue.asm")) end
end end
def main(arg)
input = if File.readable?(arg)
File.open(arg)
else
# StringIO.new("5*(3-5)*2+2-9/3-8/2-4*(5+5+5)\n")
StringIO.new("a=9\n")
end
data, bss, code = *parse(input)
template = File.read("template.asm")
asm = interpolate(template, :data => data, :bss => bss, :code => code)
File.open("test.asm", "w") { |f| f.puts(asm) }
end
main(ARGV[0].to_s)