Compare commits

..

22 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
28 changed files with 841 additions and 140 deletions

View File

@@ -1,22 +1,64 @@
# 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
TODO: Write installation instructions here
To compile chalk, simply run
```Bash
crystal build --release src/chalk.cr
```
## 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
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`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
@@ -24,4 +66,4 @@ TODO: Write development instructions here
## 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

@@ -4,15 +4,14 @@ module Chalk
# that is provided by chalk's standard library, and therefore
# has predefined output.
abstract class BuiltinFunction
# Gets the number of parameters this function has.
getter param_count : Int32
# Creates a new function with *param_count* parameters.
def initialize(@param_count)
def initialize()
end
# Uses the given `Compiler::Emitter` to output code.
abstract def generate!(codegen)
# Gets the `Compiler::FunctionType` of this function.
abstract def type
end
# A function to which a call is not generated. This function
@@ -20,11 +19,8 @@ module Chalk
# function also accepts trees rather than register numbers,
# and therefore can accept and manipulate trees.
abstract class InlineFunction
# Gets the number of parameters this function has.
getter param_count : Int32
# Creates a new function with *param_count* parameters.
def initialize(@param_count)
def initialize()
end
# Generates code like `Compiler::CodeGenerator` would.
@@ -32,6 +28,8 @@ module Chalk
# the *params* are trees that are being passed as arguments.
# See `Compiler::CodeGenerator#generate!` for what the other parameters mean.
abstract def generate!(codegen, params, table, target, free)
# Gets the `Compiler::FunctionType` of this function.
abstract def type
end
end
end

View File

@@ -8,11 +8,6 @@ module Chalk
class CodeGenerator
include Emitter
# The register into which the return value of a function is stored.
RETURN_REG = 14
# The register into which the "stack pointer" is stored.
STACK_REG = 13
# Gets the instructions currently emitted by this code generator.
getter instructions
@@ -24,7 +19,7 @@ module Chalk
@table = Table.new table
@function.params.each do |param|
@table[param] = VarEntry.new @registers
@table.set_var param, VarEntry.new @registers
@registers += 1
end
end
@@ -42,10 +37,7 @@ module Chalk
# `#generate!` call.
def generate!(tree, function : Trees::TreeFunction | Builtin::BuiltinFunction, table, target, free)
start_at = free
# Move I to stack
setis
# Get to correct stack position
addi STACK_REG
to_stack
# Store variables
store (start_at - 1) unless start_at == 0
# Increment I and stack position
@@ -67,10 +59,7 @@ module Chalk
# 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
to_stack
# Restore
restore (start_at - 1) unless start_at == 0
# Get call value into target
@@ -85,9 +74,8 @@ module Chalk
def generate!(tree, table, target, free)
case tree
when Trees::TreeId
entry = table[tree.id]?
raise "Unknown variable" unless entry &&
entry.is_a?(VarEntry)
entry = table.get_var? tree.id
raise "Unknown variable" unless entry
loadr target, entry.register
when Trees::TreeLit
load target, tree.lit
@@ -96,31 +84,26 @@ module Chalk
generate! tree.right, table, free, free + 1
opr tree.op, target, free
when Trees::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
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, free, free + 1
free += generate! child, table, THROWAWAY_REG, free
end
when Trees::TreeVar
entry = table[tree.name]?
entry = table.get_var? tree.name
if entry == nil
entry = VarEntry.new free
free += 1
table[tree.name] = entry
table.set_var tree.name, entry
end
raise "Unknown variable" unless entry.is_a?(VarEntry)
generate! tree.expr, table, entry.register, free
generate! tree.expr, table, entry.as(VarEntry).register, free
return 1
when Trees::TreeAssign
entry = table[tree.name]?
raise "Unknown variable" unless entry &&
entry.is_a?(VarEntry)
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
@@ -148,7 +131,8 @@ module Chalk
cond_jump.offset = @instructions.size - old_size + 1
after_jump.offset = before_cond - instructions.size + 1
when Trees::TreeReturn
generate! tree.rvalue, table, RETURN_REG, free
generate! tree.rvalue, table, free, free + 1
loadr RETURN_REG, free
ret
end
return 0
@@ -156,7 +140,7 @@ module Chalk
# Generates code for the function that was given to it.
def generate!
generate!(@function.block, @table, -1, @registers)
generate!(@function.block, @table, 0, @registers)
return @instructions
end
end

View File

@@ -12,7 +12,7 @@ module Chalk
def initialize(@config : Ui::Config)
@logger = Logger.new STDOUT
@logger.debug("Initialized compiler")
@logger.level = Logger::DEBUG
@logger.level = @config.loglevel
end
# Reads a file an extracts instances of
@@ -33,8 +33,9 @@ module Chalk
@logger.debug("Beginning constant folding")
folder = Trees::ConstantFolder.new
trees.map! do |tree|
next tree unless tree.is_a?(Trees::TreeFunction)
@logger.debug("Constant folding #{tree.name}")
tree.apply(folder).as(Trees::TreeFunction)
tree.apply(folder)
end
@logger.debug("Done constant folding")
return trees
@@ -48,16 +49,26 @@ module Chalk
table = Table.new
@logger.debug("Creating symbol table")
trees.each do |tree|
@logger.debug("Storing #{tree.name} in symbol table")
table[tree.name] = FunctionEntry.new 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")
table["draw"] = FunctionEntry.new Builtin::InlineDrawFunction.new
table["get_key"] = FunctionEntry.new Builtin::InlineAwaitKeyFunction.new
table["get_font"] = FunctionEntry.new Builtin::InlineGetFontFunction.new
table["set_delay"] = FunctionEntry.new Builtin::InlineSetDelayFunction.new
table["get_delay"] = FunctionEntry.new Builtin::InlineGetDelayFunction.new
table.set_function "get_key", FunctionEntry.new Builtin::InlineAwaitKeyFunction.new
table.set_function "set_delay", FunctionEntry.new Builtin::InlineSetDelayFunction.new
table.set_function "get_delay", FunctionEntry.new Builtin::InlineGetDelayFunction.new
table.set_function "set_sound", FunctionEntry.new Builtin::InlineSetSoundFunction.new
table.set_function "draw_number", FunctionEntry.new Builtin::InlineDrawNumberFunction.new
table.set_function "draw_sprite", FunctionEntry.new Builtin::InlineDrawSpriteFunction.new
table.set_function "clear", FunctionEntry.new Builtin::InlineClearFunction.new
return table
end
@@ -65,12 +76,13 @@ module Chalk
# 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 optimizer.optimize(code)
return code # optimizer.optimize(code)
end
# Generate code for a builtin function. Neither the *table* nor the *instruction*
@@ -92,6 +104,11 @@ module Chalk
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.
@@ -125,9 +142,9 @@ module Chalk
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.write_byte(first)
dest << first
second = (inst & 0xff).to_u8
dest.write_byte(second)
dest << second
end
end
@@ -142,8 +159,8 @@ module Chalk
first = open.first
open.delete first
entry = table[first]?
raise "Unknown function" unless entry && entry.is_a?(FunctionEntry)
entry = table.get_function? first
raise "Unknown function" unless entry
function = entry.function
next if function.is_a?(Builtin::InlineFunction)
done << first
@@ -165,13 +182,13 @@ module Chalk
names = collect_calls(table)
names.delete "main"
main_entry = table["main"]?.as(FunctionEntry)
main_entry = table.get_function?("main").not_nil!
all_instructions.concat create_code(main_entry.function.as(Trees::TreeFunction),
table, Ir::JumpRelativeInstruction.new 0)
main_entry.addr = 0
names.each do |name|
entry = table[name]?.as(FunctionEntry)
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)
@@ -179,8 +196,22 @@ module Chalk
all_instructions << Ir::ReturnInstruction.new
end
file = File.open("out.ch8", "w")
generate_binary(table, all_instructions, file)
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

View File

@@ -22,10 +22,20 @@ module Chalk
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)
@mode = OutputMode::Tree,
@loglevel = Logger::Severity::DEBUG,
@output : String = "out.ch8")
end
# Reads a configuration from the command line options.
@@ -34,20 +44,35 @@ module Chalk
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
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

View File

@@ -1,8 +1,21 @@
module Chalk
module Compiler
# The register into which the return value of a function is stored.
RETURN_REG = 14
# The register into which the "stack pointer" is stored.
STACK_REG = 13
# Register used for throwing away values.
THROWAWAY_REG = 12
# Module to emit instructions and store
# them into an existing array.
module Emitter
# Moves I to the next available value on the stack.
def to_stack
setis
addi STACK_REG
end
# Emits an instruction to load a *value* into a register, *into*.
def load(into, value)
inst = Ir::LoadInstruction.new into, value.to_i32

View File

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

View File

@@ -12,6 +12,33 @@ module Chalk
end
end
# Instruction to clear the screen
class ClearInstruction < Instruction
def to_s(io)
io << "clear"
end
def to_bin(table, stack, index)
return 0x00e0
end
end
# Instruction to assign a random value to a register.
class RandomInstruction < Instruction
def initialize(@register : Int32, @value : Int32)
end
def to_s(io)
io << "rand R"
@register.to_s(16, io)
io << " " << @value
end
def to_bin(table, stack, index)
return 0xc000 | (@register << 8) | (@value)
end
end
# Instruction to load a value into a register.
class LoadInstruction < Instruction
def initialize(@register : Int32, @value : Int32)
@@ -255,7 +282,7 @@ module Chalk
end
def to_bin(table, stack, index)
return 0x2000 | (table[name]?.as(Compiler::FunctionEntry).addr * 2 + 0x200)
return 0x2000 | (table.get_function?(name).as(Compiler::FunctionEntry).addr * 2 + 0x200)
end
end
@@ -270,6 +297,21 @@ module Chalk
end
end
class SetISpriteInstruction < Instruction
getter name
def initialize(@name : String)
end
def to_s(io)
io << "setispr " << @name
end
def to_bin(table, stack, index)
return 0xa000 | (table.get_sprite?(@name).not_nil!.addr + 0x200)
end
end
# Instruction to add a register to I.
class AddIRegInstruction < Instruction
def initialize(@reg : Int32)
@@ -367,5 +409,34 @@ module Chalk
return 0xf007 | (@into << 8)
end
end
# Instruction to set the sound timer to a value.
class SetSoundTimerInstruction < Instruction
def initialize(@from : Int32)
end
def to_s(io)
io << "set_sound R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf018 | (@from << 8)
end
end
class BCDInstruction < Instruction
def initialize(@from : Int32)
end
def to_s(io)
io << "bcd R"
@from.to_s(16, io)
end
def to_bin(table, stack, index)
return 0xf033 | (@from << 8)
end
end
end
end

View File

@@ -7,6 +7,7 @@ module Chalk
Any,
Str,
Id,
SpriteRow,
LitDec,
LitBin,
LitHex,
@@ -21,6 +22,7 @@ module Chalk
KwInline
KwFun
KwU0
KwU4
KwU8
KwU12
KwVar
@@ -53,6 +55,8 @@ module Chalk
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]+",
@@ -70,6 +74,7 @@ module Chalk
@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)

View File

@@ -6,9 +6,26 @@ module Chalk
class Parser
include ParserBuilder
private def create_sprite
type(Compiler::TokenType::KwSprite)
.then(type(Compiler::TokenType::Id))
.then(char('['))
.then(many(type(Compiler::TokenType::SpriteRow)))
.then(char(']'))
.transform do |array|
array = array.flatten
name = array[1].string
sprite = Compiler::Sprite.new(array[3..array.size - 2].map &.string)
Trees::TreeSprite.new(name, sprite).as(Trees::Tree)
end
end
# Creates a parser for a type.
private def create_type
either(type(Compiler::TokenType::KwU0), type(Compiler::TokenType::KwU8), type(Compiler::TokenType::KwU12))
either(type(Compiler::TokenType::KwU0),
type(Compiler::TokenType::KwU4),
type(Compiler::TokenType::KwU8),
type(Compiler::TokenType::KwU12))
end
# Creates a parser for an integer literal.
@@ -176,14 +193,20 @@ module Chalk
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
Trees::TreeFunction.new(name, params, code)
table = {
Compiler::TokenType::KwU0 => Compiler::Type::U0,
Compiler::TokenType::KwU4 => Compiler::Type::U4,
Compiler::TokenType::KwU8 => Compiler::Type::U8,
Compiler::TokenType::KwU12 => Compiler::Type::U12
}
Trees::TreeFunction.new(name, params, table[type], code).as(Trees::Tree)
end
return func
end
def initialize
_, block = create_statement_block
@parser = many(create_func(block, create_type)).as(BasicParser(Array(Trees::TreeFunction)))
@parser = many(either(create_func(block, create_type), create_sprite)).as(BasicParser(Array(Trees::Tree)))
end
# Parses the given tokens into a tree.

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,11 +1,9 @@
require "./sprite.cr"
module Chalk
module Compiler
# An entry in the symbol table.
class Entry
end
# An entry that represents a function in the symbol table.
class FunctionEntry < Entry
class FunctionEntry
# Gets the function stored in this entry.
getter function
# Gets the address in code of this function.
@@ -23,7 +21,7 @@ module Chalk
end
# An entry that represents a variable in the symbol table.
class VarEntry < Entry
class VarEntry
# Gets the register occupied by the variable
# in this entry.
getter register
@@ -36,32 +34,47 @@ module Chalk
end
end
class SpriteEntry
property sprite : Sprite
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)
@data = {} of String => Entry
@functions = {} of String => FunctionEntry
@vars = {} of String => VarEntry
@sprites = {} of String => SpriteEntry
end
# Looks up the given *key* first in this table,
# then in its parent, continuing recursively.
def []?(key)
if entry = @data[key]?
return entry
end
return @parent.try &.[key]?
macro table_functions(name)
def get_{{name}}?(key)
@{{name}}s[key]? || @parent.try &.get_{{name}}?(key)
end
# Stores an *entry* under the given *key* into this table.
def []=(key, entry)
@data[key] = entry
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)
io << @data.map { |k, v| k + ": " + v.to_s }.join("\n")
end
end
end

View File

@@ -18,6 +18,11 @@ module Chalk
end
end
class Reducer(T)
def reduce(tree, children)
end
end
# The base class of a tree.
class Tree
def accept(v)
@@ -28,6 +33,10 @@ module Chalk
def apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, [] of T)
end
end
# A tree that represents an ID.
@@ -66,6 +75,10 @@ module Chalk
end
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @params.map &.reduce(r))
end
end
# A tree that represents an operation on two values.
@@ -89,6 +102,10 @@ module Chalk
@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
# A tree that represents a block of statements.
@@ -110,6 +127,9 @@ module Chalk
end
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
return r.reduce(self, @children.map &.reduce(r))
end
end
# A tree that represents a function declaration.
@@ -117,8 +137,10 @@ module Chalk
property name : String
property params : Array(String)
property block : Tree
getter type
def initialize(@name, @params, @block)
def initialize(@name, @params, return_type : Compiler::Type, @block)
@type = Compiler::FunctionType.new([Compiler::Type::U8] * @params.size, return_type)
end
def param_count
@@ -135,6 +157,10 @@ module Chalk
@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
# A tree that represents the declaration of
@@ -156,6 +182,10 @@ module Chalk
@expr = @expr.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end
# A tree that represents the assignment
@@ -177,6 +207,10 @@ module Chalk
@expr = @expr.apply(t)
return t.transform(self)
end
def reduce(r : Reducer(T)) forall T
r.reduce(self, [@expr.reduce(r)])
end
end
# A tree that represents an if statement.
@@ -202,6 +236,16 @@ module Chalk
@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
# A tree that represents a while loop.
@@ -224,6 +268,10 @@ module Chalk
@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.
@@ -243,6 +291,18 @@ module Chalk
@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

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

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

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

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

25
src/test.cr Normal file
View File

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