Compare commits

..

28 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
4ce503d02d Clean up some code. 2018-07-29 22:29:50 -07:00
dff0c8078c Remove useless assignment. 2018-07-29 21:46:10 -07:00
37 changed files with 2450 additions and 1476 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,38 +0,0 @@
require "./lexer.cr"
require "./parsers.cr"
module Chalk
module Builder
def type(type) : BasicParser(Token)
return TypeParser.new(type).as(BasicParser(Token))
end
def char(type) : BasicParser(Token)
return CharParser.new(type).as(BasicParser(Token))
end
def transform(parser : BasicParser(T), &transform : T -> R) forall T, R
return TransformParser.new(parser, &transform).as(BasicParser(R))
end
def optional(parser : BasicParser(T)) : BasicParser(T?) forall T
return OptionalParser.new(parser).as(BasicParser(T?))
end
def either(*args : BasicParser(T)) : BasicParser(T) forall T
return EitherParser.new(args.to_a).as(BasicParser(T))
end
def many(parser : BasicParser(T)) : BasicParser(Array(T)) forall T
return ManyParser.new(parser).as(BasicParser(Array(T)))
end
def delimited(parser : BasicParser(T), delimiter : BasicParser(R)) : BasicParser(Array(T)) forall T, R
return DelimitedParser.new(parser, delimiter).as(BasicParser(Array(T)))
end
def then(first : BasicParser(T), second : BasicParser(R)) forall T, R
return NextParser.new(first, second).as(BasicParser(Array(T | R)))
end
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
# has predefined output.
abstract class BuiltinFunction
# Creates a new function with *param_count* parameters.
def initialize()
end
def initialize(@param_count) # Uses the given `Compiler::Emitter` to output code.
abstract def generate!(codegen)
# Gets the `Compiler::FunctionType` of this function.
abstract def type
end end
def generate!(into) # A function to which a call is not generated. This function
end # is copied everywhere a call to it occurs. Besides this, the
end # function also accepts trees rather than register numbers,
# and therefore can accept and manipulate trees.
abstract class InlineFunction
# Creates a new function with *param_count* parameters.
def initialize()
end
class InlineFunction # Generates code like `Compiler::CodeGenerator` would.
getter param_count : Int32 # The *codegen* parameter is used to emit instructions,
# the *params* are trees that are being passed as arguments.
def initialize(@param_count) # See `Compiler::CodeGenerator#generate!` for what the other parameters mean.
end abstract def generate!(codegen, params, table, target, free)
# Gets the `Compiler::FunctionType` of this function.
def generate!(codegen, params, table, target, free) abstract def type
end end
end end
end end

View File

@@ -2,142 +2,147 @@ require "./ir.cr"
require "./emitter.cr" require "./emitter.cr"
module Chalk module Chalk
class CodeGenerator module Compiler
include Emitter # A class that converts a tree into the corresponding
# intermediate representation, without optimizing.
class CodeGenerator
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 : Trees::TreeFunction)
@registers = 0
@instructions = [] of Ir::Instruction
@table = Table.new table
def initialize(table, @function : TreeFunction) @function.params.each do |param|
@registers = 0 @table.set_var param, VarEntry.new @registers
@instructions = [] of Instruction @registers += 1
@table = Table.new table
@function.params.each do |param|
@table[param] = VarEntry.new @registers
@registers += 1
end
end
def generate!(tree, function : InlineFunction, table, target, free)
start = free
function.generate!(self, tree.params, table, target, free)
end
def generate!(tree, function : TreeFunction | BuiltinFunction, table, target, free)
start_at = free
# Move I to stack
setis
# Get to correct stack position
addi STACK_REG
# Store variables
store (start_at - 1) unless start_at == 0
# Increment I and stack position
load free, start_at
opr TokenType::OpAdd, STACK_REG, free
addi free
# Calculate the parameters
tree.params.each do |param|
generate! param, table, free, free + 1
free += 1
end
# Call the function
tree.params.size.times do |time|
loadr time, time + start_at
end
call tree.name
# Reduce stack pointer
load free, start_at
opr TokenType::OpSub, STACK_REG, free
# Move I to stack
setis
# Get to correct stack position
addi STACK_REG
# Restore
restore (start_at - 1) unless start_at == 0
# Get call value into target
loadr target, RETURN_REG
end
def generate!(tree, table, target, free)
case tree
when TreeId
entry = table[tree.id]?
raise "Unknown variable" unless entry &&
entry.is_a?(VarEntry)
loadr target, entry.register
when TreeLit
load target, tree.lit
when TreeOp
generate! tree.left, table, target, free
generate! tree.right, table, free, free + 1
opr tree.op, target, free
when TreeCall
entry = table[tree.name]?
raise "Unknown function" unless entry &&
entry.is_a?(FunctionEntry)
function = entry.function
raise "Invalid call" if tree.params.size != function.param_count
generate! tree, function, table, target, free
when TreeBlock
table = Table.new(table)
tree.children.each do |child|
free += generate! child, table, free, free + 1
end end
when TreeVar end
entry = table[tree.name]?
if entry == nil # Generates code for an inline function, with the given *tree* being the `Trees::TreeCall`
entry = VarEntry.new free # 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)
end
# 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
to_stack
# Store variables
store (start_at - 1) unless start_at == 0
# Increment I and stack position
load free, start_at
opr TokenType::OpAdd, STACK_REG, free
addi free
# Calculate the parameters
tree.params.each do |param|
generate! param, table, free, free + 1
free += 1 free += 1
table[tree.name] = entry
end end
raise "Unknown variable" unless entry.is_a?(VarEntry) # Call the function
generate! tree.expr, table, entry.register, free tree.params.size.times do |time|
return 1 loadr time, time + start_at
when TreeAssign end
entry = table[tree.name]? call tree.name
raise "Unknown variable" unless entry &&
entry.is_a?(VarEntry)
generate! tree.expr, table, entry.register, free
when TreeIf
generate! tree.condition, table, free, free + 1
sne free, 0
jump_inst = jr 0
old_size = @instructions.size # Reduce stack pointer
generate! tree.block, table, free, free + 1 load free, start_at
jump_after = jr 0 opr TokenType::OpSub, STACK_REG, free
jump_inst.offset = @instructions.size - old_size + 1 to_stack
# Restore
old_size = @instructions.size restore (start_at - 1) unless start_at == 0
generate! tree.otherwise, table, free, free + 1 if tree.otherwise # Get call value into target
jump_after.offset = @instructions.size - old_size + 1 loadr target, RETURN_REG
when TreeWhile
before_cond = @instructions.size
generate! tree.condition, table, free, free + 1
sne free, 0
cond_jump = jr 0
old_size = @instructions.size
generate! tree.block, table, free, free + 1
after_jump = jr 0
cond_jump.offset = @instructions.size - old_size + 1
after_jump.offset = before_cond - instructions.size + 1
when TreeReturn
generate! tree.rvalue, table, RETURN_REG, free
ret
end end
return 0
end
def generate! # Generates code for a *tree*, using a symbol *table*
generate!(@function.block, @table, -1, @registers) # housing all the names for identifiers in the code.
return @instructions # 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)
case tree
when Trees::TreeId
entry = table.get_var? tree.id
raise "Unknown variable" unless entry
loadr target, entry.register
when Trees::TreeLit
load target, tree.lit
when Trees::TreeOp
generate! tree.left, table, target, free
generate! tree.right, table, free, free + 1
opr tree.op, target, free
when Trees::TreeCall
entry = table.get_function?(tree.name).not_nil!
raise "Unknown function" unless entry.is_a?(FunctionEntry)
generate! tree, entry.function, table, target, free
when Trees::TreeBlock
table = Table.new(table)
tree.children.each do |child|
free += generate! child, table, THROWAWAY_REG, free
end
when Trees::TreeVar
entry = table.get_var? tree.name
if entry == nil
entry = VarEntry.new free
free += 1
table.set_var tree.name, entry
end
generate! tree.expr, table, entry.as(VarEntry).register, free
return 1
when Trees::TreeAssign
entry = table.get_var? tree.name
raise "Unknown variable" unless entry
generate! tree.expr, table, entry.register, free
when Trees::TreeIf
generate! tree.condition, table, free, free + 1
sne free, 0
jump_inst = jr 0
old_size = @instructions.size
generate! tree.block, table, free, free + 1
jump_after = jr 0
jump_inst.offset = @instructions.size - old_size + 1
old_size = @instructions.size
generate! tree.otherwise, table, free, free + 1 if tree.otherwise
jump_after.offset = @instructions.size - old_size + 1
when Trees::TreeWhile
before_cond = @instructions.size
generate! tree.condition, table, free, free + 1
sne free, 0
cond_jump = jr 0
old_size = @instructions.size
generate! tree.block, table, free, free + 1
after_jump = jr 0
cond_jump.offset = @instructions.size - old_size + 1
after_jump.offset = before_cond - instructions.size + 1
when Trees::TreeReturn
generate! tree.rvalue, table, free, free + 1
loadr RETURN_REG, free
ret
end
return 0
end
# Generates code for the function that was given to it.
def generate!
generate!(@function.block, @table, 0, @registers)
return @instructions
end
end end
end end
end end

View File

@@ -3,161 +3,228 @@ require "./constant_folder.cr"
require "./table.cr" require "./table.cr"
module Chalk module Chalk
class Compiler module Compiler
def initialize(@config : Config) # Top-level class to tie together the various
@logger = Logger.new STDOUT # components, such as the `Lexer`,
@logger.debug("Initialized compiler") # `ParserCombinators::Parser`, and `Optimizer`
@logger.level = Logger::DEBUG class Compiler
end # Creates a new compiler with the given *config*.
def initialize(@config : Ui::Config)
private def create_trees(file) @logger = Logger.new STDOUT
string = File.read(file) @logger.debug("Initialized compiler")
@logger.debug("Tokenizing") @logger.level = @config.loglevel
lexer = Lexer.new
tokens = lexer.lex string
if tokens.size == 0 && string != ""
raise "Unable to tokenize file."
end end
@logger.debug("Finished tokenizing")
@logger.debug("Beginning parsing") # Reads a file an extracts instances of
parser = Parser.new # `Trees:TreeFunction`.
if trees = parser.parse?(tokens) private def create_trees(file)
@logger.debug("Finished parsing") string = File.read(file)
@logger.debug("Beginning constant folding") @logger.debug("Tokenizing")
folder = ConstantFolder.new lexer = Lexer.new
trees.map! do |tree| tokens = lexer.lex string
@logger.debug("Constant folding #{tree.name}") if tokens.size == 0 && string != ""
tree.apply(folder).as(TreeFunction) raise "Unable to tokenize file."
end end
@logger.debug("Done constant folding") @logger.debug("Finished tokenizing")
return trees @logger.debug("Beginning parsing")
end parser = ParserCombinators::Parser.new
raise "Unable to parse file." if trees = parser.parse?(tokens)
end @logger.debug("Finished parsing")
@logger.debug("Beginning constant folding")
private def create_table(trees) folder = Trees::ConstantFolder.new
table = Table.new trees.map! do |tree|
@logger.debug("Creating symbol table") next tree unless tree.is_a?(Trees::TreeFunction)
trees.each do |tree| @logger.debug("Constant folding #{tree.name}")
@logger.debug("Storing #{tree.name} in symbol table") tree.apply(folder)
table[tree.name] = FunctionEntry.new tree end
end @logger.debug("Done constant folding")
@logger.debug("Done creating symbol table") return trees
end
table["draw"] = FunctionEntry.new InlineDrawFunction.new raise "Unable to parse file."
table["get_key"] = FunctionEntry.new InlineAwaitKeyFunction.new
table["get_font"] = FunctionEntry.new InlineGetFontFunction.new
table["set_delay"] = FunctionEntry.new InlineSetDelayFunction.new
table["get_delay"] = FunctionEntry.new InlineGetDelayFunction.new
return table
end
private def create_code(tree : TreeFunction, table)
generator = CodeGenerator.new table, tree
@logger.debug("Generating code for #{tree.name}")
return generator.generate!
end
private def create_code(tree : BuiltinFunction, table)
instructions = [] of Instruction
tree.generate!(instructions)
return instructions
end
private def create_code(trees : Array(TreeFunction), table)
code = {} of String => Array(Instruction)
trees.each do |tree|
code[tree.name] = create_code(tree, table)
end
return code
end
private def run_tree
trees = create_trees(@config.file)
trees.each do |it|
STDOUT << it
end
end
private def run_intermediate
trees = create_trees(@config.file)
table = create_table(trees)
code = create_code(trees, table)
code.each do |name, insts|
puts "Code for #{name}:"
insts.each { |it| puts it }
puts "-----"
end
end
private def generate_binary(table, instructions, dest)
context = InstructionContext.new table, instructions.size
binary = instructions.map_with_index { |it, i| it.to_bin(context, i).to_u16 }
binary.each do |inst|
first = (inst >> 8).to_u8
dest.write_byte(first)
second = (inst & 0xff).to_u8
dest.write_byte(second)
end
end
private def collect_calls(table)
open = Set(String).new
done = Set(String).new
open << "main"
while !open.empty?
first = open.first
open.delete first
entry = table[first]?
raise "Unknown function" unless entry && entry.is_a?(FunctionEntry)
function = entry.function
next if function.is_a?(InlineFunction)
done << first
next unless function.is_a?(TreeFunction)
visitor = CallVisitor.new
function.accept(visitor)
open.concat(visitor.calls - done)
end
return done
end
private def run_binary
all_instructions = [] of Instruction
trees = create_trees(@config.file)
table = create_table(trees)
names = collect_calls(table)
names.delete "main"
main_entry = table["main"]?.as(FunctionEntry)
all_instructions.concat create_code(main_entry.function.as(TreeFunction), table)
main_entry.addr = 0
all_instructions << JumpRelativeInstruction.new 0
names.each do |name|
entry = table[name]?.as(FunctionEntry)
entry.addr = all_instructions.size
function = entry.function
raise "Trying to compile inlined function" if function.is_a?(InlineFunction)
all_instructions.concat create_code(function, table)
all_instructions << ReturnInstruction.new
end end
file = File.open("out.ch8", "w") # Creates a default symbol table using the default functions,
generate_binary(table, all_instructions, file) # as well as the functions declared by *trees*
file.close private def create_table(trees)
end table = Table.new
@logger.debug("Creating symbol table")
trees.each do |tree|
case 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
@logger.debug("Done creating symbol table")
def run table.set_function "get_key", FunctionEntry.new Builtin::InlineAwaitKeyFunction.new
case @config.mode table.set_function "set_delay", FunctionEntry.new Builtin::InlineSetDelayFunction.new
when OutputMode::Tree table.set_function "get_delay", FunctionEntry.new Builtin::InlineGetDelayFunction.new
run_tree table.set_function "set_sound", FunctionEntry.new Builtin::InlineSetSoundFunction.new
when OutputMode::Intermediate table.set_function "draw_number", FunctionEntry.new Builtin::InlineDrawNumberFunction.new
run_intermediate table.set_function "draw_sprite", FunctionEntry.new Builtin::InlineDrawSpriteFunction.new
when OutputMode::Binary table.set_function "clear", FunctionEntry.new Builtin::InlineClearFunction.new
run_binary return table
end
# Generates and optimizes intermediate representation for the given *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
generator = CodeGenerator.new table, tree
@logger.debug("Generating code for #{tree.name}")
code = generator.generate!
code << instruction
return code # optimizer.optimize(code)
end
# Generate code for a builtin function. Neither the *table* nor the *instruction*
# are used, and serve to allow function overloading.
private def create_code(function : Builtin::BuiltinFunction, table, instruction = nil)
instructions = [] of Ir::Instruction
function.generate!(instructions)
return instructions
end
# Creates a hash containing function names and their generated code.
# 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|
code[tree.name] = create_code(tree, table)
end
return code
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
trees = create_trees(@config.file)
trees.each do |it|
STDOUT << it
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
trees = create_trees(@config.file)
table = create_table(trees)
code = create_code(trees, table)
code.each do |name, insts|
puts "Code for #{name}:"
insts.each { |it| puts it }
puts "-----"
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)
binary = instructions.map_with_index { |it, i| it.to_bin(table, instructions.size, i).to_u16 }
binary.each do |inst|
first = (inst >> 8).to_u8
dest << first
second = (inst & 0xff).to_u8
dest << second
end
end
# Find all calls performed by the functions
# stored in the *table*, starting at the main function.
private def collect_calls(table)
open = Set(String).new
done = Set(String).new
open << "main"
while !open.empty?
first = open.first
open.delete first
entry = table.get_function? first
raise "Unknown function" unless entry
function = entry.function
next if function.is_a?(Builtin::InlineFunction)
done << first
next unless function.is_a?(Trees::TreeFunction)
visitor = Trees::CallVisitor.new
function.accept(visitor)
open.concat(visitor.calls - done)
end
return done
end
# Runs in the binary `Ui::OutputMode`. The file is
# converted into an executable.
private def run_binary
all_instructions = [] of Ir::Instruction
trees = create_trees(@config.file)
table = create_table(trees)
names = collect_calls(table)
names.delete "main"
main_entry = table.get_function?("main").not_nil!
all_instructions.concat create_code(main_entry.function.as(Trees::TreeFunction),
table, Ir::JumpRelativeInstruction.new 0)
main_entry.addr = 0
names.each do |name|
entry = table.get_function?(name).not_nil!
entry.addr = all_instructions.size
function = entry.function
raise "Trying to compile inlined function" if function.is_a?(Builtin::InlineFunction)
all_instructions.concat create_code(function, table)
all_instructions << Ir::ReturnInstruction.new
end
sprite_bytes = [] of UInt8
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
end
# Runs the compiler.
def run
case @config.mode
when Ui::OutputMode::Tree
run_tree
when Ui::OutputMode::Intermediate
run_intermediate
when Ui::OutputMode::Binary
run_binary
end
end end
end end
end end

95
src/chalk/config.cr Normal file
View File

@@ -0,0 +1,95 @@
module Chalk
module Ui
# The mode in which the compiler operates.
# Defines what actions are and aren't performed.
enum OutputMode
# The text is only parsed, and the result is printed to the screen.
Tree,
# The text is parsed and converted to intermediate representation.
# The intermediate representation is then printed to the screen.
Intermediate,
# The text is converted into a full CHIP-8 executable.
Binary
end
# A configuration class created from the command-line parameters.
class Config
# Gets the file to be compiled.
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 = "",
@mode = OutputMode::Tree,
@loglevel = Logger::Severity::DEBUG,
@output : String = "out.ch8")
end
# Reads a configuration from the command line options.
def self.parse!
config = self.new
OptionParser.parse! do |parser|
parser.banner = "Usage: chalk [arguments]"
parser.on("-m", "--mode=MODE", "Set the mode of the compiler.") do |mode|
hash = {
"tree" => OutputMode::Tree,
"t" => OutputMode::Tree,
"intermediate" => OutputMode::Intermediate,
"i" => OutputMode::Intermediate,
"binary" => OutputMode::Binary,
"b" => OutputMode::Binary
}
puts "Invalid mode type. Using default." if !hash.has_key?(mode)
config.mode = hash[mode]? || OutputMode::Tree
end
parser.on("-f", "--file=FILE", "Set the input file to compile.") do |file|
config.file = file
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 }
end
return config
end
# Validates the options provided, returning true if
# they are valid and false otherwise.
def validate!
if file == ""
puts "No source file specified."
return false
elsif !File.exists? file
puts "Unable to open source file."
return false
end
return true
end
end
end
end

View File

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

View File

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

View File

@@ -1,13 +1,16 @@
module Chalk module Chalk
class CallVisitor < Visitor module Trees
property calls : Set(String) # Visitor that finds all function calls in a function.
class CallVisitor < Visitor
property calls : Set(String)
def initialize def initialize
@calls = Set(String).new @calls = Set(String).new
end end
def visit(t : TreeCall) def visit(t : TreeCall)
@calls << t.name @calls << t.name
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 class InlineAwaitKeyFunction < InlineFunction
end def generate!(emitter, params, table, target, free)
emitter.instructions << Ir::AwaitKeyInstruction.new target
def generate!(emitter, params, table, target, free) end
if !params[2].is_a?(TreeLit) def type
raise "Third parameter must be a constant." return Compiler::FunctionType.new(([] of Compiler::Type), Compiler::Type::U8)
end 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
def initialize
@param_count = 0
end end
def generate!(emitter, params, table, target, free) # Inline function to set the delay timer.
emitter.instructions << AwaitKeyInstruction.new target class InlineSetDelayFunction < InlineFunction
end def generate!(emitter, params, table, target, free)
end emitter.generate! params[0], table, free, free + 1
emitter.instructions << Ir::SetDelayTimerInstruction.new free
end
class InlineGetFontFunction < InlineFunction def type
def initialize return Compiler::FunctionType.new([Compiler::Type::U8], Compiler::Type::U0)
@param_count = 1 end
end end
def generate!(emitter, params, table, target, free) # Inline function to get the delay timer.
emitter.generate! params[0], table, free, free + 1 class InlineGetDelayFunction < InlineFunction
emitter.instructions << GetFontInstruction.new free def generate!(emitter, params, table, target, free)
end emitter.instructions << Ir::GetDelayTimerInstruction.new target
end end
def type
class InlineSetDelayFunction < InlineFunction return Compiler::FunctionType.new(([] of Compiler::Type), Compiler::Type::U8)
def initialize end
@param_count = 1
end end
def generate!(emitter, params, table, target, free) # Function to set the sound timer.
emitter.generate! params[0], table, free, free + 1 class InlineSetSoundFunction < InlineFunction
emitter.instructions << SetDelayTimerInstruction.new free def generate!(emitter, params, table, target, free)
end emitter.generate! params[0], table, free, free + 1
end emitter.instructions << Ir::SetSoundTimerInstruction.new free
end
class InlineGetDelayFunction < InlineFunction def type
def initialize return Compiler::FunctionType.new([Compiler::Type::U8], Compiler::Type::U0)
@param_count = 0 end
end end
def generate!(emitter, params, table, target, free) # Function to draw numbers.
emitter.instructions << GetDelayTimerInstruction.new target 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,373 +1,442 @@
require "./lexer.cr" require "./lexer.cr"
module Chalk module Chalk
class Instruction module Ir
def to_bin(i, index) # Base instruction class.
return 0 class Instruction
end # Converts the instruction to binary, using
end # A table for symbol lookups, the stack position,
# and the inex of the instruction.
class InstructionContext def to_bin(table, stack, index)
property table : Table return 0
property stack : Int32
def initialize(@table, @stack)
end
end
class LoadInstruction < Instruction
property register : Int32
property value : Int32
def initialize(@register, @value)
end
def to_s(io)
io << "load R"
@register.to_s(16, io)
io << " " << @value
end
def to_bin(i, index)
0x6000 | (@register << 8) | @value
end
end
class LoadRegInstruction < Instruction
property into : Int32
property from : Int32
def initialize(@into, @from)
end
def to_s(io)
io << "loadr R"
@into.to_s(16, io)
io << " R"
@from.to_s(16, io)
end
def to_bin(i, index)
0x8000 | (@into << 8) | (@from << 4)
end
end
class OpInstruction < Instruction
property op : TokenType
property into : Int32
property value : Int32
def initialize(@op, @into, @value)
end
def to_s(io)
io << "op " << op << " R"
@into.to_s(16, io)
io << " " << @value
end
def to_bin(i, index)
case op
when TokenType::OpAdd
return 0x7000 | (@into << 8) | @value
else
raise "Invalid instruction"
end end
end end
end
class OpRegInstruction < Instruction # Instruction to clear the screen
property op : TokenType class ClearInstruction < Instruction
property into : Int32 def to_s(io)
property from : Int32 io << "clear"
def initialize(@op, @into, @from)
end
def to_s(io)
io << "opr " << op << " R"
@into.to_s(16, io)
io << " R"
@from.to_s(16, io)
end
def to_bin(i, index)
code = 0
case op
when TokenType::OpAdd
code = 4
when TokenType::OpSub
code = 5
when TokenType::OpOr
code = 1
when TokenType::OpAnd
code = 2
when TokenType::OpXor
code = 3
else
raise "Invalid instruction"
end end
return 0x8000 | (@into << 8) | (@from << 4) | code
end
end
class StoreInstruction < Instruction def to_bin(table, stack, index)
property up_to : Int32 return 0x00e0
end
def initialize(@up_to)
end end
def to_s(io) # Instruction to assign a random value to a register.
io << "store R" class RandomInstruction < Instruction
@up_to.to_s(16, io) 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 end
def to_bin(i, index) # Instruction to load a value into a register.
return 0xf055 | (@up_to << 8) class LoadInstruction < Instruction
end def initialize(@register : Int32, @value : Int32)
end end
class RestoreInstruction < Instruction def to_s(io)
property up_to : Int32 io << "load R"
@register.to_s(16, io)
io << " " << @value
end
def initialize(@up_to) def to_bin(table, stack, index)
0x6000 | (@register << 8) | @value
end
end end
def to_s(io) # Instruction to load a register into another register.
io << "restore R" class LoadRegInstruction < Instruction
@up_to.to_s(16, io) # Gets the register being written to.
getter into
# Gets the register being used as right-hand operand.
getter from
def initialize(@into : Int32, @from : Int32)
end
def to_s(io)
io << "loadr R"
@into.to_s(16, io)
io << " R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
0x8000 | (@into << 8) | (@from << 4)
end
end end
def to_bin(i, index) # Instruction to perform an operation on a register and a value,
return 0xf065 | (@up_to << 8) # storing the output back into the register.
end class OpInstruction < Instruction
end def initialize(@op : Compiler::TokenType, @into : Int32, @value : Int32)
end
class ReturnInstruction < Instruction def to_s(io)
def initialize io << "op " << @op << " R"
@into.to_s(16, io)
io << " " << @value
end
def to_bin(table, stack, index)
case @op
when Compiler::TokenType::OpAdd
return 0x7000 | (@into << 8) | @value
else
raise "Invalid instruction"
end
end
end end
def to_s(io) # Instruction to perform an operation on a register and another register,
io << "return" # storing the output back into left hand register.
class OpRegInstruction < Instruction
def initialize(@op : Compiler::TokenType, @into : Int32, @from : Int32)
end
def to_s(io)
io << "opr " << @op << " R"
@into.to_s(16, io)
io << " R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
code = 0
case @op
when Compiler::TokenType::OpAdd
code = 4
when Compiler::TokenType::OpSub
code = 5
when Compiler::TokenType::OpOr
code = 1
when Compiler::TokenType::OpAnd
code = 2
when Compiler::TokenType::OpXor
code = 3
else
raise "Invalid instruction"
end
return 0x8000 | (@into << 8) | (@from << 4) | code
end
end end
def to_bin(i, index) # Instruction to write registers to memory at address I.
return 0x00ee # The *up_to* parameter specifies the highest register
end # that should be stored.
end class StoreInstruction < Instruction
def initialize(@up_to : Int32)
end
class JumpRelativeInstruction < Instruction def to_s(io)
property offset : Int32 io << "store R"
@up_to.to_s(16, io)
end
def initialize(@offset) def to_bin(table, stack, index)
return 0xf055 | (@up_to << 8)
end
end end
def to_s(io) # Instruction to read registers from memory at address I.
io << "jr " << @offset # The *up_to* parameter specifies the highest register
# that should be read into.
class RestoreInstruction < Instruction
def initialize(@up_to : Int32)
end
def to_s(io)
io << "restore R"
@up_to.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf065 | (@up_to << 8)
end
end end
def to_bin(i, index) # Instruction to return from a call.
return 0x1000 | ((@offset + index) * 2 + 0x200) class ReturnInstruction < Instruction
end def initialize
end end
class SkipEqInstruction < Instruction def to_s(io)
property left : Int32 io << "return"
property right : Int32 end
def initialize(@left, @right) def to_bin(table, stack, index)
return 0x00ee
end
end end
def to_s(io) # Instruction to jump relative to its own position.
io << "seq R" class JumpRelativeInstruction < Instruction
@left.to_s(16, io) # Gets the offset of this instruction.
io << " " << right getter offset
# Sets the offset of this instruction
setter offset
def initialize(@offset : Int32)
end
def to_s(io)
io << "jr " << @offset
end
def to_bin(table, stack, index)
return 0x1000 | ((@offset + index) * 2 + 0x200)
end
end end
def to_bin(i, index) # Instruction to skip the next instruction if
return 0x3000 | (@left << 8) | @right # the left-hand register is equal to the right-hand value.
end class SkipEqInstruction < Instruction
end def initialize(@left : Int32, @right : Int32)
end
class SkipNeInstruction < Instruction def to_s(io)
property left : Int32 io << "seq R"
property right : Int32 @left.to_s(16, io)
io << " " << @right
end
def initialize(@left, @right) def to_bin(table, stack, index)
return 0x3000 | (@left << 8) | @right
end
end end
def to_s(io) # Instruction to skip the next instruction if
io << "sne R" # the left-hand register is not equal to the right-hand value.
@left.to_s(16, io) class SkipNeInstruction < Instruction
io << " " << right def initialize(@left : Int32, @right : Int32)
end
def to_s(io)
io << "sne R"
@left.to_s(16, io)
io << " " << @right
end
def to_bin(table, stack, index)
return 0x4000 | (@left << 8) | @right
end
end end
def to_bin(i, index) # Instruction to skip the next instruction if
return 0x4000 | (@left << 8) | @right # the left-hand register is equal to the right-hand register.
end class SkipRegEqInstruction < Instruction
end def initialize(@left : Int32, @right : Int32)
end
class SkipRegEqInstruction < Instruction def to_s(io)
property left : Int32 io << "seqr R"
property right : Int32 @left.to_s(16, io)
io << " R"
@right.to_s(16, io)
end
def initialize(@left, @right) def to_bin(table, stack, index)
return 0x5000 | (@left << 8) | (@right << 4)
end
end end
def to_s(io) # Instruction to skip the next instruction if
io << "seqr R" # the left-hand register is not equal to the right-hand register.
@left.to_s(16, io) class SkipRegNeInstruction < Instruction
io << " R" def initialize(@left : Int32, @right : Int32)
@right.to_s(16, io) end
def to_s(io)
io << "sner R"
@left.to_s(16, io)
io << " R"
@right.to_s(16, io)
end
def to_bin(table, stack, index)
return 0x9000 | (@left << 8) | (@right << 4)
end
end end
def to_bin(i, index) # Instruction to call a function by name.
return 0x5000 | (@left << 8) | (@right << 4) class CallInstruction < Instruction
end # Gets the name of the function being called.
end getter name
class SkipRegNeInstruction < Instruction def initialize(@name : String)
property left : Int32 end
property right : Int32
def initialize(@left, @right) def to_s(io)
io << "call " << @name
end
def to_bin(table, stack, index)
return 0x2000 | (table.get_function?(name).as(Compiler::FunctionEntry).addr * 2 + 0x200)
end
end end
def to_s(io) # Instruction to set I to the base position of the stack.
io << "sner R" class SetIStackInstruction < Instruction
@left.to_s(16, io) def to_s(io)
io << " R" io << "setis"
@right.to_s(16, io) end
def to_bin(table, stack, index)
return 0xa000 | (stack * 2 + 0x200)
end
end end
def to_bin(i, index) class SetISpriteInstruction < Instruction
return 0x9000 | (@left << 8) | (@right << 4) getter name
end
end
class CallInstruction < Instruction def initialize(@name : String)
property name : String end
def initialize(@name) 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 end
def to_s(io) # Instruction to add a register to I.
io << "call " << @name class AddIRegInstruction < Instruction
def initialize(@reg : Int32)
end
def to_s(io)
io << "addi R"
@reg.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf000 | (@reg << 8) | 0x1e
end
end end
def to_bin(i, index) # Instruction to draw on screen.
return 0x2000 | (i.table[name]?.as(FunctionEntry).addr * 2 + 0x200) # The x and y coordinates specify the position of the sprite,
end # and the height gives the height of the sprite.
end class DrawInstruction < Instruction
def initialize(@x : Int32, @y : Int32, @height : Int32)
end
class SetIStackInstruction < Instruction def to_s(io)
def to_s(io) io << "draw R"
io << "setis" @x.to_s(16, io)
io << " R"
@y.to_s(16, io)
io << " " << @height
end
def to_bin(table, stack, index)
return 0xd000 | (@x << 8) | (@y << 4) | @height
end
end end
def to_bin(i, index) # Instruction to await a key press and store it into a register.
return 0xa000 | (i.stack * 2 + 0x200) class AwaitKeyInstruction < Instruction
end def initialize(@into : Int32)
end end
class AddIRegInstruction < Instruction def to_s(io)
property reg : Int32 io << "getk R"
@into.to_s(16, io)
end
def initialize(@reg) def to_bin(table, stack, index)
return 0xf00a | (@into << 8)
end
end end
def to_s(io) # Instruction to set I to the font given by the value
io << "addi R" # of a register.
reg.to_s(16, io) class GetFontInstruction < Instruction
def initialize(@from : Int32)
end
def to_s(io)
io << "font R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf029 | (@from << 8)
end
end end
def to_bin(i, index) # Instruction to set the delay timer to the value
return 0xf000 | (@reg << 8) | 0x1e # of the given register.
end class SetDelayTimerInstruction < Instruction
end def initialize(@from : Int32)
end
class DrawInstruction < Instruction def to_s(io)
property x : Int32 io << "set_delay R"
property y : Int32 @from.to_s(16, io)
property height : Int32 end
def initialize(@x, @y, @height) def to_bin(table, stack, index)
return 0xf015 | (@from << 8)
end
end end
def to_s(io) # Instruction to get the delay timer, and store
io << "draw R" # the value into the given register.
x.to_s(16, io) class GetDelayTimerInstruction < Instruction
io << " R" def initialize(@into : Int32)
y.to_s(16, io) end
io << " " << height
def to_s(io)
io << "get_delay R"
@into.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf007 | (@into << 8)
end
end end
def to_bin(i, index) # Instruction to set the sound timer to a value.
return 0xd000 | (@x << 8) | (@y << 4) | height class SetSoundTimerInstruction < Instruction
end def initialize(@from : Int32)
end end
class AwaitKeyInstruction < Instruction def to_s(io)
property into : Int32 io << "set_sound R"
@from.to_s(16, io)
end
def initialize(@into) def to_bin(table, stack, index)
return 0xf018 | (@from << 8)
end
end end
def to_s(io) class BCDInstruction < Instruction
io << "getk R" def initialize(@from : Int32)
@into.to_s(16, io) end
end
def to_bin(i, index) def to_s(io)
return 0xf00a | (@into << 8) io << "bcd R"
end @from.to_s(16, io)
end end
class GetFontInstruction < Instruction def to_bin(table, stack, index)
property from : Int32 return 0xf033 | (@from << 8)
end
def initialize(@from)
end
def to_s(io)
io << "font R"
@from.to_s(16, io)
end
def to_bin(i, index)
return 0xf029 | (@from << 8)
end
end
class SetDelayTimerInstruction < Instruction
property from : Int32
def initialize(@from)
end
def to_s(io)
io << "set_delay R"
@from.to_s(16, io)
end
def to_bin(i, index)
return 0xf015 | (@from << 8)
end
end
class GetDelayTimerInstruction < Instruction
property into : Int32
def initialize(@into)
end
def to_s(io)
io << "get_delay R"
@into.to_s(16, io)
end
def to_bin(i, index)
return 0xf007 | (@into << 8)
end end
end end
end end

View File

@@ -1,82 +1,98 @@
require "lex" require "lex"
module Chalk module Chalk
enum TokenType module Compiler
Any, # The type of a token that can be lexed.
Str, enum TokenType
Id, Any,
LitDec, Str,
LitBin, Id,
LitHex, SpriteRow,
OpAdd LitDec,
OpSub LitBin,
OpMul LitHex,
OpDiv OpAdd
OpOr OpSub
OpAnd OpMul
OpXor OpDiv
KwSprite OpOr
KwInline OpAnd
KwFun OpXor
KwU0 KwSprite
KwU8 KwInline
KwU12 KwFun
KwVar KwU0
KwIf KwU4
KwElse KwU8
KwWhile KwU12
KwReturn KwVar
end KwIf
KwElse
class Token KwWhile
def initialize(@string : String, @type : TokenType) KwReturn
end end
getter string : String # A class that stores the string it matched and its token type.
getter type : TokenType class Token
end def initialize(@string : String, @type : TokenType)
end
class Lexer # Gets the string this token represents.
def initialize getter string : String
@lexer = Lex::Lexer.new # Gets the type of this token.
@lexer.add_pattern(".", TokenType::Any.value) getter type : TokenType
@lexer.add_pattern("\"(\\\\\"|[^\"])*\"",
TokenType::Str.value)
@lexer.add_pattern("[a-zA-Z_][a-zA-Z_0-9]*",
TokenType::Id.value)
@lexer.add_pattern("[0-9]+",
TokenType::LitDec.value)
@lexer.add_pattern("0b[0-1]+",
TokenType::LitBin.value)
@lexer.add_pattern("0x[0-9a-fA-F]+",
TokenType::LitHex.value)
@lexer.add_pattern("\\+", TokenType::OpAdd.value)
@lexer.add_pattern("-", TokenType::OpSub.value)
@lexer.add_pattern("\\*", TokenType::OpMul.value)
@lexer.add_pattern("/", TokenType::OpDiv.value)
@lexer.add_pattern("&", TokenType::OpAdd.value)
@lexer.add_pattern("\\|", TokenType::OpOr.value)
@lexer.add_pattern("^", TokenType::OpXor.value)
@lexer.add_pattern("sprite", TokenType::KwSprite.value)
@lexer.add_pattern("inline", TokenType::KwInline.value)
@lexer.add_pattern("fun", TokenType::KwFun.value)
@lexer.add_pattern("u0", TokenType::KwU0.value)
@lexer.add_pattern("u8", TokenType::KwU8.value)
@lexer.add_pattern("u12", TokenType::KwU12.value)
@lexer.add_pattern("var", TokenType::KwVar.value)
@lexer.add_pattern("if", TokenType::KwIf.value)
@lexer.add_pattern("else", TokenType::KwElse.value)
@lexer.add_pattern("while", TokenType::KwWhile.value)
@lexer.add_pattern("return", TokenType::KwReturn.value)
end end
def lex(string) # Creates a new Lexer with default token values.
return @lexer.lex(string) # The lexer is backed by liblex. When a string is
.select { |t| !t[0][0].whitespace? } # matched by several tokens, the longest match is chosen
.map do |tuple| # first, followed by the match with the highest enum value.
string, id = tuple class Lexer
Token.new(string, TokenType.new(id)) def initialize
end @lexer = Lex::Lexer.new
@lexer.add_pattern(".", TokenType::Any.value)
@lexer.add_pattern("\"(\\\\\"|[^\"])*\"",
TokenType::Str.value)
@lexer.add_pattern("[a-zA-Z_][a-zA-Z_0-9]*",
TokenType::Id.value)
@lexer.add_pattern("`[ x]*`",
TokenType::SpriteRow.value)
@lexer.add_pattern("[0-9]+",
TokenType::LitDec.value)
@lexer.add_pattern("0b[0-1]+",
TokenType::LitBin.value)
@lexer.add_pattern("0x[0-9a-fA-F]+",
TokenType::LitHex.value)
@lexer.add_pattern("\\+", TokenType::OpAdd.value)
@lexer.add_pattern("-", TokenType::OpSub.value)
@lexer.add_pattern("\\*", TokenType::OpMul.value)
@lexer.add_pattern("/", TokenType::OpDiv.value)
@lexer.add_pattern("&", TokenType::OpAdd.value)
@lexer.add_pattern("\\|", TokenType::OpOr.value)
@lexer.add_pattern("^", TokenType::OpXor.value)
@lexer.add_pattern("sprite", TokenType::KwSprite.value)
@lexer.add_pattern("inline", TokenType::KwInline.value)
@lexer.add_pattern("fun", TokenType::KwFun.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("u12", TokenType::KwU12.value)
@lexer.add_pattern("var", TokenType::KwVar.value)
@lexer.add_pattern("if", TokenType::KwIf.value)
@lexer.add_pattern("else", TokenType::KwElse.value)
@lexer.add_pattern("while", TokenType::KwWhile.value)
@lexer.add_pattern("return", TokenType::KwReturn.value)
end
# Converts a string into tokens.
def lex(string)
return @lexer.lex(string)
.select { |t| !t[0][0].whitespace? }
.map do |tuple|
string, id = tuple
Token.new(string, TokenType.new(id))
end
end
end end
end end
end end

View File

@@ -1,42 +1,69 @@
module Chalk module Chalk
class Optimizer module Compiler
def initialize(instructions : Array(Instruction)) # Class to optimize instructions.
@instructions = instructions.dup class Optimizer
end # 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
return false
end end
return false
end
private def optimize!(range) # Optimizes *instructions* in the basic block given by the *range*,
offset = 0 # storing addresses of instructions to be deleted into *deletions*,
range.each do |index| # and the number of deleted instructions so far into *deletions_at*
if check_dead(@instructions[index + offset]) private def optimize!(instructions, range, deletions, deletions_at)
@instructions.delete_at(index + offset) range.each do |index|
offset -= 1 if check_dead(instructions[index])
deletions << index
end
deletions_at[index] = deletions.size
end end
end end
return offset
end
private def optimize! # Optimizes the given list of instructions.
block_boundaries = [@instructions.size] # The basic blocks are inferred from the various
@instructions.each_with_index do |inst, i| # jumps and skips.
if inst.is_a?(JumpRelativeInstruction) def optimize(instructions)
block_boundaries << (inst.offset + i) instructions = instructions.dup
block_boundaries = [instructions.size]
instructions.each_with_index do |inst, i|
if inst.is_a?(Ir::JumpRelativeInstruction)
block_boundaries << (i + 1)
block_boundaries << (inst.offset + i)
end
if inst.is_a?(Ir::SkipNeInstruction | Ir::SkipEqInstruction |
Ir::SkipRegEqInstruction | Ir::SkipRegNeInstruction)
block_boundaries << (i + 1)
end
end end
end block_boundaries.uniq!.sort!
block_boundaries.sort!
previous = 0 previous = 0
offset = 0 deletions = [] of Int32
block_boundaries.each do |boundary| deletions_at = {} of Int32 => Int32
range = (previous + offset)...(boundary + offset) block_boundaries.each do |boundary|
offset += optimize!(range) range = previous...boundary
previous = boundary optimize!(instructions, range, deletions, deletions_at)
previous = boundary
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
end end
end end
end end

View File

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

View File

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

View File

@@ -1,148 +1,171 @@
require "./builder.cr"
module Chalk module Chalk
abstract class BasicParser(T) module ParserCombinators
abstract def parse?(tokens : Array(Token), # Abstract class for a parser function,
index : Int64) : Tuple(T, Int64)? # as used in parser combinators. This is basically
# a building block of parsing.
abstract class BasicParser(T)
# Attempts to parse the given *tokens*, starting at the given *index*.
abstract def parse?(tokens : Array(Compiler::Token),
index : Int64) : Tuple(T, Int64)?
def parse(tokens, index) # Attempts to parse the given tokens like `#parse?`, but throws
return parse?(tokens, index).not_nil! # on error.
end def parse(tokens, index)
return parse?(tokens, index).not_nil!
def transform(&transform : T -> R) forall R
return TransformParser.new(self, &transform).as(BasicParser(R))
end
def then(other : BasicParser(R)) : BasicParser(Array(T | R)) forall R
return NextParser.new(self, other).as(BasicParser(Array(T | R)))
end
end
class TypeParser < BasicParser(Token)
def initialize(@type : TokenType)
end
def parse?(tokens, index)
return nil unless index < tokens.size
return nil unless tokens[index].type == @type
return {tokens[index], index + 1}
end
end
class CharParser < BasicParser(Token)
def initialize(@char : Char)
end
def parse?(tokens, index)
return nil unless index < tokens.size
return nil unless (tokens[index].type == TokenType::Any) &&
tokens[index].string[0] == @char
return {tokens[index], index + 1}
end
end
class TransformParser(T, R) < BasicParser(R)
def initialize(@parser : BasicParser(T), &@block : T -> R)
end
def parse?(tokens, index)
if parsed = @parser.parse?(tokens, index)
return {@block.call(parsed[0]), parsed[1]}
end end
return nil
end
end
class OptionalParser(T) < BasicParser(T?) # Applies the given transformation to this parser,
def initialize(@parser : BasicParser(T)) # creating a new parser.
end def transform(&transform : T -> R) forall R
return TransformParser.new(self, &transform).as(BasicParser(R))
def parse?(tokens, index)
if parsed = @parser.parse?(tokens, index)
return {parsed[0], parsed[1]}
end end
return {nil, index}
end
end
class EitherParser(T) < BasicParser(T) # Creates a sequence with the given parser,
def initialize(@parsers : Array(BasicParser(T))) # creating a new parser.
def then(other : BasicParser(R)) : BasicParser(Array(T | R)) forall R
return NextParser.new(self, other).as(BasicParser(Array(T | R)))
end
end end
def parse?(tokens, index) # Parser that expects a specific token type.
@parsers.each do |parser| class TypeParser < BasicParser(Compiler::Token)
if parsed = parser.parse?(tokens, index) def initialize(@type : Compiler::TokenType)
return parsed end
def parse?(tokens, index)
return nil unless index < tokens.size
return nil unless tokens[index].type == @type
return {tokens[index], index + 1}
end
end
# Parser that expects a specific character.
class CharParser < BasicParser(Compiler::Token)
def initialize(@char : Char)
end
def parse?(tokens, index)
return nil unless index < tokens.size
return nil unless (tokens[index].type == Compiler::TokenType::Any) &&
tokens[index].string[0] == @char
return {tokens[index], index + 1}
end
end
# Parser that applies a transformation to the output
# of its child parser.
class TransformParser(T, R) < BasicParser(R)
def initialize(@parser : BasicParser(T), &@block : T -> R)
end
def parse?(tokens, index)
if parsed = @parser.parse?(tokens, index)
return {@block.call(parsed[0]), parsed[1]}
end end
return nil
end end
return nil
end
end
class ManyParser(T) < BasicParser(Array(T))
def initialize(@parser : BasicParser(T))
end end
def parse?(tokens, index) # Parser that attempts to use its child parser,
many = [] of T # and successfully returns nil if the child parser fails.
while parsed = @parser.parse?(tokens, index) class OptionalParser(T) < BasicParser(T?)
item, index = parsed def initialize(@parser : BasicParser(T))
many << item
end
return {many, index}
end
end
class DelimitedParser(T, R) < BasicParser(Array(T))
def initialize(@parser : BasicParser(T), @delimiter : BasicParser(R))
end
def parse?(tokens, index)
array = [] of T
first = @parser.parse?(tokens, index)
return {array, index} unless first
first_value, index = first
array << first_value
while delimiter = @delimiter.parse?(tokens, index)
_, new_index = delimiter
new = @parser.parse?(tokens, new_index)
break unless new
new_value, index = new
array << new_value
end end
return {array, index} def parse?(tokens, index)
end if parsed = @parser.parse?(tokens, index)
end return {parsed[0], parsed[1]}
end
class NextParser(T, R) < BasicParser(Array(T | R)) return {nil, index}
def initialize(@first : BasicParser(T), @second : BasicParser(R)) end
end end
def parse?(tokens, index) # Parser that tries all of its children until one succeeds.
first = @first.parse?(tokens, index) class EitherParser(T) < BasicParser(T)
return nil unless first def initialize(@parsers : Array(BasicParser(T)))
first_value, index = first end
second = @second.parse?(tokens, index) def parse?(tokens, index)
return nil unless second @parsers.each do |parser|
second_value, index = second if parsed = parser.parse?(tokens, index)
return parsed
array = Array(T | R).new end
array << first_value << second_value end
return {array, index} return nil
end end
end
class PlaceholderParser(T) < BasicParser(T)
property parser : BasicParser(T)?
def initialize
@parser = nil
end end
def parse?(tokens, index) # Parser that parses at least one of a given type.
@parser.try &.parse?(tokens, index) class ManyParser(T) < BasicParser(Array(T))
def initialize(@parser : BasicParser(T))
end
def parse?(tokens, index)
many = [] of T
while parsed = @parser.parse?(tokens, index)
item, index = parsed
many << item
end
return {many, index}
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))
def initialize(@parser : BasicParser(T), @delimiter : BasicParser(R))
end
def parse?(tokens, index)
array = [] of T
first = @parser.parse?(tokens, index)
return {array, index} unless first
first_value, index = first
array << first_value
while delimiter = @delimiter.parse?(tokens, index)
_, new_index = delimiter
new = @parser.parse?(tokens, new_index)
break unless new
new_value, index = new
array << new_value
end
return {array, index}
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))
def initialize(@first : BasicParser(T), @second : BasicParser(R))
end
def parse?(tokens, index)
first = @first.parse?(tokens, index)
return nil unless first
first_value, index = first
second = @second.parse?(tokens, index)
return nil unless second
second_value, index = second
array = Array(T | R).new
array << first_value << second_value
return {array, index}
end
end
# Parser used to declare recursive grammars.
class PlaceholderParser(T) < BasicParser(T)
property parser : BasicParser(T)?
def initialize
@parser = nil
end
def parse?(tokens, index)
@parser.try &.parse?(tokens, index)
end
end end
end end
end end

View File

@@ -1,53 +1,55 @@
require "./tree.cr" require "./tree.cr"
module Chalk module Chalk
class PrintVisitor < Visitor module Trees
def initialize(@stream : IO) # Visitor that prints a `Tree`.
@indent = 0 class PrintVisitor < Visitor
end def initialize(@stream : IO)
@indent = 0
def print_indent
@indent.times do
@stream << " "
end end
end
def visit(id : TreeId) private def print_indent
print_indent @indent.times do
@stream << id.id << "\n" @stream << " "
end end
def visit(lit : TreeLit)
print_indent
@stream << lit.lit << "\n"
end
def visit(op : TreeOp)
print_indent
@stream << "[op] "
@stream << op.op << "\n"
@indent += 1
end
def finish(op : TreeOp)
@indent -= 1
end
def visit(function : TreeFunction)
print_indent
@stream << "[function] " << function.name << "( "
function.params.each do |param|
@stream << param << " "
end end
@stream << ")" << "\n"
@indent += 1
end
def finish(function : TreeFunction) def visit(id : TreeId)
@indent -= 1 print_indent
end @stream << id.id << "\n"
end
macro forward(text, type) def visit(lit : TreeLit)
print_indent
@stream << lit.lit << "\n"
end
def visit(op : TreeOp)
print_indent
@stream << "[op] "
@stream << op.op << "\n"
@indent += 1
end
def finish(op : TreeOp)
@indent -= 1
end
def visit(function : TreeFunction)
print_indent
@stream << "[function] " << function.name << "( "
function.params.each do |param|
@stream << param << " "
end
@stream << ")" << "\n"
@indent += 1
end
def finish(function : TreeFunction)
@indent -= 1
end
macro forward(text, type)
def visit(tree : {{type}}) def visit(tree : {{type}})
print_indent print_indent
@stream << {{text}} << "\n" @stream << {{text}} << "\n"
@@ -59,18 +61,19 @@ module Chalk
end end
end end
forward("[call]", TreeCall) forward("[call]", TreeCall)
forward("[block]", TreeBlock) forward("[block]", TreeBlock)
forward("[var]", TreeVar) forward("[var]", TreeVar)
forward("[assign]", TreeAssign) forward("[assign]", TreeAssign)
forward("[if]", TreeIf) forward("[if]", TreeIf)
forward("[while]", TreeWhile) forward("[while]", TreeWhile)
forward("[return]", TreeReturn) forward("[return]", TreeReturn)
end end
class Tree class Tree
def to_s(io) def to_s(io)
accept(PrintVisitor.new io) accept(PrintVisitor.new io)
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,51 +1,81 @@
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 end
def initialize(@function, @addr = -1) def to_s(io)
end io << "[function]"
def to_s(io)
io << "[function]"
end
end
class VarEntry < Entry
property register : Int32
def initialize(@register)
end
def to_s(io)
io << "[variable] " << "(R" << @register.to_s(16) << ")"
end
end
class Table
property parent : Table?
def initialize(@parent = nil)
@data = {} of String => Entry
end
def []?(key)
if entry = @data[key]?
return entry
end end
return @parent.try &.[key]?
end end
def []=(key, entry) # An entry that represents a variable in the symbol table.
@data[key] = entry class VarEntry
# Gets the register occupied by the variable
# in this entry.
getter register
def initialize(@register : Int32)
end
def to_s(io)
io << "[variable] " << "(R" << @register.to_s(16) << ")"
end
end end
def to_s(io) class SpriteEntry
@parent.try &.to_s(io) property sprite : Sprite
io << @data.map { |k, v| k + ": " + v.to_s }.join("\n") property addr : Int32
def initialize(@sprite, @addr = -1)
end
end
# A symbol table.
class 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 : Table? = nil)
@functions = {} of String => FunctionEntry
@vars = {} of String => VarEntry
@sprites = {} of String => SpriteEntry
end
macro table_functions(name)
def get_{{name}}?(key)
@{{name}}s[key]? || @parent.try &.get_{{name}}?(key)
end
def set_{{name}}(key, value)
@{{name}}s[key] = value
end
end
table_functions function
table_functions var
table_functions sprite
def to_s(io)
@parent.try &.to_s(io)
end
end end
end end
end end

View File

@@ -1,228 +1,308 @@
module Chalk module Chalk
class Visitor module Trees
def visit(tree) # A class used to visit nodes of a tree.
end class Visitor
def visit(tree)
def finish(tree)
end
end
class Transformer
def transform(tree)
return tree
end
end
class Tree
def accept(v)
v.visit(self)
v.finish(self)
end
def apply(t)
return t.transform(self)
end
end
class TreeId < Tree
property id : String
def initialize(@id)
end
end
class TreeLit < Tree
property lit : Int64
def initialize(@lit)
end
end
class TreeCall < Tree
property name : String
property params : Array(Tree)
def initialize(@name, @params)
end
def accept(v)
v.visit(self)
@params.each &.accept(v)
v.finish(self)
end
def apply(t)
@params.map! do |param|
param.apply(t)
end end
return t.transform(self)
end
end
class TreeOp < Tree def finish(tree)
property op : TokenType
property left : Tree
property right : Tree
def initialize(@op, @left, @right)
end
def accept(v)
v.visit(self)
@left.accept(v)
@right.accept(v)
v.finish(self)
end
def apply(t)
@left = @left.apply(t)
@right = @right.apply(t)
return t.transform(self)
end
end
class TreeBlock < Tree
property children : Array(Tree)
def initialize(@children)
end
def accept(v)
v.visit(self)
@children.each &.accept(v)
v.finish(self)
end
def apply(t)
@children.map! do |child|
child.apply(t)
end end
return t.transform(self)
end
end
class TreeFunction < Tree
property name : String
property params : Array(String)
property block : Tree
def initialize(@name, @params, @block)
end end
def param_count # A class used to transform a tree, bottom up.
return @params.size # "Modern Compiler Design" refers to this technique
# as BURS.
class Transformer
def transform(tree)
return tree
end
end end
def accept(v) class Reducer(T)
v.visit(self) def reduce(tree, children)
@block.accept(v) end
v.finish(self)
end end
def apply(t) # The base class of a tree.
@block = @block.apply(t) class Tree
return t.transform(self) def accept(v)
end v.visit(self)
end v.finish(self)
end
class TreeVar < Tree def apply(t)
property name : String return t.transform(self)
property expr : Tree end
def initialize(@name, @expr) def reduce(r : Reducer(T)) forall T
return r.reduce(self, [] of T)
end
end end
def accept(v) # A tree that represents an ID.
v.visit(self) class TreeId < Tree
@expr.accept(v) property id : String
v.finish(self)
def initialize(@id)
end
end end
def apply(t) # A tree that represents an integer literal.
@expr = @expr.apply(t) class TreeLit < Tree
return t.transform(self) property lit : Int64
end
end
class TreeAssign < Tree def initialize(@lit)
property name : String end
property expr : Tree
def initialize(@name, @expr)
end end
def accept(v) # A tree that represents a function call.
v.visit(self) class TreeCall < Tree
@expr.accept(v) property name : String
v.finish(self) property params : Array(Tree)
def initialize(@name, @params)
end
def accept(v)
v.visit(self)
@params.each &.accept(v)
v.finish(self)
end
def apply(t)
@params.map! do |param|
param.apply(t)
end
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @params.map &.reduce(r))
end
end end
def apply(t) # A tree that represents an operation on two values.
@expr = @expr.apply(t) class TreeOp < Tree
return t.transform(self) property op : Compiler::TokenType
end property left : Tree
end property right : Tree
class TreeIf < Tree def initialize(@op, @left, @right)
property condition : Tree end
property block : Tree
property otherwise : Tree?
def initialize(@condition, @block, @otherwise = nil) def accept(v)
v.visit(self)
@left.accept(v)
@right.accept(v)
v.finish(self)
end
def apply(t)
@left = @left.apply(t)
@right = @right.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [@left.reduce(r), @right.reduce(r)])
end
end end
def accept(v) # A tree that represents a block of statements.
v.visit(self) class TreeBlock < Tree
@condition.accept(v) property children : Array(Tree)
@block.accept(v)
@otherwise.try &.accept(v) def initialize(@children)
v.finish(self) end
def accept(v)
v.visit(self)
@children.each &.accept(v)
v.finish(self)
end
def apply(t)
@children.map! do |child|
child.apply(t)
end
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @children.map &.reduce(r))
end
end end
def apply(t) # A tree that represents a function declaration.
@condition = @condition.apply(t) class TreeFunction < Tree
@block = @block.apply(t) property name : String
@otherwise = @otherwise.try &.apply(t) property params : Array(String)
return t.transform(self) property block : Tree
end getter type
end
class TreeWhile < Tree def initialize(@name, @params, return_type : Compiler::Type, @block)
property condition : Tree @type = Compiler::FunctionType.new([Compiler::Type::U8] * @params.size, return_type)
property block : Tree end
def initialize(@condition, @block) def param_count
return @params.size
end
def accept(v)
v.visit(self)
@block.accept(v)
v.finish(self)
end
def apply(t)
@block = @block.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [@block.reduce(r)])
end
end end
def accept(v) # A tree that represents the declaration of
v.visit(self) # a new variable.
@condition.accept(v) class TreeVar < Tree
@block.accept(v) property name : String
v.finish(self) property expr : Tree
def initialize(@name, @expr)
end
def accept(v)
v.visit(self)
@expr.accept(v)
v.finish(self)
end
def apply(t)
@expr = @expr.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end end
def apply(t) # A tree that represents the assignment
@condition = @condition.apply(t) # to an existing variable.
@block = @block.apply(t) class TreeAssign < Tree
return t.transform(self) property name : String
end property expr : Tree
end
class TreeReturn < Tree def initialize(@name, @expr)
property rvalue : Tree end
def initialize(@rvalue) def accept(v)
v.visit(self)
@expr.accept(v)
v.finish(self)
end
def apply(t)
@expr = @expr.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end end
def accept(v) # A tree that represents an if statement.
v.visit(self) class TreeIf < Tree
@rvalue.accept(v) property condition : Tree
v.finish(self) property block : Tree
property otherwise : Tree?
def initialize(@condition, @block, @otherwise = nil)
end
def accept(v)
v.visit(self)
@condition.accept(v)
@block.accept(v)
@otherwise.try &.accept(v)
v.finish(self)
end
def apply(t)
@condition = @condition.apply(t)
@block = @block.apply(t)
@otherwise = @otherwise.try &.apply(t)
return t.transform(self)
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
def apply(t) # A tree that represents a while loop.
@rvalue = @rvalue.apply(t) class TreeWhile < Tree
return t.transform(self) property condition : Tree
property block : Tree
def initialize(@condition, @block)
end
def accept(v)
v.visit(self)
@condition.accept(v)
@block.accept(v)
v.finish(self)
end
def apply(t)
@condition = @condition.apply(t)
@block = @block.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@condition.reduce(r), @block.reduce(r)])
end
end
# A tree that represents a return statement.
class TreeReturn < Tree
property rvalue : Tree
def initialize(@rvalue)
end
def accept(v)
v.visit(self)
@rvalue.accept(v)
v.finish(self)
end
def apply(t)
@rvalue = @rvalue.apply(t)
return t.transform(self)
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 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

View File

@@ -1,51 +0,0 @@
module Chalk
enum OutputMode
Tree,
Intermediate,
Binary
end
class Config
property file : String
property mode : OutputMode
def initialize(@file = "",
@mode = OutputMode::Tree)
end
def self.parse!
config = self.new
OptionParser.parse! do |parser|
parser.banner = "Usage: chalk [arguments]"
parser.on("-m", "--mode=MODE", "Set the mode of the compiler.") do |mode|
case mode.downcase
when "tree", "t"
config.mode = OutputMode::Tree
when "intermediate", "i"
config.mode = OutputMode::Intermediate
when "binary", "b"
config.mode = OutputMode::Binary
else
puts "Invalid mode type. Using default."
end
end
parser.on("-f", "--file=FILE", "Set the input file to compile.") do |file|
config.file = file
end
parser.on("-h", "--help", "Show this message.") { puts parser }
end
return config
end
def validate!
if file == ""
puts "No source file specified."
return false
elsif !File.exists? file
puts "Unable to open source file."
return false
end
return true
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