#!/usr/bin/env ruby require 'pp' # Program format: # uint8 dictSize: dictionary size # uint16 varSize: variables size # uint16 dstackSize: data stack size # uint16 lstackSize: link stack size # uint16*dictSize dictionary # uint8*varSize variables # uint8*dstackSize dataStack # uint16*lstackSize linkStack # opcodes # Opcodes # For opcodes with n field for number of bytes, the number of bytes is 2^n: # n = 0: 1 byte # n = 1: 2 bytes # n = 2: 4 bytes # n = 3: 8 bytes OPCODES = { # 0:6 n:2 - push 0. stack+n # OPCODE_PUSH_0 = 0x00 # 1:6 n:2 - push 1. stack+n # OPCODE_PUSH_1 = 0x01 # 2:6 n:2 - push n following bytes. stack+n push: 0x02, # 3:6 n:2 - pop n bytes. stack-n pop: 0x03, # 4:6 n:2 - dup n bytes # 5:6 n:2 - swap n bytes # 16:6 n:2 - + operator, n bytes. stack-n add: 0x10, # 17:6 n:2 - - operator, n bytes. stack-n sub: 0x11, # 18:6 n:2 - * operator, n bytes. stack-n mul: 0x12, # 19:6 n:2 - / operator, n bytes. stack-n div: 0x13, # 20:6 n:2 - | operator, n bytes. stack-n bitor: 0x14, # 21:6 n:2 - & operator, n bytes. stack-n bitand: 0x15, # 22:6 n:2 - ^ operator, n bytes. stack-n bitxor: 0x16, # 23:6 n:2 - < operator, n bytes. stack-n lt: 0x17, # 24:6 n:2 - > operator, n bytes. stack-n gt: 0x18, # 25:6 n:2 - = operator, n bytes. stack-n eq: 0x19, # 50:6 (uint8 dict_idx uint8 size) :def => 0x30, # 51:6 (uint8 dict_idx) - call word call: 0x31, # 52:6 - return ret: 0x32, # 0:1 c:1 33:6 (uint8 dist) # 1:1 c:1 33:6 (uint16 dist) # increment pc by dist # c = 0: if stack top is false. stack-n # c = 1: if stack top is true. stack-n cjmp_f: 0x33, # cjmp_t: 0x80 | 0x33, # 0:1 c:1 33:6 (uint8 dist) # 1:1 c:1 33:6 (uint16 dist) # decrement pc by dist # c = 0: if stack top is false. stack-n # c = 1: if stack top is true. stack-n # rcjmp_f: 0x34, # rcjmp_t: 0x80 | 0x34, # 0:1 d:1 35:6 (uint8 dist) # 1:1 d:1 35:6 (uint16 dist) # increment pc by dist + 1 or decrement pc by dist - 1 # d = 0: reverse jump (decrement) # d = 1: forward jump (increment) jmp: 0x35, # rjmp: 0x40 | 0x35, # n:2 3C:6 - print operator, n bytes. stack-n print: 0x3C, # 3F:6 - reset stacks reset_stacks: 0x3F } BUILTINS = { :+ => :add, :- => :sub, :* => :mul, :/ => :div, :| => :bitor, :& => :bitand, :^ => :bitxor, :< => :lt, :> => :gt, '='.to_sym => :eq, '.'.to_sym => :print } DSTACK_SIZE = 64 LSTACK_SIZE = 64 $dict = {} $var_dict = {} $intermediate = [] $program = [] def compile(src) words = src.scan(/(\(.*\)|[-\+]*0x[0-9a-fA-F]+|[-\+]*0b[01]+|[-\+]*\d+|\W|[a-zA-Z_]\w*)/) words.reject! {|x| x[0].chomp == ' ' || x[0].chomp == ''} words.map! {|x| x[0]} pp words # TODO: dictionary optimization # Count definitions. # Symbols with multiple definitions get put in the RAM dictionary, as they may be # redefined during program execution. # Other symbols get put in the direct-call dictionary. idx = 0 while(idx < words.length) tok = words[idx] idx += 1 if(tok.start_with?('(')) # comment elsif(tok == ':') # Procedure definition # assign a dictionary slot for name ident = words[idx].to_sym idx += 1 if($dict[ident] == nil) $dict[ident] = $dict.length end $intermediate.push({op: :def, sym: ident.to_sym}) elsif(tok == ';') $intermediate.push({op: :ret}) elsif((tok =~ /[-\+]*\d+/) != nil) val = tok.to_i $intermediate.push({op: :push, opts: 0x00, val: val}) # $intermediate.push({op: :push, opts: 0x40, val: val}) elsif(tok == 'if') $intermediate.push({op: :if}) elsif(tok == 'else') # set target of "if" jump (jump from if to else) $intermediate.push({op: :else}) elsif(tok == 'then') # set target of "else" jump (jump from end of if to end) $intermediate.push({op: :then}) elsif($dict.keys.include?(tok.to_sym)) # procedure call $intermediate.push({op: :call, sym: tok.to_sym}) elsif(BUILTINS.keys.include?(tok.to_sym)) $intermediate.push({op: BUILTINS[tok.to_sym]}) else puts "don't know what to do with word: #{tok}" end end # Generate bytecodes jumpstack = [] idx = 0 pc = 0 while(idx < $intermediate.length) op = $intermediate[idx] idx += 1 if(op[:op] == :def) jumpstack.push({idx: $program.length, addr: pc}) $program.push([OPCODES[:def], $dict[op[:sym]], 0]) pc += 3 elsif(op[:op] == :ret) $program.push([OPCODES[:ret]]) start = jumpstack.pop $program[start[:idx]][2] = pc - (start[:addr] + 0) pc += 1 elsif(op[:op] == :if) jumpstack.push({idx: $program.length, addr: pc}) $program.push([OPCODES[:cjmp_f], 0]) pc += 2 elsif(op[:op] == :else) $program.push([OPCODES[:jmp], 0]) start = jumpstack.pop $program[start[:idx]][1] = pc - (start[:addr] + 0) pc += 2 jumpstack.push({idx: $program.length, addr: pc}) elsif(op[:op] == :then) start = jumpstack.pop $program[start[:idx]][1] = pc - (start[:addr] + 0) elsif(op[:op] == :push) $program.push([OPCODES[:push] | op[:opts], (op[:val] & 0xFF)]) pc += 2 # $program.push([OPCODES[:push] | op[:opts], ((op[:val] >> 8) & 0xFF), (op[:val] & 0xFF)]) # pc += 3 elsif(op[:op] == :call) $program.push([OPCODES[:call], $dict[op[:sym]]]) pc += 2 else $program.push([OPCODES[op[:op]]]) pc += 1 end end pp $intermediate puts "" dict_size = $dict.length*2 vardict_size = $var_dict.length*2 prog_size = pc puts "" puts $program.map {|x| x.map{|c| "%02X"%c}.join(' ')}.join("\n") # pp $program puts "" puts $program.flatten.map {|x| "\\x%02X" % x}.join puts "" puts "Dictionary size: #{dict_size} B" puts "Variables size: #{vardict_size} B" puts "Program size: #{prog_size} B" puts "Total size: #{dict_size + vardict_size + prog_size} B" File.open('microforthtest.bin', 'w+b') {|fout| fout.write $program.flatten.pack('C*')} end # compile File.open('uforth_opcodes.h', 'w') {|fout| fout.puts "#ifndef MICROFORTH_OPCODES_H" fout.puts "#define MICROFORTH_OPCODES_H" fout.puts "" fout.puts OPCODES.keys.map{|x| "#define OPCODE_%s (0x%02X)"%[x.upcase, OPCODES[x]]}.join("\n") fout.puts "" fout.puts "#endif // MICROFORTH_OPCODES_H" } compile < if 0xFF else 0b00 then ; 42 4 * . 17 . foo . 5 big . 15 big . END