stack-machine/assemble.rb

688 lines
19 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# vim: set sw=3 tw=80:
require 'base64'
POS_INT_REGEX = /(?:0[0-7]+|0b[01]+|0x[0-9a-fA-F]+|[0-9]+)/
# Make sure input is treated as a signed 32-bit number
def signed32(n)
if n and (n < 0 or n >= 0x80000000)
-(-n & 0xffffffff)
else
n
end
end
def split_immed(n)
result = []
while (n < -16) || (n > 15)
result = [ 0b10000000 | (n & 0x7f) ] + result
n >>= 7
end
[ 0b00100000 | (n & 0x1f) ] + result
end
$labels = {}
$forward_labels = {}
class Label
attr_accessor :name
def initialize(name, pc=nil)
unless name =~ /^\.*[a-zA-Z_]\w*$/
raise StandardError.new("Bad label name '#{name}' at #{$file}:#{$line}")
end
@name, @pc = name, pc
@export, @global = false, false
@alias = nil
end
def export=(x) @export = x ? true : false; end
def export?() @export; end
def global=(x) @global = x ? true : false; end
def global?() @global; end
def local?() (@name =~ /^(\.+)/) ? $1.length : false; end
def level() self.local? || 0; end
def pc=(x) @pc = x; end
def pc() @alias ? @alias.pc : @pc; end
def alias() @alias; end
def alias?() @alias ? true : false; end
def Label.get(name)
lbl = $labels[name] || $forward_labels[name]
if not lbl
lbl = Label.new(name)
$forward_labels[name] = lbl
end
return lbl
end
def Label.define(name)
if $labels.has_key? name
$stderr.puts "WARNING: Redefining label '#{name}'."
end
lbl = $forward_labels[name] || Label.new(name)
$forward_labels.delete name
$labels[name] = lbl
return lbl
end
def Label.alias(label, name)
if $labels.has_key? name
$stderr.puts "WARNING: Redefining label '#{name}'."
end
lbl = $forward_labels[name] || Label.new(name)
lbl.instance_variable_set(:@alias, label)
$forward_labels.delete name
$labels[name] = lbl
return lbl
end
def Label.in_file_scope()
old_labels, old_forwards = $labels.dup, $forward_labels.dup
# Local references end at file boundaries
$labels.delete_if { |k,v| not v.global? }
$forward_labels.delete_if { |k,v| not v.global? }
yield
# Local references end at file boundaries
$labels.delete_if { |k,v| not v.global? }
$forward_labels.delete_if { |k,v| not v.global? }
$labels = old_labels.merge($labels)
$forward_labels = old_forwards.merge($forward_labels)
end
end
class Instruction
def initialize(labels, pc, opcode, args)
@labels = labels.map do |name|
lbl = Label.define(name)
$labels.delete_if { |k,v| v.level > lbl.level }
lbl
end
@type = opcode.downcase.to_sym
@args = args
@align = 1
valid = true
case @type
when :nop, :rshift, :nip, :and, :or,
:xor, :swap, :dup, :add, :sub
valid = args.empty? || ((args.length == 1) && (args[0] =~ /return/i))
@return = !args.empty?
when :load, :store
valid = args.empty? || ((args.length == 1) && (args[0] =~ /byte/i))
@byte_op = !args.empty?
when :return
valid = args.empty?
@type = :nop
@return = true
when :rdrop, :pop, :pushpc, :push, :rdup, :mult, :over, :nlz
valid = args.empty?
when :immed
if (args.length == 1) and (args[0] =~ /^-?#{POS_INT_REGEX}$/)
# Integer constant
valid, @value = true, signed32(args[0].to_i(0))
elsif (args.length == 1) and (args[0] =~ /^'.'$/)
# Character constant
valid, @value = true, args[0][1].ord
else
valid, @target = true, Label.get(args[0])
end
when :jump, :call
@rel, @target, @cc, @drop = false, nil, :always, false
args.each do |arg|
case arg
when /^rel$/i
@rel = true
when /^(never|eq|lt|gt|always|ne|ge|le)$/i
@cc = $1.downcase.to_sym
when /^drop$/i
@drop = true;
else
if @target
raise StandardError.new("Syntax error: #{opcode} #{args.join(' ')}")
else
@target = Label.get(arg)
end
end
end
when :drop
@type = :jump
@rel = false
@target = nil
@cc = :never
@drop = false
when :drop2
@type = :jump
@target = nil
@rel = false
@cc = :never
@drop = true
when :merge
valid = (args.length == 1) && (args[0] =~ /^#{POS_INT_REGEX}$/)
@value = args[0].to_i(0) if valid
valid &&= (@value >= 0 and @value <= 127)
when :resb
valid = (args.length == 1) && (args[0] =~ /^#{POS_INT_REGEX}$/)
@immed = [ 0 ] * args[0].to_i(0) if valid
when :align
valid = (args.length == 1) && (args[0] =~ /^#{POS_INT_REGEX}$/)
@align = args[0].to_i(0) if valid
valid &&= (@align >= 1)
when :byte
valid = true
@immed = []
args.each do |arg|
case arg
when /^-?#{POS_INT_REGEX}$/
v = arg.to_i(0)
valid &&= (-128..255).member? v
@immed << (v & 0xff) if valid
when /^'(.)'$/
@immed << (arg[1].ord & 0xff) if valid
when /^".*"$/
arg[1..-2].scan(/([^\\"])|(\\")|(\\\\)/) do |a,b,c|
a = '"' if b
a = '\\' if c
@immed << (a[0].ord & 0xff)
end
else
raise StandardError.new("Unsupported BYTE operand.")
end
end
when :word
valid = true
@immed = []
@align = 4
args.each do |arg|
case arg
when /^-?#{POS_INT_REGEX}$/
v = arg.to_i(0)
# Convert long to unsigned chars w/ big-endian byte order
@immed += [v].pack("N").unpack("C*")
else
raise StandardError.new("WORD only supports integer operands.")
end
end
else
raise StandardError.new("Syntax error: #{opcode} #{args.join(' ')}")
end
if not valid
raise StandardError.new("Invalid instruction: #{opcode} #{args.join(' ')}")
end
reflow(pc)
end
def reflow(pc)
if pc >= $options[:ram_size]
raise StandardError.new("No more room in Block RAM.")
end
@align_bytes = (-pc) % @align
@pc = pc + @align_bytes
old_bytes = @bytes
@labels.each { |lbl| lbl.pc = @pc }
if @target
@value = signed32(@target.pc)
if @value
if @rel
# Known relative; PC of jump/call depends on immed. length
diff = @value - (@pc + 6)
im_size = split_immed(diff).length
diff = @value - (@pc + im_size + 1)
@immed = split_immed(diff)
if @immed.length > im_size
im_size = @immed.length
diff = @value - (@pc + im_size + 1)
@immed = split_immed(diff)
end
if @immed.length > im_size
raise StandardError.new("Relative target offset won't converge!")
end
else
# Absolute known address
@immed = split_immed(@value)
end
else
# Unknown address; reserve maximum space that may be required
@immed = split_immed(-($max_pc+1))
end
elsif @value
@immed = split_immed(@value)
end
if @type == :align
@bytes = 0
elsif not @immed
@bytes = 1
elsif @type == :jump or @type == :call
@bytes = @immed.length + 1
else
@bytes = @immed.length
end
@bytes += @align_bytes
if old_bytes != @bytes
$layout_changed = true
end
end
def bytes() @bytes; end
def finalize_target
if @target
@value = signed32(@target.pc)
if not @value
raise StandardError.new("Unknown label: '#{@target.name}'")
else
if @rel
@immed = split_immed(@value - (@pc + @bytes))
else
@immed = split_immed(@value)
end
end
if @type == :immed
bytes = @immed.length
else
bytes = @immed.length + 1
end
if @bytes < bytes
raise StandardError.new("Insufficient space for target address.")
end
end
end
OPCODES = {
:nop => 0b00000000,
:rshift => 0b00000010,
:add => 0b00000100,
:sub => 0b00000110,
:nip => 0b00001000,
:and => 0b00001010,
:or => 0b00001100,
:xor => 0b00001110,
:swap => 0b00010000,
:dup => 0b00010010,
:over => 0b00010100,
:rdup => 0b00010101,
:nlz => 0b00010110,
:mult => 0b00010111,
:load => 0b00011000,
:store => 0b00011010,
:rdrop => 0b00011100,
:pop => 0b00011101,
:pushpc => 0b00011110,
:push => 0b00011111,
:jump => 0b01000000,
:call => 0b01100000
}.freeze
CC_MAP = {
:never => 0b000,
:eq => 0b001,
:lt => 0b010,
:gt => 0b011,
:always => 0b100,
:ne => 0b101,
:ge => 0b110,
:le => 0b111
}.freeze
def to_bytes
finalize_target
bytes = [0] * @align_bytes
case @type
when :align
# no opcode
nil
when :nop, :rshift, :add, :sub, :nip,
:and, :or, :xor, :swap, :dup
bytes << (OPCODES[@type] | (@return ? 1 : 0))
when :load, :store
bytes << (OPCODES[@type] | (@byte_op ? 1 : 0))
when :rdrop, :pop, :pushpc, :push, :rdup, :mult, :over, :nlz
bytes << OPCODES[@type]
when :immed, :resb, :byte, :word
bytes += @immed
when :jump, :call
result = OPCODES[@type]
result |= 0b10000 if @rel
result |= (CC_MAP[@cc]<<1) if @cc
result |= 0b00001 if @drop
# Insert NOPs if immed is shorter than allocated
pad = @immed && ([0] * (@bytes - (@immed.length + 1)))
bytes += pad if pad
bytes += @immed if @immed
bytes << result
when :merge
bytes << (0b10000000 | (@value & 0x7f))
else
raise StandardError.new("Don't know how to output a #{@type} opcode.")
end
bytes
end
def to_s
finalize_target
s = @labels.map {|lbl| lbl.name + ":\n" }.join('')
s << " #{ADDR_FMT % @pc} "
case @type
when :align
s << ('%s %d' % [ @type, @align ])
when :nop, :rshift, :add, :sub, :nip,
:and, :or, :xor, :swap, :dup
s << @type.to_s
s << ' return' if @return
when :load, :store
s << @type.to_s
s << ' byte' if @byte_op
when :rdrop, :pop, :pushpc, :push, :rdup, :mult, :over, :nlz
s << @type.to_s
when :immed
s << @type.to_s
s << (' 0x%-8X' % (@value & 0xffffffff))
s << ' # ' << @value.to_s << ' decimal'
when :jump, :call
s << @type.to_s
s << ' ' << @target.name if @target
s << ' (' << (ADDR_FMT % @value) << ')' if @value
s << ' rel' if @rel
s << ' ' << @cc.to_s if @cc
s << ' drop' if @drop
when :merge
s << @type.to_s << (' 0b%07b' % @value)
when :resb
s << @type.to_s << ' ' << @immed.length.to_s
when :byte
s << @type.to_s << ' ' << @immed.join(' ')
when :word
words = @immed.pack('C*').unpack('N*').map { |w| '0x%08x' % w }
s << @type.to_s << ' ' << words.join(' ')
else
raise StandardError.new("Don't know how to output a #{@type} opcode.")
end
s
end
end
def parse_options(argv)
options = {
:include_path => [],
:files => []
}
ARGV.each_with_index do |option,idx|
case option
when /^--origin=(#{POS_INT_REGEX})$/
options[:origin] = $1.to_i(0)
when /^--format=(binary|hex)$/
options[:format] = $1.to_sym
options[:pad] = true unless options.has_key? :pad
when /^--format=(raw|write|base64)$/
options[:format] = $1.to_sym
when /^-p|--pad$/
options[:pad] = true
when /^-r|--no-pad|--raw$/
options[:pad] = false
when /^--(?:code|disassembly)=(.*)$/
options[:code_file] = ($1=='-') ? $stdout : File.open($1, "w")
when /^--sym(?:bol)?s=(.*)$/
options[:symbol_file] = ($1=='-') ? $stdout : File.open($1, "w")
when /^--ram(?:-size)?=(#{POS_INT_REGEX})$/
options[:ram_size] = $1.to_i(0)
when /^--partial$/
options[:code_file] ||= $stdout
options[:partial] = true
when /^(?:--includes=|-I)(.*)$/
options[:include_path] << File.expand_path($1)
when /^--depends$/
options[:depends_file] = $stdout
when /^--$/
options[:files] += ARGV[(idx+1) .. -1]
break
when /^-/
raise StandardError.new("Unknown option '#{option}'.")
else
options[:files] << option
end
end
return options
end
$options = {
# Defaults
:origin => 0x400,
:format => :base64,
:pad => false,
:code_file => nil,
:symbol_file => nil,
:ram_size => (4 * 2048),
:partial => false,
:depends_file => false
}.merge(parse_options(ARGV)).freeze
$pc = $options[:origin]
$max_pc = $options[:ram_size] - 1
$instructions = []
$accum_labels = []
OPERAND = /(?:'.'|"(?:[^\\"]|\\"|\\\\)*"|\S+)/
ADDR_FMT = "0x%0#{$max_pc.to_s(16).length}x"
$included_files = []
def read_asm_file(fname)
old_file, old_line, $file, $line = $file, $line, fname, 1
Label.in_file_scope do
File.open(fname).each_line do |ln|
ln.sub!(/#.*$/,'')
ln.split(';').each do |part|
while part.sub!(/^\s*(\S+)\s*:\s*/, '')
$accum_labels << $1
end
case part
when /^\s*$/
nil # Empty line or comment
when /^\s*include\s+"([^"]+)"\s*$/
inc_name = $1
search_path = [File.dirname(fname)] + $options[:include_path]
found = nil
search_path.each do |dir|
try_name = File.expand_path(inc_name, dir)
if File.exist? try_name
found = try_name
break
end
end
if found
$included_files << found
read_asm_file(found)
else
raise StandardError.new("Unable to locate include file '#{inc_name}'.")
end
when /^\s*export\s+(#{OPERAND}(?:\s*#{OPERAND})*)\s*$/
$1.scan(OPERAND).each do |sym|
if sym =~ /^\./
raise StandardError.new("Can't export local label '#{sym}'.")
else
lbl = Label.get(sym)
lbl.export = true
lbl.global = true
end
end
when /^\s*import\s+(#{OPERAND}(?:\s*#{OPERAND})*)\s*$/
$1.scan(OPERAND).each do |sym|
if sym =~ /^\./
raise StandardError.new("Can't import local label '#{sym}'.")
else
Label.get(sym).global = true
end
end
when /^\s*(#{OPERAND})\s+eql\s+(#{OPERAND})\s*$/i
name, value = $1, $2
if value =~ /^-?#{POS_INT_REGEX}$/
value = value.to_i(0)
unless $labels[name] and $labels[name].pc == value
lbl = Label.define(name)
lbl.pc = value
lbl.global = !lbl.local?
end
else
lbl = Label.alias(Label.get(value), name)
lbl.global = !lbl.local?
end
when /^\s*(\S+)((?:\s+#{OPERAND})*)\s*$/
ins = Instruction.new($accum_labels,$pc,$1,$2.scan(/#{OPERAND}/))
$accum_labels = []
$instructions << ins
$pc += ins.bytes
end
end
$line += 1
end
end
$file, $line = old_file, old_line
end
begin
$options[:files].each { |fname| read_asm_file(fname) }
rescue => e
$stderr.puts "Error at #{$file} line #{$line}:"
$stderr.puts e
$stderr.puts e.backtrace
end
if $options[:depends_file]
$options[:depends_file].puts $included_files
exit
end
if $instructions.empty? or not $accum_labels.empty?
ins = Instruction.new($accum_labels, $pc, "align", ["1"])
$instructions << ins
$pc += ins.bytes
end
5.times do
$max_pc = $pc
$pc = $options[:origin]
$layout_changed = false
$instructions.each { |ins| ins.reflow($pc); $pc += ins.bytes }
break unless $layout_changed
end
if $layout_changed
$stderr.puts "WARNING: Opcode layout may be suboptimal."
end
if $options[:code_file]
$instructions.each do |ins|
$options[:code_file].puts ins
end
end
$max_length = $labels.values.inject(0) do |max, lbl|
len = lbl.name.length
if lbl.export? and (len > max)
len
else
max
end
end
if $options[:symbol_file]
file = $options[:symbol_file]
($labels.values.sort {|a,b| a.pc <=> b.pc }).each do |lbl|
if lbl.pc and lbl.export?
file.printf "%-*s EQL 0x%08x\n", $max_length, lbl.name, lbl.pc
end
end
end
unless $options[:partial]
bytes = ($instructions.map { |ins| ins.to_bytes }).flatten
if $options[:pad] and [ :raw, :binary, :hex ].member? $options[:format]
bytes = ([0] * $options[:origin]) + bytes
bytes += ([0] * ($options[:ram_size] - bytes.length))
end
file = $stdout
case $options[:format]
when :raw
file.write bytes.pack('C*')
when :base64
file.puts('STORE %X' % $options[:origin])
file.print Base64.encode64(bytes.pack('C*'))
file.puts '.'
($labels.values.sort {|a,b| a.name <=> b.name }).each do |lbl|
if lbl.pc and lbl.export?
file.printf "# %-*s EQL 0x%08x\n", $max_length, lbl.name, lbl.pc
end
end
when :binary, :hex, :write
pc = $options[:origin]
bytes = ([0] * (pc & 0x3)) + bytes
pc &= ~0x3
words = (bytes+[0,0,0]).pack('C*').unpack('N*')
addr_len = $max_pc.to_s(16).length
words.each do |w|
if $options[:format] == :write
file.printf "WRITE %08X %0*X\n", w, addr_len, pc
elsif $options[:format] == :binary
file.printf "%032b\n", w
else # hex
file.printf "%08X\n", w
end
pc += 4
end
else
raise StandardError.new("Unknown output format")
end
end