688 lines
19 KiB
Ruby
Executable File
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
|