first pass transpiler implementation - pb2c.rb

- Modified version of Gary Bernhardt's example compiler from scratch
- Transpiles absolute barebones PowerBasic to C

Compile with `./pb2c.rb | gcc -xc -`
This commit is contained in:
Andrew 2025-03-26 00:00:52 -05:00
parent 771c39441e
commit acbdf1de5f
3 changed files with 171 additions and 0 deletions

3
hello.bas Normal file
View File

@ -0,0 +1,3 @@
Function PBMain() as Long
PRINT "Hello, world!"
End Function

6
neopb.c Normal file
View File

@ -0,0 +1,6 @@
#include <stdio.h>
int parse();
int main() {
}

162
pb2c.rb Executable file
View File

@ -0,0 +1,162 @@
#!/usr/bin/ruby
class Tokenizer
TOKEN_TYPES = [
[:function, /\bfunction\b/i],
[:sub, /\bsub\b/i],
[:end, /\bend\b/i],
[:as, /\bas\b/i],
[:typename, /\blong\b/i],
[:identifier, /\b[a-zA-Z]+\b/],
[:integer, /\b[0-9]+\b/],
[:string, /".*"/],
[:oparen, /\(/],
[:cparen, /\)/],
[:comma, /,/],
[:quote, /'/],
]
def initialize(code)
@code = code
end
def tokenize
tokens = []
begin
until @code.empty?
tokens << tokenize_one_token
@code = @code.strip
end
rescue RuntimeError => e
puts tokens.join("\n")
raise
end
tokens
end
def tokenize_one_token
TOKEN_TYPES.each do |type, re|
re = /\A(#{re})/
if @code =~ re
value = $1
@code = @code[value.length..-1]
return Token.new(type, value)
end
end
raise RuntimeError.new(
"Couldn't match token on #{@code.inspect}")
end
end
Token = Struct.new(:type, :value)
class Parser
def initialize(tokens)
@tokens = tokens
end
def parse
parse_function
end
def parse_function
consume(:function)
name = consume(:identifier).value
arg_names = parse_arg_names
consume(:as)
rtype = consume(:typename).value
body = parse_expr
consume(:end)
consume(:function)
FunctionNode.new(name, rtype, arg_names, body)
end
def parse_arg_names
arg_names = []
consume(:oparen)
if peek(:identifier)
arg_names << consume(:identifier).value
while peek(:comma)
consume(:comma)
arg_names << consume(:identifier).value
end
end
consume(:cparen)
arg_names
end
def parse_expr
if peek(:integer)
parse_integer
elsif peek(:string)
parse_string
elsif peek(:identifier) && peek(:oparen, 1)
parse_call
elsif peek(:identifier) && peek(:string, 1)
parse_stmt
else
parse_var_ref
end
end
def parse_stmt
name = consume(:identifier).value
arg_exprs = consume(:string).value
CallNode.new(name, arg_exprs)
end
def peek(expected_type, offset=0)
@tokens.fetch(offset).type == expected_type
end
def consume(expected_type)
token = @tokens.shift
if token.type == expected_type
token
else
raise RuntimeError.new(
"Expected token type #{expected_type.inspect} but got #{token.type.inspect}")
end
end
end
FunctionNode = Struct.new(:name, :type, :arg_names, :body)
StringNode = Struct.new(:value)
CallNode = Struct.new(:name, :arg_exprs)
class Generator
def generate(node)
case node
when FunctionNode
"%s %s(%s) { return %s ; }" % [
node.type.downcase,
node.name,
node.arg_names.join(','),
generate(node.body),
]
when CallNode
"%s(%s)" % [
node.name,
node.arg_exprs
]
when StringNode
node.value
else
raise RuntimeError.new("Unexpected node type: #{node.class}")
end
end
end
tokens = Tokenizer.new(File.read("hello.bas")).tokenize
#puts "Tokens:\n"
#puts tokens.join("\n")
tree = Parser.new(tokens).parse
#puts "\nAST:\n"
#puts tree
RUNTIME = "#include <stdio.h>\n#define PRINT(a) printf(a)\n"
CMAIN = "int main(void) { PBMain(); return 0; }"
generated = Generator.new.generate(tree)
#puts "\nGenerated:\n"
#puts generated
#puts "\nGenerated with preamble/postamble:\n"
puts [RUNTIME, generated, CMAIN].join("\n")