Compare commits

...

26 Commits

Author SHA1 Message Date
bc798f11fe Add README. 2018-09-19 17:51:12 -07:00
d40ea2a8ae Add some more complex programs. 2018-08-08 22:22:06 -07:00
eb32577d38 Add a clear function. 2018-08-08 22:21:57 -07:00
e740fd7688 Optimizer is still broken. 2018-08-08 22:21:50 -07:00
5ce0d41a53 Add a sprite draw function. 2018-08-08 21:47:47 -07:00
b79d717f1a Generate sprite data. 2018-08-06 23:43:46 -07:00
60c320dfea Add sprites to source code and table. 2018-08-06 21:51:53 -07:00
7fe5cec941 Merge branch 'master' of https://dev.danilafe.com/Chip-8-Wizardry/chalk 2018-08-05 20:32:32 -07:00
89709fef97 Split symbol table into have a separate hash for each entry type. 2018-08-05 20:32:10 -07:00
45f3e51890 Add a sprite representation. 2018-08-05 20:27:43 -07:00
61fb44bbce Add more small test programs. 2018-08-05 14:48:51 -07:00
230a50c532 Dedicate a throwaway register. 2018-08-05 13:36:54 -07:00
a96d503095 Properly throw away unused expressions in blocks. 2018-08-05 13:35:16 -07:00
caaae87344 Add non-empty test programs. 2018-08-05 13:34:12 -07:00
86ee6557cf Fix bug in codegen. 2018-08-05 13:11:12 -07:00
a7e24c059b Begin working on a testing script. 2018-08-05 00:59:27 -07:00
2ed2a4932c Add an output file parameter. 2018-08-04 13:49:29 -07:00
1150e2b3ef Add log level. 2018-08-03 16:36:27 -07:00
4de89d98a1 Make inline functions more high level. 2018-08-03 16:09:09 -07:00
ecbe84134d Add additional instructions. 2018-08-03 14:31:00 -07:00
3fa292347f Remove code made obsolete with type checking. 2018-08-03 13:52:40 -07:00
f8320bcb82 Add basic type checking. 2018-08-03 01:13:23 -07:00
8d015d47f3 Write documentation. 2018-08-02 01:09:48 -07:00
96059d6e04 Organize code into modules. 2018-08-01 22:40:41 -07:00
cd0e5c2919 Fix optimizer to adjust jumps after instruction deletion. 2018-08-01 21:46:44 -07:00
147837c011 Temporarily remove broken optimizer. 2018-07-29 22:56:17 -07:00
35 changed files with 2424 additions and 1446 deletions

View File

@@ -1,22 +1,64 @@
# chalk # chalk
TODO: Write a description here Chalk is a basic compiler from a toy language into CHIP-8 bytecode.
It supports some higher-level constructs like looks and if-statements,
and has some higher-level functions for drawing numbers and sprites.
## Installation ## Installation
TODO: Write installation instructions here To compile chalk, simply run
```Bash
crystal build --release src/chalk.cr
```
## Usage ## Usage
### Syntax
Chalk has minimal syntax. All code goes into functions, which are declared as follows:
```
fun function(param, another_param): return_type {
TODO: Write usage instructions here }
```
All parameters are assumed to be of type `u8`, an unsigned byte.
The return type can be either `u8` or `u0`, the latter being the same as `void` in C.
Users can declare variables using the `var` keyword:
```
var name = 5;
```
Assigning to existing variables is performed as such:
```
name = 6;
```
It's possible to call another function, which uses the CHIP-8 stack. This stack
is used exclusively for function calls, and may not exceed 12, as per the specifications
of the CHIP-8 virtual machine.
```
var result = double(name);
```
Additionally, to ensure that user-defined variables stored
in registers are not modified, chalk uses its own custom stack, placed at the end of the program.
This stack is accessed by modifying a stack pointer register, which holds the offset from the beginning of the stack. Because CHIP-8 registers can only hold up 8-bit unsinged numbers, the stack is therefore limited to 254 items.
## Development If statements take "truthy" values to be those that have a nonzero value. As such,
`if (0) {}` will never run, while `while(5) {}` will never terminate. Input can be awaited
using the `get_key` function, which returns the number of the next key pressed.
TODO: Write development instructions here Sprites can be declared using a quasi-visual syntax:
```
sprite dum [
` x x `
` x x `
` x x `
` `
`x x`
` xxxxxx `
]
```
These sprites can then be used in calls to `draw_sprite(sprite, x, y)`.
## Contributing ## Contributing
1. Fork it (<https://github.com/your-github-user/chalk/fork>) 1. Fork it (<https://github.com/DanilaFe/chalk/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`) 2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`) 3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`) 4. Push to the branch (`git push origin my-new-feature`)
@@ -24,4 +66,4 @@ TODO: Write development instructions here
## Contributors ## Contributors
- [your-github-user](https://github.com/your-github-user) Danila Fedorin - creator, maintainer - [DanilaFe](https://github.com/DanilaFe) Danila Fedorin - creator, maintainer

View File

@@ -0,0 +1,7 @@
fun main(): u0 {
1+2+3;
var a = 5+5;
var b = a;
var a = a + b;
var c = a - b;
}

21
programs/basic_call.chalk Normal file
View File

@@ -0,0 +1,21 @@
fun double(a): u8 {
return a + a;
}
fun quadruple(a): u8 {
return double(double(a));
}
fun reuse(): u0 {
var one = 2;
var two = 4;
var three = 6;
}
fun main(): u0 {
var a = quadruple(4);
var one = 1;
var two = 2;
var three = 3;
reuse();
}

View File

@@ -0,0 +1,3 @@
fun main(): u0 {
draw_number(69, 1, 1);
}

View File

@@ -0,0 +1,3 @@
fun main(): u0 {
}

17
programs/basic_if.chalk Normal file
View File

@@ -0,0 +1,17 @@
fun main(): u0 {
var a = 3+3;
if(a) {
a = 0;
}
var b = 0;
if(b) {
b = 4;
}
if(b) {
b = 4;
} else {
b = 5;
}
}

View File

@@ -0,0 +1,4 @@
fun main(): u0 {
var a = get_key();
var b = get_key();
}

View File

@@ -0,0 +1,12 @@
sprite dum [
` x x `
` x x `
` x x `
` `
`x x`
` xxxxxx `
]
fun main(): u0 {
draw_sprite(dum, 0, 0);
}

View File

@@ -0,0 +1,8 @@
fun main(): u0 {
var a = 5;
var b = 0;
while(a) {
b = b + 2;
a = a - 1;
}
}

14
programs/comb_fib.chalk Normal file
View File

@@ -0,0 +1,14 @@
fun main(): u0 {
var a = 1;
var b = 1;
while(233 - a) {
draw_number(a, 24, 13);
set_delay(30);
var temp = a;
a = b;
b = b + temp;
while(get_delay()){}
draw_number(temp, 24, 13);
}
draw_number(a, 24, 13);
}

13
programs/comb_input.chalk Normal file
View File

@@ -0,0 +1,13 @@
fun main(): u0 {
var a = 1;
var b = 0;
var previous = 0;
draw_number(0, 24, 13);
while(a) {
previous = b;
b = get_key();
draw_number(previous, 24, 13);
draw_number(b, 24, 13);
}
}

View File

@@ -0,0 +1,39 @@
sprite dum [
` x x `
` x x `
` x x `
` `
`x x`
` xxxxxx `
]
fun main(): u8 {
var x = 0;
var y = 5;
var vy = 0;
var ay = 1;
var mode = 0;
while(1) {
draw_sprite(dum, x, y);
set_delay(1);
while(get_delay()){}
draw_sprite(dum, x, y);
if(5 - y) {
} else {
mode = 0;
}
if(26 - y) {
} else {
mode = 1;
}
if(mode) {
vy = vy - ay;
y = y - vy;
} else {
y = y + vy;
vy = vy + ay;
}
}
}

View File

@@ -0,0 +1,34 @@
sprite dum [
` x x `
` x x `
` x x `
` `
`x x`
` xxxxxx `
]
fun main(): u8 {
var x = 0;
var y = 27;
var vx = 1;
var vy = 0;
var a = 1;
while(1) {
var draw_y = 32 - y;
draw_sprite(dum, x, draw_y);
set_delay(1);
while(get_delay()){}
vy = vy - a;
y = y + vy;
x = x + vx;
if(6-y) {
} else {
var vtemp = vy;
vy = 0 - vtemp;
vy = vy + 1;
}
clear();
}
}

View File

@@ -2,9 +2,9 @@ require "./chalk/*"
require "option_parser" require "option_parser"
module Chalk module Chalk
config = Config.parse! config = Ui::Config.parse!
exit unless config.validate! exit unless config.validate!
compiler = Compiler.new config compiler = Compiler::Compiler.new config
compiler.run compiler.run
end end

View File

@@ -1,21 +1,35 @@
module Chalk module Chalk
class BuiltinFunction module Builtin
getter param_count : Int32 # A normal function (i.e., a "call" is generated for it)
# that is provided by chalk's standard library, and therefore
def initialize(@param_count) # has predefined output.
abstract class BuiltinFunction
# Creates a new function with *param_count* parameters.
def initialize()
end end
def generate!(codegen) # Uses the given `Compiler::Emitter` to output code.
end abstract def generate!(codegen)
# Gets the `Compiler::FunctionType` of this function.
abstract def type
end end
class InlineFunction # A function to which a call is not generated. This function
getter param_count : Int32 # is copied everywhere a call to it occurs. Besides this, the
# function also accepts trees rather than register numbers,
def initialize(@param_count) # and therefore can accept and manipulate trees.
abstract class InlineFunction
# Creates a new function with *param_count* parameters.
def initialize()
end end
def generate!(codegen, params, table, target, free) # Generates code like `Compiler::CodeGenerator` would.
# The *codegen* parameter is used to emit instructions,
# the *params* are trees that are being passed as arguments.
# See `Compiler::CodeGenerator#generate!` for what the other parameters mean.
abstract def generate!(codegen, params, table, target, free)
# Gets the `Compiler::FunctionType` of this function.
abstract def type
end end
end end
end end

View File

@@ -2,35 +2,42 @@ require "./ir.cr"
require "./emitter.cr" require "./emitter.cr"
module Chalk module Chalk
module Compiler
# A class that converts a tree into the corresponding
# intermediate representation, without optimizing.
class CodeGenerator class CodeGenerator
include Emitter include Emitter
RETURN_REG = 14 # Gets the instructions currently emitted by this code generator.
STACK_REG = 13 getter instructions
property instructions : Array(Instruction) # Creates a new compiler with the given symbol *table*
# and *function* for which code should be generated.
def initialize(table, @function : TreeFunction) def initialize(table, @function : Trees::TreeFunction)
@registers = 0 @registers = 0
@instructions = [] of Instruction @instructions = [] of Ir::Instruction
@table = Table.new table @table = Table.new table
@function.params.each do |param| @function.params.each do |param|
@table[param] = VarEntry.new @registers @table.set_var param, VarEntry.new @registers
@registers += 1 @registers += 1
end end
end end
def generate!(tree, function : InlineFunction, table, target, free) # Generates code for an inline function, with the given *tree* being the `Trees::TreeCall`
# that caused the function call. The other parameters are as described in the more general
# `#generate!` call.
def generate!(tree, function : Builtin::InlineFunction, table, target, free)
function.generate!(self, tree.params, table, target, free) function.generate!(self, tree.params, table, target, free)
end end
def generate!(tree, function : TreeFunction | BuiltinFunction, table, target, free) # Generates code for a tree or a builtin function (that is, a call is actually necessary).
# I is set to the current stack pointer, the registers are stored, and the call is made.
# The registers are then restored. The other parameters are as described in the more general
# `#generate!` call.
def generate!(tree, function : Trees::TreeFunction | Builtin::BuiltinFunction, table, target, free)
start_at = free start_at = free
# Move I to stack to_stack
setis
# Get to correct stack position
addi STACK_REG
# Store variables # Store variables
store (start_at - 1) unless start_at == 0 store (start_at - 1) unless start_at == 0
# Increment I and stack position # Increment I and stack position
@@ -52,57 +59,53 @@ module Chalk
# Reduce stack pointer # Reduce stack pointer
load free, start_at load free, start_at
opr TokenType::OpSub, STACK_REG, free opr TokenType::OpSub, STACK_REG, free
# Move I to stack to_stack
setis
# Get to correct stack position
addi STACK_REG
# Restore # Restore
restore (start_at - 1) unless start_at == 0 restore (start_at - 1) unless start_at == 0
# Get call value into target # Get call value into target
loadr target, RETURN_REG loadr target, RETURN_REG
end end
# Generates code for a *tree*, using a symbol *table*
# housing all the names for identifiers in the code.
# The result is stored into the *target* register,
# and the *free* register is the next register into
# which a value can be stored for "scratch work".
def generate!(tree, table, target, free) def generate!(tree, table, target, free)
case tree case tree
when TreeId when Trees::TreeId
entry = table[tree.id]? entry = table.get_var? tree.id
raise "Unknown variable" unless entry && raise "Unknown variable" unless entry
entry.is_a?(VarEntry)
loadr target, entry.register loadr target, entry.register
when TreeLit when Trees::TreeLit
load target, tree.lit load target, tree.lit
when TreeOp when Trees::TreeOp
generate! tree.left, table, target, free generate! tree.left, table, target, free
generate! tree.right, table, free, free + 1 generate! tree.right, table, free, free + 1
opr tree.op, target, free opr tree.op, target, free
when TreeCall when Trees::TreeCall
entry = table[tree.name]? entry = table.get_function?(tree.name).not_nil!
raise "Unknown function" unless entry && raise "Unknown function" unless entry.is_a?(FunctionEntry)
entry.is_a?(FunctionEntry) generate! tree, entry.function, table, target, free
function = entry.function when Trees::TreeBlock
raise "Invalid call" if tree.params.size != function.param_count
generate! tree, function, table, target, free
when TreeBlock
table = Table.new(table) table = Table.new(table)
tree.children.each do |child| tree.children.each do |child|
free += generate! child, table, free, free + 1 free += generate! child, table, THROWAWAY_REG, free
end end
when TreeVar when Trees::TreeVar
entry = table[tree.name]? entry = table.get_var? tree.name
if entry == nil if entry == nil
entry = VarEntry.new free entry = VarEntry.new free
free += 1 free += 1
table[tree.name] = entry table.set_var tree.name, entry
end end
raise "Unknown variable" unless entry.is_a?(VarEntry) generate! tree.expr, table, entry.as(VarEntry).register, free
generate! tree.expr, table, entry.register, free
return 1 return 1
when TreeAssign when Trees::TreeAssign
entry = table[tree.name]? entry = table.get_var? tree.name
raise "Unknown variable" unless entry && raise "Unknown variable" unless entry
entry.is_a?(VarEntry)
generate! tree.expr, table, entry.register, free generate! tree.expr, table, entry.register, free
when TreeIf when Trees::TreeIf
generate! tree.condition, table, free, free + 1 generate! tree.condition, table, free, free + 1
sne free, 0 sne free, 0
jump_inst = jr 0 jump_inst = jr 0
@@ -115,7 +118,7 @@ module Chalk
old_size = @instructions.size old_size = @instructions.size
generate! tree.otherwise, table, free, free + 1 if tree.otherwise generate! tree.otherwise, table, free, free + 1 if tree.otherwise
jump_after.offset = @instructions.size - old_size + 1 jump_after.offset = @instructions.size - old_size + 1
when TreeWhile when Trees::TreeWhile
before_cond = @instructions.size before_cond = @instructions.size
generate! tree.condition, table, free, free + 1 generate! tree.condition, table, free, free + 1
sne free, 0 sne free, 0
@@ -127,16 +130,19 @@ module Chalk
cond_jump.offset = @instructions.size - old_size + 1 cond_jump.offset = @instructions.size - old_size + 1
after_jump.offset = before_cond - instructions.size + 1 after_jump.offset = before_cond - instructions.size + 1
when TreeReturn when Trees::TreeReturn
generate! tree.rvalue, table, RETURN_REG, free generate! tree.rvalue, table, free, free + 1
loadr RETURN_REG, free
ret ret
end end
return 0 return 0
end end
# Generates code for the function that was given to it.
def generate! def generate!
generate!(@function.block, @table, -1, @registers) generate!(@function.block, @table, 0, @registers)
return @instructions return @instructions
end end
end end
end end
end

View File

@@ -3,13 +3,20 @@ require "./constant_folder.cr"
require "./table.cr" require "./table.cr"
module Chalk module Chalk
module Compiler
# Top-level class to tie together the various
# components, such as the `Lexer`,
# `ParserCombinators::Parser`, and `Optimizer`
class Compiler class Compiler
def initialize(@config : Config) # Creates a new compiler with the given *config*.
def initialize(@config : Ui::Config)
@logger = Logger.new STDOUT @logger = Logger.new STDOUT
@logger.debug("Initialized compiler") @logger.debug("Initialized compiler")
@logger.level = Logger::DEBUG @logger.level = @config.loglevel
end end
# Reads a file an extracts instances of
# `Trees:TreeFunction`.
private def create_trees(file) private def create_trees(file)
string = File.read(file) string = File.read(file)
@logger.debug("Tokenizing") @logger.debug("Tokenizing")
@@ -20,14 +27,15 @@ module Chalk
end end
@logger.debug("Finished tokenizing") @logger.debug("Finished tokenizing")
@logger.debug("Beginning parsing") @logger.debug("Beginning parsing")
parser = Parser.new parser = ParserCombinators::Parser.new
if trees = parser.parse?(tokens) if trees = parser.parse?(tokens)
@logger.debug("Finished parsing") @logger.debug("Finished parsing")
@logger.debug("Beginning constant folding") @logger.debug("Beginning constant folding")
folder = ConstantFolder.new folder = Trees::ConstantFolder.new
trees.map! do |tree| trees.map! do |tree|
next tree unless tree.is_a?(Trees::TreeFunction)
@logger.debug("Constant folding #{tree.name}") @logger.debug("Constant folding #{tree.name}")
tree.apply(folder).as(TreeFunction) tree.apply(folder)
end end
@logger.debug("Done constant folding") @logger.debug("Done constant folding")
return trees return trees
@@ -35,44 +43,75 @@ module Chalk
raise "Unable to parse file." raise "Unable to parse file."
end end
# Creates a default symbol table using the default functions,
# as well as the functions declared by *trees*
private def create_table(trees) private def create_table(trees)
table = Table.new table = Table.new
@logger.debug("Creating symbol table") @logger.debug("Creating symbol table")
trees.each do |tree| trees.each do |tree|
@logger.debug("Storing #{tree.name} in symbol table") case tree
table[tree.name] = FunctionEntry.new tree when Trees::TreeSprite
@logger.debug("Storing sprite #{tree.name} in symbol table")
table.set_sprite tree.name, SpriteEntry.new tree.sprite
when Trees::TreeFunction
@logger.debug("Storing function #{tree.name} in symbol table")
table.set_function tree.name, FunctionEntry.new tree
else
@logger.debug("Unexpected tree type in input.")
end
end end
@logger.debug("Done creating symbol table") @logger.debug("Done creating symbol table")
table["draw"] = FunctionEntry.new InlineDrawFunction.new table.set_function "get_key", FunctionEntry.new Builtin::InlineAwaitKeyFunction.new
table["get_key"] = FunctionEntry.new InlineAwaitKeyFunction.new table.set_function "set_delay", FunctionEntry.new Builtin::InlineSetDelayFunction.new
table["get_font"] = FunctionEntry.new InlineGetFontFunction.new table.set_function "get_delay", FunctionEntry.new Builtin::InlineGetDelayFunction.new
table["set_delay"] = FunctionEntry.new InlineSetDelayFunction.new table.set_function "set_sound", FunctionEntry.new Builtin::InlineSetSoundFunction.new
table["get_delay"] = FunctionEntry.new InlineGetDelayFunction.new table.set_function "draw_number", FunctionEntry.new Builtin::InlineDrawNumberFunction.new
table.set_function "draw_sprite", FunctionEntry.new Builtin::InlineDrawSpriteFunction.new
table.set_function "clear", FunctionEntry.new Builtin::InlineClearFunction.new
return table return table
end end
private def create_code(tree : TreeFunction, table) # Generates and optimizes intermediate representation for the given *tree*,
generator = CodeGenerator.new table, tree # looking up identifiers in the symbol *table*, and appending the given *instruction*
# at the end of the function to ensure correct program flow.
private def create_code(tree : Trees::TreeFunction, table, instruction = Ir::ReturnInstruction.new)
tree.reduce(Trees::TypeChecker.new table, tree.type.return_type)
optimizer = Optimizer.new optimizer = Optimizer.new
generator = CodeGenerator.new table, tree
@logger.debug("Generating code for #{tree.name}") @logger.debug("Generating code for #{tree.name}")
return optimizer.optimize generator.generate! code = generator.generate!
code << instruction
return code # optimizer.optimize(code)
end end
private def create_code(tree : BuiltinFunction, table) # Generate code for a builtin function. Neither the *table* nor the *instruction*
instructions = [] of Instruction # are used, and serve to allow function overloading.
tree.generate!(instructions) private def create_code(function : Builtin::BuiltinFunction, table, instruction = nil)
instructions = [] of Ir::Instruction
function.generate!(instructions)
return instructions return instructions
end end
private def create_code(trees : Array(TreeFunction), table) # Creates a hash containing function names and their generated code.
code = {} of String => Array(Instruction) # Only functions parsed from the file are compiled, and the *table*
# is used for looking up identifiers.
private def create_code(trees : Array(Trees::TreeFunction), table)
code = {} of String => Array(Ir::Instruction)
trees.each do |tree| trees.each do |tree|
code[tree.name] = create_code(tree, table) code[tree.name] = create_code(tree, table)
end end
return code return code
end end
private def create_code(trees : Array(Trees::Tree), table)
functions = trees.select &.is_a?(Trees::TreeFunction)
return create_code(functions.map &.as(Trees::TreeFunction), table)
end
# Runs in the tree `Ui::OutputMode`. The file is
# tokenized and parsed, and the result is printed
# to the standard output.
private def run_tree private def run_tree
trees = create_trees(@config.file) trees = create_trees(@config.file)
trees.each do |it| trees.each do |it|
@@ -80,6 +119,11 @@ module Chalk
end end
end end
# Runs in the intermediate `Ui::OutputMode`. The file
# is tokenized and parsed, and for each function,
# intermediate representation is generated. However,
# an executable is not generated, and the IR
# is printed to the screen.
private def run_intermediate private def run_intermediate
trees = create_trees(@config.file) trees = create_trees(@config.file)
table = create_table(trees) table = create_table(trees)
@@ -91,17 +135,21 @@ module Chalk
end end
end end
# Creates binary from the given *instructions*,
# using the symbol *table* for lookups, and writes
# the output to *dest*
private def generate_binary(table, instructions, dest) private def generate_binary(table, instructions, dest)
context = InstructionContext.new table, instructions.size binary = instructions.map_with_index { |it, i| it.to_bin(table, instructions.size, i).to_u16 }
binary = instructions.map_with_index { |it, i| it.to_bin(context, i).to_u16 }
binary.each do |inst| binary.each do |inst|
first = (inst >> 8).to_u8 first = (inst >> 8).to_u8
dest.write_byte(first) dest << first
second = (inst & 0xff).to_u8 second = (inst & 0xff).to_u8
dest.write_byte(second) dest << second
end end
end end
# Find all calls performed by the functions
# stored in the *table*, starting at the main function.
private def collect_calls(table) private def collect_calls(table)
open = Set(String).new open = Set(String).new
done = Set(String).new done = Set(String).new
@@ -111,55 +159,73 @@ module Chalk
first = open.first first = open.first
open.delete first open.delete first
entry = table[first]? entry = table.get_function? first
raise "Unknown function" unless entry && entry.is_a?(FunctionEntry) raise "Unknown function" unless entry
function = entry.function function = entry.function
next if function.is_a?(InlineFunction) next if function.is_a?(Builtin::InlineFunction)
done << first done << first
next unless function.is_a?(TreeFunction) next unless function.is_a?(Trees::TreeFunction)
visitor = CallVisitor.new visitor = Trees::CallVisitor.new
function.accept(visitor) function.accept(visitor)
open.concat(visitor.calls - done) open.concat(visitor.calls - done)
end end
return done return done
end end
# Runs in the binary `Ui::OutputMode`. The file is
# converted into an executable.
private def run_binary private def run_binary
all_instructions = [] of Instruction all_instructions = [] of Ir::Instruction
trees = create_trees(@config.file) trees = create_trees(@config.file)
table = create_table(trees) table = create_table(trees)
names = collect_calls(table) names = collect_calls(table)
names.delete "main" names.delete "main"
main_entry = table["main"]?.as(FunctionEntry) main_entry = table.get_function?("main").not_nil!
all_instructions.concat create_code(main_entry.function.as(TreeFunction), table) all_instructions.concat create_code(main_entry.function.as(Trees::TreeFunction),
table, Ir::JumpRelativeInstruction.new 0)
main_entry.addr = 0 main_entry.addr = 0
all_instructions << JumpRelativeInstruction.new 0
names.each do |name| names.each do |name|
entry = table[name]?.as(FunctionEntry) entry = table.get_function?(name).not_nil!
entry.addr = all_instructions.size entry.addr = all_instructions.size
function = entry.function function = entry.function
raise "Trying to compile inlined function" if function.is_a?(InlineFunction) raise "Trying to compile inlined function" if function.is_a?(Builtin::InlineFunction)
all_instructions.concat create_code(function, table) all_instructions.concat create_code(function, table)
all_instructions << ReturnInstruction.new all_instructions << Ir::ReturnInstruction.new
end end
file = File.open("out.ch8", "w") sprite_bytes = [] of UInt8
generate_binary(table, all_instructions, file) offset = 0
table.sprites.each do |k, v|
data = v.sprite.encode
v.addr = offset + all_instructions.size * 2
offset += data.size
sprite_bytes.concat data
end
binary = [] of UInt8
file = File.open(@config.output, "w")
generate_binary(table, all_instructions, binary)
binary.concat sprite_bytes
binary.each do |byte|
file.write_byte byte
end
file.close file.close
end end
# Runs the compiler.
def run def run
case @config.mode case @config.mode
when OutputMode::Tree when Ui::OutputMode::Tree
run_tree run_tree
when OutputMode::Intermediate when Ui::OutputMode::Intermediate
run_intermediate run_intermediate
when OutputMode::Binary when Ui::OutputMode::Binary
run_binary run_binary
end end
end end
end end
end end
end

View File

@@ -1,42 +1,85 @@
module Chalk module Chalk
module Ui
# The mode in which the compiler operates.
# Defines what actions are and aren't performed.
enum OutputMode enum OutputMode
# The text is only parsed, and the result is printed to the screen.
Tree, Tree,
# The text is parsed and converted to intermediate representation.
# The intermediate representation is then printed to the screen.
Intermediate, Intermediate,
# The text is converted into a full CHIP-8 executable.
Binary Binary
end end
# A configuration class created from the command-line parameters.
class Config class Config
property file : String # Gets the file to be compiled.
property mode : OutputMode getter file : String
# Sets the file to be compiled.
setter file : String
# Gets the mode in which the compiler should operate.
getter mode : OutputMode
# Sets the mode in which the compiler should operate.
setter mode : OutputMode
# Gets the log level.
getter loglevel : Logger::Severity
# Sets the log level.
setter loglevel : Logger::Severity
# Gets the output file destination.
getter output : String
# Sets the output file destination.
setter output : String
# Creates a new configuration.
def initialize(@file = "", def initialize(@file = "",
@mode = OutputMode::Tree) @mode = OutputMode::Tree,
@loglevel = Logger::Severity::DEBUG,
@output : String = "out.ch8")
end end
# Reads a configuration from the command line options.
def self.parse! def self.parse!
config = self.new config = self.new
OptionParser.parse! do |parser| OptionParser.parse! do |parser|
parser.banner = "Usage: chalk [arguments]" parser.banner = "Usage: chalk [arguments]"
parser.on("-m", "--mode=MODE", "Set the mode of the compiler.") do |mode| parser.on("-m", "--mode=MODE", "Set the mode of the compiler.") do |mode|
case mode.downcase hash = {
when "tree", "t" "tree" => OutputMode::Tree,
config.mode = OutputMode::Tree "t" => OutputMode::Tree,
when "intermediate", "i" "intermediate" => OutputMode::Intermediate,
config.mode = OutputMode::Intermediate "i" => OutputMode::Intermediate,
when "binary", "b" "binary" => OutputMode::Binary,
config.mode = OutputMode::Binary "b" => OutputMode::Binary
else }
puts "Invalid mode type. Using default." puts "Invalid mode type. Using default." if !hash.has_key?(mode)
end config.mode = hash[mode]? || OutputMode::Tree
end end
parser.on("-f", "--file=FILE", "Set the input file to compile.") do |file| parser.on("-f", "--file=FILE", "Set the input file to compile.") do |file|
config.file = file config.file = file
end end
parser.on("-o", "--output=OUT", "Sets the output file.") do |out|
config.output = out
end
parser.on("-l", "--log=LOG", "Set the log level of the compiler.") do |log|
hash = {
"debug" => Logger::Severity::DEBUG,
"fatal" => Logger::Severity::FATAL,
"error" => Logger::Severity::ERROR,
"info" => Logger::Severity::INFO,
"unknown" => Logger::Severity::UNKNOWN,
"warn" => Logger::Severity::WARN
}
puts "Invalid log level. Using default." if !hash.has_key?(log)
config.loglevel = hash[log]? || Logger::Severity::DEBUG
end
parser.on("-h", "--help", "Show this message.") { puts parser } parser.on("-h", "--help", "Show this message.") { puts parser }
end end
return config return config
end end
# Validates the options provided, returning true if
# they are valid and false otherwise.
def validate! def validate!
if file == "" if file == ""
puts "No source file specified." puts "No source file specified."
@@ -49,3 +92,4 @@ module Chalk
end end
end end
end end
end

View File

@@ -1,33 +1,37 @@
require "./tree.cr" require "./tree.cr"
module Chalk module Chalk
module Trees
# `Trees::Transformer` that turns operations on
# Constants into constants.
class ConstantFolder < Transformer class ConstantFolder < Transformer
private def perform_op(op, left, right) private def perform_op(op, left, right)
case op case op
when TokenType::OpAdd when Compiler::TokenType::OpAdd
left + right left + right
when TokenType::OpSub when Compiler::TokenType::OpSub
left - right left - right
when TokenType::OpMul when Compiler::TokenType::OpMul
left*right left*right
when TokenType::OpDiv when Compiler::TokenType::OpDiv
left/right left/right
when TokenType::OpAnd when Compiler::TokenType::OpAnd
left & right left & right
when TokenType::OpOr when Compiler::TokenType::OpOr
left | right left | right
else TokenType::OpXor else Compiler::TokenType::OpXor
left ^ right left ^ right
end end
end end
def transform(tree : TreeOp) def transform(tree : Trees::TreeOp)
if tree.left.is_a?(TreeLit) && tree.right.is_a?(TreeLit) if tree.left.is_a?(Trees::TreeLit) && tree.right.is_a?(Trees::TreeLit)
return TreeLit.new perform_op(tree.op, return Trees::TreeLit.new perform_op(tree.op,
tree.left.as(TreeLit).lit, tree.left.as(Trees::TreeLit).lit,
tree.right.as(TreeLit).lit) tree.right.as(Trees::TreeLit).lit)
end end
return tree return tree
end end
end end
end end
end

View File

@@ -1,75 +1,124 @@
module Chalk module Chalk
module Compiler
# The register into which the return value of a function is stored.
RETURN_REG = 14
# The register into which the "stack pointer" is stored.
STACK_REG = 13
# Register used for throwing away values.
THROWAWAY_REG = 12
# Module to emit instructions and store
# them into an existing array.
module Emitter module Emitter
# Moves I to the next available value on the stack.
def to_stack
setis
addi STACK_REG
end
# Emits an instruction to load a *value* into a register, *into*.
def load(into, value) def load(into, value)
inst = LoadInstruction.new into, value.to_i32 inst = Ir::LoadInstruction.new into, value.to_i32
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits an instruction to load a register, *from*, into
# another register, *into*
def loadr(into, from) def loadr(into, from)
inst = LoadRegInstruction.new into, from inst = Ir::LoadRegInstruction.new into, from
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits an instruction that's converted
# to an operation, *op* that mutates the register, *into*,
# with the right hand operand *from*
def op(op, into, from) def op(op, into, from)
inst = OpInstruction.new op, into, from inst = Ir::OpInstruction.new op, into, from
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits an instruction that's converted
# to an operation, *op*, that mutates the register, *into*,
# with the right hand operand (a register), *from*
def opr(op, into, from) def opr(op, into, from)
inst = OpRegInstruction.new op, into, from inst = Ir::OpRegInstruction.new op, into, from
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits a "skip next instruction if not equal"
# instruction. The left hand side is a register,
# an the right hand side is a value.
def sne(l, r) def sne(l, r)
inst = SkipNeInstruction.new l, r inst = Ir::SkipNeInstruction.new l, r
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits an instruction to jump relative to
# where the instruction is.
# ```
# jr 0 # Infinite loop
# jr -1 # Run previous instruction
# jr 1 # pretty much a no-op.
# ```
def jr(o) def jr(o)
inst = JumpRelativeInstruction.new o inst = Ir::JumpRelativeInstruction.new o
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits instruction that stores register 0 through *up_to* into
# memory at address I.
def store(up_to) def store(up_to)
inst = StoreInstruction.new up_to inst = Ir::StoreInstruction.new up_to
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits instruction that loads values from address I into
# register 0 through *up_t*
def restore(up_to) def restore(up_to)
inst = RestoreInstruction.new up_to inst = Ir::RestoreInstruction.new up_to
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits a return instruction.
def ret def ret
inst = ReturnInstruction.new inst = Ir::ReturnInstruction.new
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits an instruction to call
# the given function name.
def call(func) def call(func)
inst = CallInstruction.new func inst = Ir::CallInstruction.new func
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits instruction to set I
# to the baste stack location. The stack
# pointer will need to be added to I
# to get the next available stack slot.
def setis def setis
inst = SetIStackInstruction.new inst = Ir::SetIStackInstruction.new
@instructions << inst @instructions << inst
return inst return inst
end end
# Emits instruction to add the value of a
# register to I
def addi(reg) def addi(reg)
inst = AddIRegInstruction.new reg inst = Ir::AddIRegInstruction.new reg
@instructions << inst @instructions << inst
return inst return inst
end end
end end
end end
end

View File

@@ -1,4 +1,6 @@
module Chalk module Chalk
module Trees
# Visitor that finds all function calls in a function.
class CallVisitor < Visitor class CallVisitor < Visitor
property calls : Set(String) property calls : Set(String)
@@ -11,3 +13,4 @@ module Chalk
end end
end end
end end
end

View File

@@ -1,58 +1,111 @@
require "./builtin"
require "./type"
module Chalk module Chalk
class InlineDrawFunction < InlineFunction module Builtin
def initialize # Inline function to await for a key and return it.
@param_count = 3
end
def generate!(emitter, params, table, target, free)
if !params[2].is_a?(TreeLit)
raise "Third parameter must be a constant."
end
emitter.generate! params[0], table, free, free + 1
emitter.generate! params[1], table, free + 1, free + 2
emitter.instructions << DrawInstruction.new free, free + 1, params[2].as(TreeLit).lit.to_i32
end
end
class InlineAwaitKeyFunction < InlineFunction class InlineAwaitKeyFunction < InlineFunction
def initialize
@param_count = 0
end
def generate!(emitter, params, table, target, free) def generate!(emitter, params, table, target, free)
emitter.instructions << AwaitKeyInstruction.new target emitter.instructions << Ir::AwaitKeyInstruction.new target
end end
end def type
return Compiler::FunctionType.new(([] of Compiler::Type), Compiler::Type::U8)
class InlineGetFontFunction < InlineFunction
def initialize
@param_count = 1
end
def generate!(emitter, params, table, target, free)
emitter.generate! params[0], table, free, free + 1
emitter.instructions << GetFontInstruction.new free
end end
end end
# Inline function to set the delay timer.
class InlineSetDelayFunction < InlineFunction class InlineSetDelayFunction < InlineFunction
def initialize
@param_count = 1
end
def generate!(emitter, params, table, target, free) def generate!(emitter, params, table, target, free)
emitter.generate! params[0], table, free, free + 1 emitter.generate! params[0], table, free, free + 1
emitter.instructions << SetDelayTimerInstruction.new free emitter.instructions << Ir::SetDelayTimerInstruction.new free
end
def type
return Compiler::FunctionType.new([Compiler::Type::U8], Compiler::Type::U0)
end end
end end
# Inline function to get the delay timer.
class InlineGetDelayFunction < InlineFunction class InlineGetDelayFunction < InlineFunction
def initialize def generate!(emitter, params, table, target, free)
@param_count = 0 emitter.instructions << Ir::GetDelayTimerInstruction.new target
end
def type
return Compiler::FunctionType.new(([] of Compiler::Type), Compiler::Type::U8)
end
end end
# Function to set the sound timer.
class InlineSetSoundFunction < InlineFunction
def generate!(emitter, params, table, target, free) def generate!(emitter, params, table, target, free)
emitter.instructions << GetDelayTimerInstruction.new target emitter.generate! params[0], table, free, free + 1
emitter.instructions << Ir::SetSoundTimerInstruction.new free
end
def type
return Compiler::FunctionType.new([Compiler::Type::U8], Compiler::Type::U0)
end
end
# Function to draw numbers.
class InlineDrawNumberFunction < InlineFunction
def generate!(emitter, params, table, target, free)
emitter.to_stack
# Save variables from R0-R2
emitter.store 0x2
emitter.load free, 0x3
emitter.addi free
# Write BCD values to I
emitter.generate! params[0], table, free, free + 1
emitter.instructions << Ir::BCDInstruction.new free
emitter.restore 0x2
# Get the coordinates
free = 3 if free < 3
emitter.generate! params[1], table, free, free + 1
emitter.generate! params[2], table, free + 1, free + 2
# Draw
emitter.instructions << Ir::GetFontInstruction.new 0x0
emitter.instructions << Ir::DrawInstruction.new free, free + 1, 5
emitter.op(Compiler::TokenType::OpAdd, free, 6)
emitter.instructions << Ir::GetFontInstruction.new 0x1
emitter.instructions << Ir::DrawInstruction.new free, free + 1, 5
emitter.op(Compiler::TokenType::OpAdd, free, 6)
emitter.instructions << Ir::GetFontInstruction.new 0x2
emitter.instructions << Ir::DrawInstruction.new free, free + 1, 5
# Load variables from RO-R2 back
emitter.to_stack
emitter.restore 0x2
end
def type
return Compiler::FunctionType.new([Compiler::Type::U8] * 3, Compiler::Type::U0)
end
end
class InlineDrawSpriteFunction < InlineFunction
def generate!(emitter, params, table, target, free)
raise "First parameter should be a sprite name." if !params[0].is_a?(Trees::TreeId)
sprite_name = params[0].as(Trees::TreeId).id
sprite = table.get_sprite?(sprite_name).not_nil!.sprite
emitter.generate! params[1], table, free, free + 1
emitter.generate! params[2], table, free + 1, free + 2
emitter.instructions << Ir::SetISpriteInstruction.new params[0].as(Trees::TreeId).id
emitter.instructions << Ir::DrawInstruction.new free, free + 1, sprite.height.to_i32
end
def type
return Compiler::FunctionType.new([Compiler::Type::U8] * 3, Compiler::Type::U0)
end
end
class InlineClearFunction < InlineFunction
def generate!(emitter, params, table, target, free)
emitter.instructions << Ir::ClearInstruction.new
end
def type
return Compiler::FunctionType.new(([] of Compiler::Type), Compiler::Type::U0)
end
end end
end end
end end

View File

@@ -1,25 +1,47 @@
require "./lexer.cr" require "./lexer.cr"
module Chalk module Chalk
module Ir
# Base instruction class.
class Instruction class Instruction
def to_bin(i, index) # Converts the instruction to binary, using
# A table for symbol lookups, the stack position,
# and the inex of the instruction.
def to_bin(table, stack, index)
return 0 return 0
end end
end end
class InstructionContext # Instruction to clear the screen
property table : Table class ClearInstruction < Instruction
property stack : Int32 def to_s(io)
io << "clear"
end
def initialize(@table, @stack) def to_bin(table, stack, index)
return 0x00e0
end end
end end
# Instruction to assign a random value to a register.
class RandomInstruction < Instruction
def initialize(@register : Int32, @value : Int32)
end
def to_s(io)
io << "rand R"
@register.to_s(16, io)
io << " " << @value
end
def to_bin(table, stack, index)
return 0xc000 | (@register << 8) | (@value)
end
end
# Instruction to load a value into a register.
class LoadInstruction < Instruction class LoadInstruction < Instruction
property register : Int32 def initialize(@register : Int32, @value : Int32)
property value : Int32
def initialize(@register, @value)
end end
def to_s(io) def to_s(io)
@@ -28,16 +50,19 @@ module Chalk
io << " " << @value io << " " << @value
end end
def to_bin(i, index) def to_bin(table, stack, index)
0x6000 | (@register << 8) | @value 0x6000 | (@register << 8) | @value
end end
end end
# Instruction to load a register into another register.
class LoadRegInstruction < Instruction class LoadRegInstruction < Instruction
property into : Int32 # Gets the register being written to.
property from : Int32 getter into
# Gets the register being used as right-hand operand.
getter from
def initialize(@into, @from) def initialize(@into : Int32, @from : Int32)
end end
def to_s(io) def to_s(io)
@@ -47,28 +72,26 @@ module Chalk
@from.to_s(16, io) @from.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
0x8000 | (@into << 8) | (@from << 4) 0x8000 | (@into << 8) | (@from << 4)
end end
end end
# Instruction to perform an operation on a register and a value,
# storing the output back into the register.
class OpInstruction < Instruction class OpInstruction < Instruction
property op : TokenType def initialize(@op : Compiler::TokenType, @into : Int32, @value : Int32)
property into : Int32
property value : Int32
def initialize(@op, @into, @value)
end end
def to_s(io) def to_s(io)
io << "op " << op << " R" io << "op " << @op << " R"
@into.to_s(16, io) @into.to_s(16, io)
io << " " << @value io << " " << @value
end end
def to_bin(i, index) def to_bin(table, stack, index)
case op case @op
when TokenType::OpAdd when Compiler::TokenType::OpAdd
return 0x7000 | (@into << 8) | @value return 0x7000 | (@into << 8) | @value
else else
raise "Invalid instruction" raise "Invalid instruction"
@@ -76,33 +99,31 @@ module Chalk
end end
end end
# Instruction to perform an operation on a register and another register,
# storing the output back into left hand register.
class OpRegInstruction < Instruction class OpRegInstruction < Instruction
property op : TokenType def initialize(@op : Compiler::TokenType, @into : Int32, @from : Int32)
property into : Int32
property from : Int32
def initialize(@op, @into, @from)
end end
def to_s(io) def to_s(io)
io << "opr " << op << " R" io << "opr " << @op << " R"
@into.to_s(16, io) @into.to_s(16, io)
io << " R" io << " R"
@from.to_s(16, io) @from.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
code = 0 code = 0
case op case @op
when TokenType::OpAdd when Compiler::TokenType::OpAdd
code = 4 code = 4
when TokenType::OpSub when Compiler::TokenType::OpSub
code = 5 code = 5
when TokenType::OpOr when Compiler::TokenType::OpOr
code = 1 code = 1
when TokenType::OpAnd when Compiler::TokenType::OpAnd
code = 2 code = 2
when TokenType::OpXor when Compiler::TokenType::OpXor
code = 3 code = 3
else else
raise "Invalid instruction" raise "Invalid instruction"
@@ -111,10 +132,11 @@ module Chalk
end end
end end
# Instruction to write registers to memory at address I.
# The *up_to* parameter specifies the highest register
# that should be stored.
class StoreInstruction < Instruction class StoreInstruction < Instruction
property up_to : Int32 def initialize(@up_to : Int32)
def initialize(@up_to)
end end
def to_s(io) def to_s(io)
@@ -122,15 +144,16 @@ module Chalk
@up_to.to_s(16, io) @up_to.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf055 | (@up_to << 8) return 0xf055 | (@up_to << 8)
end end
end end
# Instruction to read registers from memory at address I.
# The *up_to* parameter specifies the highest register
# that should be read into.
class RestoreInstruction < Instruction class RestoreInstruction < Instruction
property up_to : Int32 def initialize(@up_to : Int32)
def initialize(@up_to)
end end
def to_s(io) def to_s(io)
@@ -138,11 +161,12 @@ module Chalk
@up_to.to_s(16, io) @up_to.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf065 | (@up_to << 8) return 0xf065 | (@up_to << 8)
end end
end end
# Instruction to return from a call.
class ReturnInstruction < Instruction class ReturnInstruction < Instruction
def initialize def initialize
end end
@@ -151,67 +175,68 @@ module Chalk
io << "return" io << "return"
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x00ee return 0x00ee
end end
end end
# Instruction to jump relative to its own position.
class JumpRelativeInstruction < Instruction class JumpRelativeInstruction < Instruction
property offset : Int32 # Gets the offset of this instruction.
getter offset
# Sets the offset of this instruction
setter offset
def initialize(@offset) def initialize(@offset : Int32)
end end
def to_s(io) def to_s(io)
io << "jr " << @offset io << "jr " << @offset
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x1000 | ((@offset + index) * 2 + 0x200) return 0x1000 | ((@offset + index) * 2 + 0x200)
end end
end end
# Instruction to skip the next instruction if
# the left-hand register is equal to the right-hand value.
class SkipEqInstruction < Instruction class SkipEqInstruction < Instruction
property left : Int32 def initialize(@left : Int32, @right : Int32)
property right : Int32
def initialize(@left, @right)
end end
def to_s(io) def to_s(io)
io << "seq R" io << "seq R"
@left.to_s(16, io) @left.to_s(16, io)
io << " " << right io << " " << @right
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x3000 | (@left << 8) | @right return 0x3000 | (@left << 8) | @right
end end
end end
# Instruction to skip the next instruction if
# the left-hand register is not equal to the right-hand value.
class SkipNeInstruction < Instruction class SkipNeInstruction < Instruction
property left : Int32 def initialize(@left : Int32, @right : Int32)
property right : Int32
def initialize(@left, @right)
end end
def to_s(io) def to_s(io)
io << "sne R" io << "sne R"
@left.to_s(16, io) @left.to_s(16, io)
io << " " << right io << " " << @right
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x4000 | (@left << 8) | @right return 0x4000 | (@left << 8) | @right
end end
end end
# Instruction to skip the next instruction if
# the left-hand register is equal to the right-hand register.
class SkipRegEqInstruction < Instruction class SkipRegEqInstruction < Instruction
property left : Int32 def initialize(@left : Int32, @right : Int32)
property right : Int32
def initialize(@left, @right)
end end
def to_s(io) def to_s(io)
@@ -221,16 +246,15 @@ module Chalk
@right.to_s(16, io) @right.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x5000 | (@left << 8) | (@right << 4) return 0x5000 | (@left << 8) | (@right << 4)
end end
end end
# Instruction to skip the next instruction if
# the left-hand register is not equal to the right-hand register.
class SkipRegNeInstruction < Instruction class SkipRegNeInstruction < Instruction
property left : Int32 def initialize(@left : Int32, @right : Int32)
property right : Int32
def initialize(@left, @right)
end end
def to_s(io) def to_s(io)
@@ -240,77 +264,92 @@ module Chalk
@right.to_s(16, io) @right.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x9000 | (@left << 8) | (@right << 4) return 0x9000 | (@left << 8) | (@right << 4)
end end
end end
# Instruction to call a function by name.
class CallInstruction < Instruction class CallInstruction < Instruction
property name : String # Gets the name of the function being called.
getter name
def initialize(@name) def initialize(@name : String)
end end
def to_s(io) def to_s(io)
io << "call " << @name io << "call " << @name
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0x2000 | (i.table[name]?.as(FunctionEntry).addr * 2 + 0x200) return 0x2000 | (table.get_function?(name).as(Compiler::FunctionEntry).addr * 2 + 0x200)
end end
end end
# Instruction to set I to the base position of the stack.
class SetIStackInstruction < Instruction class SetIStackInstruction < Instruction
def to_s(io) def to_s(io)
io << "setis" io << "setis"
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xa000 | (i.stack * 2 + 0x200) return 0xa000 | (stack * 2 + 0x200)
end end
end end
class SetISpriteInstruction < Instruction
getter name
def initialize(@name : String)
end
def to_s(io)
io << "setispr " << @name
end
def to_bin(table, stack, index)
return 0xa000 | (table.get_sprite?(@name).not_nil!.addr + 0x200)
end
end
# Instruction to add a register to I.
class AddIRegInstruction < Instruction class AddIRegInstruction < Instruction
property reg : Int32 def initialize(@reg : Int32)
def initialize(@reg)
end end
def to_s(io) def to_s(io)
io << "addi R" io << "addi R"
reg.to_s(16, io) @reg.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf000 | (@reg << 8) | 0x1e return 0xf000 | (@reg << 8) | 0x1e
end end
end end
# Instruction to draw on screen.
# The x and y coordinates specify the position of the sprite,
# and the height gives the height of the sprite.
class DrawInstruction < Instruction class DrawInstruction < Instruction
property x : Int32 def initialize(@x : Int32, @y : Int32, @height : Int32)
property y : Int32
property height : Int32
def initialize(@x, @y, @height)
end end
def to_s(io) def to_s(io)
io << "draw R" io << "draw R"
x.to_s(16, io) @x.to_s(16, io)
io << " R" io << " R"
y.to_s(16, io) @y.to_s(16, io)
io << " " << height io << " " << @height
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xd000 | (@x << 8) | (@y << 4) | height return 0xd000 | (@x << 8) | (@y << 4) | @height
end end
end end
# Instruction to await a key press and store it into a register.
class AwaitKeyInstruction < Instruction class AwaitKeyInstruction < Instruction
property into : Int32 def initialize(@into : Int32)
def initialize(@into)
end end
def to_s(io) def to_s(io)
@@ -318,15 +357,15 @@ module Chalk
@into.to_s(16, io) @into.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf00a | (@into << 8) return 0xf00a | (@into << 8)
end end
end end
# Instruction to set I to the font given by the value
# of a register.
class GetFontInstruction < Instruction class GetFontInstruction < Instruction
property from : Int32 def initialize(@from : Int32)
def initialize(@from)
end end
def to_s(io) def to_s(io)
@@ -334,15 +373,15 @@ module Chalk
@from.to_s(16, io) @from.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf029 | (@from << 8) return 0xf029 | (@from << 8)
end end
end end
# Instruction to set the delay timer to the value
# of the given register.
class SetDelayTimerInstruction < Instruction class SetDelayTimerInstruction < Instruction
property from : Int32 def initialize(@from : Int32)
def initialize(@from)
end end
def to_s(io) def to_s(io)
@@ -350,15 +389,15 @@ module Chalk
@from.to_s(16, io) @from.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf015 | (@from << 8) return 0xf015 | (@from << 8)
end end
end end
# Instruction to get the delay timer, and store
# the value into the given register.
class GetDelayTimerInstruction < Instruction class GetDelayTimerInstruction < Instruction
property into : Int32 def initialize(@into : Int32)
def initialize(@into)
end end
def to_s(io) def to_s(io)
@@ -366,8 +405,38 @@ module Chalk
@into.to_s(16, io) @into.to_s(16, io)
end end
def to_bin(i, index) def to_bin(table, stack, index)
return 0xf007 | (@into << 8) return 0xf007 | (@into << 8)
end end
end end
# Instruction to set the sound timer to a value.
class SetSoundTimerInstruction < Instruction
def initialize(@from : Int32)
end
def to_s(io)
io << "set_sound R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf018 | (@from << 8)
end
end
class BCDInstruction < Instruction
def initialize(@from : Int32)
end
def to_s(io)
io << "bcd R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf033 | (@from << 8)
end
end
end
end end

View File

@@ -1,10 +1,13 @@
require "lex" require "lex"
module Chalk module Chalk
module Compiler
# The type of a token that can be lexed.
enum TokenType enum TokenType
Any, Any,
Str, Str,
Id, Id,
SpriteRow,
LitDec, LitDec,
LitBin, LitBin,
LitHex, LitHex,
@@ -19,6 +22,7 @@ module Chalk
KwInline KwInline
KwFun KwFun
KwU0 KwU0
KwU4
KwU8 KwU8
KwU12 KwU12
KwVar KwVar
@@ -28,14 +32,21 @@ module Chalk
KwReturn KwReturn
end end
# A class that stores the string it matched and its token type.
class Token class Token
def initialize(@string : String, @type : TokenType) def initialize(@string : String, @type : TokenType)
end end
# Gets the string this token represents.
getter string : String getter string : String
# Gets the type of this token.
getter type : TokenType getter type : TokenType
end end
# Creates a new Lexer with default token values.
# The lexer is backed by liblex. When a string is
# matched by several tokens, the longest match is chosen
# first, followed by the match with the highest enum value.
class Lexer class Lexer
def initialize def initialize
@lexer = Lex::Lexer.new @lexer = Lex::Lexer.new
@@ -44,6 +55,8 @@ module Chalk
TokenType::Str.value) TokenType::Str.value)
@lexer.add_pattern("[a-zA-Z_][a-zA-Z_0-9]*", @lexer.add_pattern("[a-zA-Z_][a-zA-Z_0-9]*",
TokenType::Id.value) TokenType::Id.value)
@lexer.add_pattern("`[ x]*`",
TokenType::SpriteRow.value)
@lexer.add_pattern("[0-9]+", @lexer.add_pattern("[0-9]+",
TokenType::LitDec.value) TokenType::LitDec.value)
@lexer.add_pattern("0b[0-1]+", @lexer.add_pattern("0b[0-1]+",
@@ -61,6 +74,7 @@ module Chalk
@lexer.add_pattern("inline", TokenType::KwInline.value) @lexer.add_pattern("inline", TokenType::KwInline.value)
@lexer.add_pattern("fun", TokenType::KwFun.value) @lexer.add_pattern("fun", TokenType::KwFun.value)
@lexer.add_pattern("u0", TokenType::KwU0.value) @lexer.add_pattern("u0", TokenType::KwU0.value)
@lexer.add_pattern("u4", TokenType::KwU4.value)
@lexer.add_pattern("u8", TokenType::KwU8.value) @lexer.add_pattern("u8", TokenType::KwU8.value)
@lexer.add_pattern("u12", TokenType::KwU12.value) @lexer.add_pattern("u12", TokenType::KwU12.value)
@lexer.add_pattern("var", TokenType::KwVar.value) @lexer.add_pattern("var", TokenType::KwVar.value)
@@ -70,6 +84,7 @@ module Chalk
@lexer.add_pattern("return", TokenType::KwReturn.value) @lexer.add_pattern("return", TokenType::KwReturn.value)
end end
# Converts a string into tokens.
def lex(string) def lex(string)
return @lexer.lex(string) return @lexer.lex(string)
.select { |t| !t[0][0].whitespace? } .select { |t| !t[0][0].whitespace? }
@@ -80,3 +95,4 @@ module Chalk
end end
end end
end end
end

View File

@@ -1,41 +1,70 @@
module Chalk module Chalk
module Compiler
# Class to optimize instructions.
class Optimizer class Optimizer
# Checks if *inst* is "dead code",
# an instruction that is completely useless.
private def check_dead(inst) private def check_dead(inst)
if inst.is_a?(LoadRegInstruction) if inst.is_a?(Ir::LoadRegInstruction)
return inst.from == inst.into return inst.from == inst.into
end end
return false return false
end end
private def optimize!(instructions, range) # Optimizes *instructions* in the basic block given by the *range*,
offset = 0 # storing addresses of instructions to be deleted into *deletions*,
# and the number of deleted instructions so far into *deletions_at*
private def optimize!(instructions, range, deletions, deletions_at)
range.each do |index| range.each do |index|
if check_dead(instructions[index + offset]) if check_dead(instructions[index])
instructions.delete_at(index + offset) deletions << index
offset -= 1
end end
deletions_at[index] = deletions.size
end end
return offset
end end
# Optimizes the given list of instructions.
# The basic blocks are inferred from the various
# jumps and skips.
def optimize(instructions) def optimize(instructions)
instructions = instructions.dup instructions = instructions.dup
block_boundaries = [instructions.size] block_boundaries = [instructions.size]
instructions.each_with_index do |inst, i| instructions.each_with_index do |inst, i|
if inst.is_a?(JumpRelativeInstruction) if inst.is_a?(Ir::JumpRelativeInstruction)
block_boundaries << (i + 1)
block_boundaries << (inst.offset + i) block_boundaries << (inst.offset + i)
end end
if inst.is_a?(Ir::SkipNeInstruction | Ir::SkipEqInstruction |
Ir::SkipRegEqInstruction | Ir::SkipRegNeInstruction)
block_boundaries << (i + 1)
end end
block_boundaries.sort! end
block_boundaries.uniq!.sort!
previous = 0 previous = 0
offset = 0 deletions = [] of Int32
deletions_at = {} of Int32 => Int32
block_boundaries.each do |boundary| block_boundaries.each do |boundary|
range = (previous + offset)...(boundary + offset) range = previous...boundary
offset += optimize!(instructions, range) optimize!(instructions, range, deletions, deletions_at)
previous = boundary previous = boundary
end end
instructions.each_with_index do |inst, i|
next if !inst.is_a?(Ir::JumpRelativeInstruction)
jump_to = inst.offset + i
next unless deletions_at[jump_to]?
deletions_offset = deletions_at[i] - deletions_at[jump_to]
inst.offset += deletions_offset
end
deletions.reverse!
deletions.each do |i|
instructions.delete_at i
end
return instructions return instructions
end end
end end
end end
end

View File

@@ -1,138 +1,174 @@
require "./parser_builder.cr" require "./parser_builder.cr"
module Chalk module Chalk
module ParserCombinators
# Parser created out of the various parser combinators.
class Parser class Parser
include ParserBuilder include ParserBuilder
private def create_type private def create_sprite
either(type(TokenType::KwU0), type(TokenType::KwU8), type(TokenType::KwU12)) type(Compiler::TokenType::KwSprite)
.then(type(Compiler::TokenType::Id))
.then(char('['))
.then(many(type(Compiler::TokenType::SpriteRow)))
.then(char(']'))
.transform do |array|
array = array.flatten
name = array[1].string
sprite = Compiler::Sprite.new(array[3..array.size - 2].map &.string)
Trees::TreeSprite.new(name, sprite).as(Trees::Tree)
end
end end
# Creates a parser for a type.
private def create_type
either(type(Compiler::TokenType::KwU0),
type(Compiler::TokenType::KwU4),
type(Compiler::TokenType::KwU8),
type(Compiler::TokenType::KwU12))
end
# Creates a parser for an integer literal.
private def create_lit private def create_lit
dec_parser = type(TokenType::LitDec).transform &.string.to_i64 dec_parser = type(Compiler::TokenType::LitDec).transform &.string.to_i64
hex_parser = type(TokenType::LitHex).transform &.string.lchop("0x").to_i64(16) hex_parser = type(Compiler::TokenType::LitHex).transform &.string.lchop("0x").to_i64(16)
bin_parser = type(TokenType::LitBin).transform &.string.lchop("0b").to_i64(2) bin_parser = type(Compiler::TokenType::LitBin).transform &.string.lchop("0b").to_i64(2)
lit_parser = either(dec_parser, hex_parser, bin_parser).transform { |it| TreeLit.new(it).as(Tree) } lit_parser = either(dec_parser, hex_parser, bin_parser).transform { |it| Trees::TreeLit.new(it).as(Trees::Tree) }
return lit_parser return lit_parser
end end
# Creates a parser for an operation with a given *atom* parser
# and *op* parser.
private def create_op_expr(atom, op) private def create_op_expr(atom, op)
pl = PlaceholderParser(Tree).new pl = PlaceholderParser(Trees::Tree).new
recurse = atom.then(op).then(pl).transform do |arr| recurse = atom.then(op).then(pl).transform do |arr|
arr = arr.flatten arr = arr.flatten
TreeOp.new( Trees::TreeOp.new(
arr[1].as(Token).type, arr[1].as(Compiler::Token).type,
arr[0].as(Tree), arr[0].as(Trees::Tree),
arr[2].as(Tree)).as(Tree) arr[2].as(Trees::Tree)).as(Trees::Tree)
end end
pl.parser = either(recurse, atom) pl.parser = either(recurse, atom)
return pl return pl
end end
# Creates a parser to parse layers of *ops* with multiple
# levels of precedence, specified by their order. The *atom*
# is the most basic expression.
private def create_op_exprs(atom, ops) private def create_op_exprs(atom, ops)
ops.reduce(atom) do |previous, current| ops.reduce(atom) do |previous, current|
create_op_expr(previous, current) create_op_expr(previous, current)
end end
end end
# Creates a parser for a call, with the given expression parser.
private def create_call(expr) private def create_call(expr)
call = type(TokenType::Id).then(char '(').then(delimited(expr, char ',')).then(char ')').transform do |arr| call = type(Compiler::TokenType::Id).then(char '(').then(delimited(expr, char ',')).then(char ')').transform do |arr|
arr = arr.flatten arr = arr.flatten
name = arr[0].as(Token).string name = arr[0].as(Compiler::Token).string
params = arr[2..arr.size - 2].map &.as(Tree) params = arr[2..arr.size - 2].map &.as(Trees::Tree)
TreeCall.new(name, params).as(Tree) Trees::TreeCall.new(name, params).as(Trees::Tree)
end end
return call return call
end end
# Creates a parser for an expression.
private def create_expr private def create_expr
expr_place = PlaceholderParser(Tree).new expr_place = PlaceholderParser(Trees::Tree).new
literal = create_lit literal = create_lit
id = type(TokenType::Id).transform { |it| TreeId.new(it.string).as(Tree) } id = type(Compiler::TokenType::Id).transform { |it| Trees::TreeId.new(it.string).as(Trees::Tree) }
call = create_call(expr_place) call = create_call(expr_place)
atom = either(literal, call, id) atom = either(literal, call, id)
ops = [either(type(TokenType::OpMul), type(TokenType::OpDiv)), ops = [either(type(Compiler::TokenType::OpMul), type(Compiler::TokenType::OpDiv)),
either(type(TokenType::OpAdd), type(TokenType::OpSub)), either(type(Compiler::TokenType::OpAdd), type(Compiler::TokenType::OpSub)),
type(TokenType::OpXor), type(Compiler::TokenType::OpXor),
type(TokenType::OpAnd), type(Compiler::TokenType::OpAnd),
type(TokenType::OpOr)] type(Compiler::TokenType::OpOr)]
expr = create_op_exprs(atom, ops) expr = create_op_exprs(atom, ops)
expr_place.parser = expr expr_place.parser = expr
return expr return expr
end end
# Creates a parser for a var statement.
private def create_var(expr) private def create_var(expr)
var = type(TokenType::KwVar).then(type(TokenType::Id)).then(char '=').then(expr).then(char ';').transform do |arr| var = type(Compiler::TokenType::KwVar).then(type(Compiler::TokenType::Id)).then(char '=').then(expr).then(char ';').transform do |arr|
arr = arr.flatten arr = arr.flatten
name = arr[1].as(Token).string name = arr[1].as(Compiler::Token).string
exp = arr[arr.size - 2].as(Tree) exp = arr[arr.size - 2].as(Trees::Tree)
TreeVar.new(name, exp).as(Tree) Trees::TreeVar.new(name, exp).as(Trees::Tree)
end end
return var return var
end end
# Creates a parser for an assignment statement.
private def create_assign(expr) private def create_assign(expr)
assign = type(TokenType::Id).then(char '=').then(expr).then(char ';').transform do |arr| assign = type(Compiler::TokenType::Id).then(char '=').then(expr).then(char ';').transform do |arr|
arr = arr.flatten arr = arr.flatten
name = arr[0].as(Token).string name = arr[0].as(Compiler::Token).string
exp = arr[arr.size - 2].as(Tree) exp = arr[arr.size - 2].as(Trees::Tree)
TreeAssign.new(name, exp).as(Tree) Trees::TreeAssign.new(name, exp).as(Trees::Tree)
end end
return assign return assign
end end
# Creates a parser for a basic statement.
private def create_basic(expr) private def create_basic(expr)
basic = expr.then(char ';').transform do |arr| basic = expr.then(char ';').transform do |arr|
arr.flatten[0].as(Tree) arr.flatten[0].as(Trees::Tree)
end end
return basic return basic
end end
# Creates a parser for an if statement.
private def create_if(expr, block) private def create_if(expr, block)
iff = type(TokenType::KwIf).then(char '(').then(expr).then(char ')').then(block) iff = type(Compiler::TokenType::KwIf).then(char '(').then(expr).then(char ')').then(block)
.then(optional(type(TokenType::KwElse).then(block))) .then(optional(type(Compiler::TokenType::KwElse).then(block)))
.transform do |arr| .transform do |arr|
arr = arr.flatten arr = arr.flatten
cond = arr[2].as(Tree) cond = arr[2].as(Trees::Tree)
code = arr[4].as(Tree) code = arr[4].as(Trees::Tree)
otherwise = arr.size == 7 ? arr[6].as(Tree) : nil otherwise = arr.size == 7 ? arr[6].as(Trees::Tree) : nil
TreeIf.new(cond, code, otherwise).as(Tree) Trees::TreeIf.new(cond, code, otherwise).as(Trees::Tree)
end end
return iff return iff
end end
# Creates a parser for a while loop.
private def create_while(expr, block) private def create_while(expr, block)
whilee = type(TokenType::KwWhile).then(char '(').then(expr).then(char ')').then(block).transform do |arr| whilee = type(Compiler::TokenType::KwWhile).then(char '(').then(expr).then(char ')').then(block).transform do |arr|
arr = arr.flatten arr = arr.flatten
cond = arr[2].as(Tree) cond = arr[2].as(Trees::Tree)
code = arr[4].as(Tree) code = arr[4].as(Trees::Tree)
TreeWhile.new(cond, code).as(Tree) Trees::TreeWhile.new(cond, code).as(Trees::Tree)
end end
return whilee return whilee
end end
# Creates a parser for a return.
private def create_return(expr) private def create_return(expr)
returnn = type(TokenType::KwReturn).then(expr).then(char ';').transform do |arr| returnn = type(Compiler::TokenType::KwReturn).then(expr).then(char ';').transform do |arr|
arr = arr.flatten arr = arr.flatten
value = arr[1].as(Tree) value = arr[1].as(Trees::Tree)
TreeReturn.new(value).as(Tree) Trees::TreeReturn.new(value).as(Trees::Tree)
end end
return returnn return returnn
end end
# Creates a parser for a block of statements.
private def create_block(statement) private def create_block(statement)
block = char('{').then(many(statement)).then(char '}').transform do |arr| block = char('{').then(many(statement)).then(char '}').transform do |arr|
arr = arr.flatten arr = arr.flatten
params = arr[1..arr.size - 2].map &.as(Tree) params = arr[1..arr.size - 2].map &.as(Trees::Tree)
TreeBlock.new(params).as(Tree) Trees::TreeBlock.new(params).as(Trees::Tree)
end end
return block return block
end end
# Creates a statement and block parser, returning both.
private def create_statement_block private def create_statement_block
statement_place = PlaceholderParser(Tree).new statement_place = PlaceholderParser(Trees::Tree).new
expr = create_expr expr = create_expr
block = create_block(statement_place) block = create_block(statement_place)
iff = create_if(expr, block) iff = create_if(expr, block)
@@ -146,28 +182,37 @@ module Chalk
return {statement, block} return {statement, block}
end end
# Creates a parser for a function declaration.
private def create_func(block, type) private def create_func(block, type)
func = type(TokenType::KwFun).then(type(TokenType::Id)) func = type(Compiler::TokenType::KwFun).then(type(Compiler::TokenType::Id))
.then(char '(').then(delimited(type(TokenType::Id), char ',')).then(char ')') .then(char '(').then(delimited(type(Compiler::TokenType::Id), char ',')).then(char ')')
.then(char ':').then(type) .then(char ':').then(type)
.then(block).transform do |arr| .then(block).transform do |arr|
arr = arr.flatten arr = arr.flatten
name = arr[1].as(Token).string name = arr[1].as(Compiler::Token).string
params = arr[3..arr.size - 5].map &.as(Token).string params = arr[3..arr.size - 5].map &.as(Compiler::Token).string
code = arr[arr.size - 1].as(Tree) code = arr[arr.size - 1].as(Trees::Tree)
type = arr[arr.size - 2].as(Token).type type = arr[arr.size - 2].as(Compiler::Token).type
TreeFunction.new(name, params, code) table = {
Compiler::TokenType::KwU0 => Compiler::Type::U0,
Compiler::TokenType::KwU4 => Compiler::Type::U4,
Compiler::TokenType::KwU8 => Compiler::Type::U8,
Compiler::TokenType::KwU12 => Compiler::Type::U12
}
Trees::TreeFunction.new(name, params, table[type], code).as(Trees::Tree)
end end
return func return func
end end
def initialize def initialize
_, block = create_statement_block _, block = create_statement_block
@parser = many(create_func(block, create_type)).as(BasicParser(Array(TreeFunction))) @parser = many(either(create_func(block, create_type), create_sprite)).as(BasicParser(Array(Trees::Tree)))
end end
# Parses the given tokens into a tree.
def parse?(tokens) def parse?(tokens)
return @parser.parse?(tokens, 0).try &.[0] return @parser.parse?(tokens, 0).try &.[0]
end end
end end
end end
end

View File

@@ -2,37 +2,47 @@ require "./lexer.cr"
require "./parsers.cr" require "./parsers.cr"
module Chalk module Chalk
module ParserCombinators
module ParserBuilder module ParserBuilder
def type(type) : BasicParser(Token) # Creates a parser for a given token type.
return TypeParser.new(type).as(BasicParser(Token)) def type(type) : BasicParser(Compiler::Token)
return TypeParser.new(type).as(BasicParser(Compiler::Token))
end end
def char(type) : BasicParser(Token) # Creates a parser for a specific character.
return CharParser.new(type).as(BasicParser(Token)) def char(type) : BasicParser(Compiler::Token)
return CharParser.new(type).as(BasicParser(Compiler::Token))
end end
# Creates a parser that transforms a value according to a block.
def transform(parser : BasicParser(T), &transform : T -> R) forall T, R def transform(parser : BasicParser(T), &transform : T -> R) forall T, R
return TransformParser.new(parser, &transform).as(BasicParser(R)) return TransformParser.new(parser, &transform).as(BasicParser(R))
end end
# Creates a parser that allows for failure to match.
def optional(parser : BasicParser(T)) : BasicParser(T?) forall T def optional(parser : BasicParser(T)) : BasicParser(T?) forall T
return OptionalParser.new(parser).as(BasicParser(T?)) return OptionalParser.new(parser).as(BasicParser(T?))
end end
# Creates a parser that tries several parsers in sequence until one succeeds.
def either(*args : BasicParser(T)) : BasicParser(T) forall T def either(*args : BasicParser(T)) : BasicParser(T) forall T
return EitherParser.new(args.to_a).as(BasicParser(T)) return EitherParser.new(args.to_a).as(BasicParser(T))
end end
# Creates a parser that parses one or more of the given parsers.
def many(parser : BasicParser(T)) : BasicParser(Array(T)) forall T def many(parser : BasicParser(T)) : BasicParser(Array(T)) forall T
return ManyParser.new(parser).as(BasicParser(Array(T))) return ManyParser.new(parser).as(BasicParser(Array(T)))
end end
# Creates a parser that parses one parser delimited by another.
def delimited(parser : BasicParser(T), delimiter : BasicParser(R)) : BasicParser(Array(T)) forall T, R def delimited(parser : BasicParser(T), delimiter : BasicParser(R)) : BasicParser(Array(T)) forall T, R
return DelimitedParser.new(parser, delimiter).as(BasicParser(Array(T))) return DelimitedParser.new(parser, delimiter).as(BasicParser(Array(T)))
end end
# Creates a parser that parses one parser, then the next.
def then(first : BasicParser(T), second : BasicParser(R)) forall T, R def then(first : BasicParser(T), second : BasicParser(R)) forall T, R
return NextParser.new(first, second).as(BasicParser(Array(T | R))) return NextParser.new(first, second).as(BasicParser(Array(T | R)))
end end
end end
end end
end

View File

@@ -1,23 +1,35 @@
module Chalk module Chalk
module ParserCombinators
# Abstract class for a parser function,
# as used in parser combinators. This is basically
# a building block of parsing.
abstract class BasicParser(T) abstract class BasicParser(T)
abstract def parse?(tokens : Array(Token), # Attempts to parse the given *tokens*, starting at the given *index*.
abstract def parse?(tokens : Array(Compiler::Token),
index : Int64) : Tuple(T, Int64)? index : Int64) : Tuple(T, Int64)?
# Attempts to parse the given tokens like `#parse?`, but throws
# on error.
def parse(tokens, index) def parse(tokens, index)
return parse?(tokens, index).not_nil! return parse?(tokens, index).not_nil!
end end
# Applies the given transformation to this parser,
# creating a new parser.
def transform(&transform : T -> R) forall R def transform(&transform : T -> R) forall R
return TransformParser.new(self, &transform).as(BasicParser(R)) return TransformParser.new(self, &transform).as(BasicParser(R))
end end
# Creates a sequence with the given parser,
# creating a new parser.
def then(other : BasicParser(R)) : BasicParser(Array(T | R)) forall R def then(other : BasicParser(R)) : BasicParser(Array(T | R)) forall R
return NextParser.new(self, other).as(BasicParser(Array(T | R))) return NextParser.new(self, other).as(BasicParser(Array(T | R)))
end end
end end
class TypeParser < BasicParser(Token) # Parser that expects a specific token type.
def initialize(@type : TokenType) class TypeParser < BasicParser(Compiler::Token)
def initialize(@type : Compiler::TokenType)
end end
def parse?(tokens, index) def parse?(tokens, index)
@@ -27,18 +39,21 @@ module Chalk
end end
end end
class CharParser < BasicParser(Token) # Parser that expects a specific character.
class CharParser < BasicParser(Compiler::Token)
def initialize(@char : Char) def initialize(@char : Char)
end end
def parse?(tokens, index) def parse?(tokens, index)
return nil unless index < tokens.size return nil unless index < tokens.size
return nil unless (tokens[index].type == TokenType::Any) && return nil unless (tokens[index].type == Compiler::TokenType::Any) &&
tokens[index].string[0] == @char tokens[index].string[0] == @char
return {tokens[index], index + 1} return {tokens[index], index + 1}
end end
end end
# Parser that applies a transformation to the output
# of its child parser.
class TransformParser(T, R) < BasicParser(R) class TransformParser(T, R) < BasicParser(R)
def initialize(@parser : BasicParser(T), &@block : T -> R) def initialize(@parser : BasicParser(T), &@block : T -> R)
end end
@@ -51,6 +66,8 @@ module Chalk
end end
end end
# Parser that attempts to use its child parser,
# and successfully returns nil if the child parser fails.
class OptionalParser(T) < BasicParser(T?) class OptionalParser(T) < BasicParser(T?)
def initialize(@parser : BasicParser(T)) def initialize(@parser : BasicParser(T))
end end
@@ -63,6 +80,7 @@ module Chalk
end end
end end
# Parser that tries all of its children until one succeeds.
class EitherParser(T) < BasicParser(T) class EitherParser(T) < BasicParser(T)
def initialize(@parsers : Array(BasicParser(T))) def initialize(@parsers : Array(BasicParser(T)))
end end
@@ -77,6 +95,7 @@ module Chalk
end end
end end
# Parser that parses at least one of a given type.
class ManyParser(T) < BasicParser(Array(T)) class ManyParser(T) < BasicParser(Array(T))
def initialize(@parser : BasicParser(T)) def initialize(@parser : BasicParser(T))
end end
@@ -91,6 +110,8 @@ module Chalk
end end
end end
# Parser that parses at least 0 of its child parser,
# delimited with its other child parser.
class DelimitedParser(T, R) < BasicParser(Array(T)) class DelimitedParser(T, R) < BasicParser(Array(T))
def initialize(@parser : BasicParser(T), @delimiter : BasicParser(R)) def initialize(@parser : BasicParser(T), @delimiter : BasicParser(R))
end end
@@ -113,6 +134,8 @@ module Chalk
end end
end end
# Parser that parses using the first parser, and, if it succeeds,
# parses using the second parses.
class NextParser(T, R) < BasicParser(Array(T | R)) class NextParser(T, R) < BasicParser(Array(T | R))
def initialize(@first : BasicParser(T), @second : BasicParser(R)) def initialize(@first : BasicParser(T), @second : BasicParser(R))
end end
@@ -132,6 +155,7 @@ module Chalk
end end
end end
# Parser used to declare recursive grammars.
class PlaceholderParser(T) < BasicParser(T) class PlaceholderParser(T) < BasicParser(T)
property parser : BasicParser(T)? property parser : BasicParser(T)?
@@ -144,3 +168,4 @@ module Chalk
end end
end end
end end
end

View File

@@ -1,12 +1,14 @@
require "./tree.cr" require "./tree.cr"
module Chalk module Chalk
module Trees
# Visitor that prints a `Tree`.
class PrintVisitor < Visitor class PrintVisitor < Visitor
def initialize(@stream : IO) def initialize(@stream : IO)
@indent = 0 @indent = 0
end end
def print_indent private def print_indent
@indent.times do @indent.times do
@stream << " " @stream << " "
end end
@@ -74,3 +76,4 @@ module Chalk
end end
end end
end end
end

97
src/chalk/sprite.cr Normal file
View File

@@ -0,0 +1,97 @@
module Chalk
module Compiler
class Sprite
def initialize
@pixels = Hash(UInt8, UInt8).new(default_value: 0_u8)
end
def height
return (@pixels.keys.max || 0_u8) + 1
end
def initialize(string, blank_char = ' ')
@pixels = Hash(UInt8, UInt8).new(default_value: 0_u8)
string.split("\n").each_with_index do |s, i|
break if i > 15
index = 0
byte = 0_u8
s.each_char do |char|
break if index > 7
bit = (char == blank_char) ? 0_u8 : 1_u8
byte |= (bit << (7 - index))
index += 1
end
@pixels[i.to_u8] = byte
end
end
def initialize(tokens)
raise "Invalid sprite" if tokens.size > 15
@pixels = Hash(UInt8, UInt8).new(default_value: 0_u8)
tokens.each_with_index do |token, i|
byte = 0_u8
substring = token[1..token.size - 2]
raise "Invalid sprite" if substring.size > 8
bit = 0b10000000
substring.each_char do |char|
byte |= bit if char == 'x'
bit >>= 1
end
@pixels[i.to_u8] = byte
end
end
def set_pixel(x, y)
raise "Invalid x-coordinate" if x > 7
raise "Invalid y-coordinate" if y > 15
x = x.to_u8
y = y.to_u8
char = @pixels.fetch y, 0_u8
char |= (1 << (7 - x))
@pixels[y] = char
end
def unset_pixel(x, y)
raise "Invalid x-coordinate" if x > 7
raise "Invalid y-coordinate" if y > 15
x = x.to_u8
y = y.to_u8
char = @pixels.fetch y, 0_u8
char &= ~(1 << (7 - x))
@pixels[y] = char
end
def toggle_pixel(x, y)
raise "Invalid x-coordinate" if x > 7
raise "Invalid y-coordinate" if y > 15
x = x.to_u8
y = y.to_u8
char = @pixels.fetch y, 0_u8
char ^= (1 << (7 - x))
@pixels[y] = char
end
def draw(io = STDOUT, blank_char = ' ', ink_char = 'x')
until_y = @pixels.keys.max?
return unless until_y
(0..until_y).each do |y|
row = @pixels.fetch y, 0_u8
pointer = 0b10000000
while pointer != 0
draw_pixel = (row & pointer) != 0
io << (draw_pixel ? ink_char : blank_char)
pointer = pointer >> 1
end
io << '\n'
end
end
def encode
if until_y = @pixels.keys.max?
return (0..until_y).map { |it| @pixels.fetch it, 0_u8 }
end
return [0_u8]
end
end
end
end

View File

@@ -1,12 +1,18 @@
require "./sprite.cr"
module Chalk module Chalk
class Entry module Compiler
end # An entry that represents a function in the symbol table.
class FunctionEntry
# Gets the function stored in this entry.
getter function
# Gets the address in code of this function.
getter addr
# Sets the address in code of this function.
setter addr
class FunctionEntry < Entry def initialize(@function : Trees::TreeFunction | Builtin::BuiltinFunction | Builtin::InlineFunction,
property function : TreeFunction | BuiltinFunction | InlineFunction @addr = -1)
property addr : Int32
def initialize(@function, @addr = -1)
end end
def to_s(io) def to_s(io)
@@ -14,10 +20,13 @@ module Chalk
end end
end end
class VarEntry < Entry # An entry that represents a variable in the symbol table.
property register : Int32 class VarEntry
# Gets the register occupied by the variable
# in this entry.
getter register
def initialize(@register) def initialize(@register : Int32)
end end
def to_s(io) def to_s(io)
@@ -25,27 +34,48 @@ module Chalk
end end
end end
class SpriteEntry
property sprite : Sprite
property addr : Int32
def initialize(@sprite, @addr = -1)
end
end
# A symbol table.
class Table class Table
property parent : Table? # Gets the parent of this table.
getter parent
# Gets the functions hash.
getter functions
# Gets the variables hash.
getter vars
# Gets the sprites hash.
getter sprites
def initialize(@parent = nil) def initialize(@parent : Table? = nil)
@data = {} of String => Entry @functions = {} of String => FunctionEntry
@vars = {} of String => VarEntry
@sprites = {} of String => SpriteEntry
end end
def []?(key) macro table_functions(name)
if entry = @data[key]? def get_{{name}}?(key)
return entry @{{name}}s[key]? || @parent.try &.get_{{name}}?(key)
end
return @parent.try &.[key]?
end end
def []=(key, entry) def set_{{name}}(key, value)
@data[key] = entry @{{name}}s[key] = value
end end
end
table_functions function
table_functions var
table_functions sprite
def to_s(io) def to_s(io)
@parent.try &.to_s(io) @parent.try &.to_s(io)
io << @data.map { |k, v| k + ": " + v.to_s }.join("\n") end
end end
end end
end end

View File

@@ -1,4 +1,6 @@
module Chalk module Chalk
module Trees
# A class used to visit nodes of a tree.
class Visitor class Visitor
def visit(tree) def visit(tree)
end end
@@ -7,12 +9,21 @@ module Chalk
end end
end end
# A class used to transform a tree, bottom up.
# "Modern Compiler Design" refers to this technique
# as BURS.
class Transformer class Transformer
def transform(tree) def transform(tree)
return tree return tree
end end
end end
class Reducer(T)
def reduce(tree, children)
end
end
# The base class of a tree.
class Tree class Tree
def accept(v) def accept(v)
v.visit(self) v.visit(self)
@@ -22,8 +33,13 @@ module Chalk
def apply(t) def apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [] of T)
end
end end
# A tree that represents an ID.
class TreeId < Tree class TreeId < Tree
property id : String property id : String
@@ -31,6 +47,7 @@ module Chalk
end end
end end
# A tree that represents an integer literal.
class TreeLit < Tree class TreeLit < Tree
property lit : Int64 property lit : Int64
@@ -38,6 +55,7 @@ module Chalk
end end
end end
# A tree that represents a function call.
class TreeCall < Tree class TreeCall < Tree
property name : String property name : String
property params : Array(Tree) property params : Array(Tree)
@@ -57,10 +75,15 @@ module Chalk
end end
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @params.map &.reduce(r))
end
end end
# A tree that represents an operation on two values.
class TreeOp < Tree class TreeOp < Tree
property op : TokenType property op : Compiler::TokenType
property left : Tree property left : Tree
property right : Tree property right : Tree
@@ -79,8 +102,13 @@ module Chalk
@right = @right.apply(t) @right = @right.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [@left.reduce(r), @right.reduce(r)])
end
end end
# A tree that represents a block of statements.
class TreeBlock < Tree class TreeBlock < Tree
property children : Array(Tree) property children : Array(Tree)
@@ -99,14 +127,20 @@ module Chalk
end end
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @children.map &.reduce(r))
end
end end
# A tree that represents a function declaration.
class TreeFunction < Tree class TreeFunction < Tree
property name : String property name : String
property params : Array(String) property params : Array(String)
property block : Tree property block : Tree
getter type
def initialize(@name, @params, @block) def initialize(@name, @params, return_type : Compiler::Type, @block)
@type = Compiler::FunctionType.new([Compiler::Type::U8] * @params.size, return_type)
end end
def param_count def param_count
@@ -123,8 +157,14 @@ module Chalk
@block = @block.apply(t) @block = @block.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [@block.reduce(r)])
end
end end
# A tree that represents the declaration of
# a new variable.
class TreeVar < Tree class TreeVar < Tree
property name : String property name : String
property expr : Tree property expr : Tree
@@ -142,8 +182,14 @@ module Chalk
@expr = @expr.apply(t) @expr = @expr.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end end
# A tree that represents the assignment
# to an existing variable.
class TreeAssign < Tree class TreeAssign < Tree
property name : String property name : String
property expr : Tree property expr : Tree
@@ -161,8 +207,13 @@ module Chalk
@expr = @expr.apply(t) @expr = @expr.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end end
# A tree that represents an if statement.
class TreeIf < Tree class TreeIf < Tree
property condition : Tree property condition : Tree
property block : Tree property block : Tree
@@ -185,8 +236,19 @@ module Chalk
@otherwise = @otherwise.try &.apply(t) @otherwise = @otherwise.try &.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
cond = @condition.reduce(r)
blk = @block.reduce(r)
if other = @otherwise
r.reduce(self, [cond, blk,other.reduce(r)])
else
r.reduce(self, [cond, blk])
end
end
end end
# A tree that represents a while loop.
class TreeWhile < Tree class TreeWhile < Tree
property condition : Tree property condition : Tree
property block : Tree property block : Tree
@@ -206,8 +268,13 @@ module Chalk
@block = @block.apply(t) @block = @block.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@condition.reduce(r), @block.reduce(r)])
end
end end
# A tree that represents a return statement.
class TreeReturn < Tree class TreeReturn < Tree
property rvalue : Tree property rvalue : Tree
@@ -224,5 +291,18 @@ module Chalk
@rvalue = @rvalue.apply(t) @rvalue = @rvalue.apply(t)
return t.transform(self) return t.transform(self)
end end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@rvalue.reduce(r)])
end
end
class TreeSprite < Tree
property name : String
property sprite : Compiler::Sprite
def initialize(@name, @sprite)
end
end
end end
end end

35
src/chalk/type.cr Normal file
View File

@@ -0,0 +1,35 @@
module Chalk
module Compiler
# Possible types of values in chalk
enum Type
U0
U4
U8
U12
# Checks if one value can be cast to another.
def casts_to?(other)
return false if other == Type::U0 || self == Type::U0
return other.value >= self.value
end
end
# A type of a function.
class FunctionType
# Gets the types of the function's parameters.
getter param_types
# Gets the return type of the function.
getter return_type
def initialize(@param_types : Array(Type), @return_type : Type)
end
def to_s(io)
io << "("
io << param_types.map(&.to_s).join(", ")
io << ") -> "
return_type.to_s(io)
end
end
end
end

58
src/chalk/typecheck.cr Normal file
View File

@@ -0,0 +1,58 @@
require "./type"
module Chalk
module Trees
# Reducer to check types.
class TypeChecker < Reducer(Compiler::Type)
def initialize(@table : Compiler::Table, @return_type : Compiler::Type)
end
def reduce(t, children)
return Compiler::Type::U0
end
def reduce(t : TreeCall, children)
entry = @table.get_function? t.name
raise "Unknwon function" unless entry
type = entry.function.type
raise "Invalid parameters" if type.param_types.size != children.size
children.each_with_index do |child, i|
raise "Incompatible parameter" if !child.casts_to?(type.param_types[i])
end
return entry.function.type.return_type
end
def reduce(t : TreeId, children)
return Compiler::Type::U8
end
def reduce(t : TreeLit, children)
max_12 = (2 ** 12) - 1
max_8 = (2 ** 8) - 1
max_4 = (2 ** 4) - 1
raise "Number too big" if t.lit > max_12
return Compiler::Type::U12 if t.lit > max_8
return Compiler::Type::U8 if t.lit > max_4
return Compiler::Type::U4
end
def reduce(t : TreeOp, children)
left = children[0]
right = children[1]
return left if right.casts_to?(left)
return right if left.casts_to?(right)
raise "Invalid operation"
end
def reduce(t : TreeAssign | TreeVar, children)
raise "Invalid assignment" if !children[0].casts_to?(Compiler::Type::U8)
return Compiler::Type::U0
end
def reduce(t : TreeReturn, children)
raise "Incompatible return type" if !children[0].casts_to?(@return_type)
return Compiler::Type::U0
end
end
end
end

25
src/test.cr Normal file
View File

@@ -0,0 +1,25 @@
require "./chalk/*"
module Chalk
regex = /([^.]+)\.chalk/
source_dir = "programs"
dest_dir = "out"
Dir.mkdir_p dest_dir
exit if !File.directory? source_dir
Dir.new(source_dir)
.children
.compact_map { |it| regex.match(it) }
.each do |match|
config = Ui::Config.new file: (source_dir + File::SEPARATOR + match[0]),
output: (dest_dir + File::SEPARATOR + match[1] + ".ch8"),
loglevel: Logger::Severity::ERROR,
mode: Ui::OutputMode::Binary
compiler = Compiler::Compiler.new config
begin
compiler.run
rescue e
puts "Exception compiling #{match[0]}"
end
end
end