From 6ef300f402527f741198a1a5816433d159f6720e Mon Sep 17 00:00:00 2001 From: Dominic Grimm Date: Fri, 17 Jun 2022 13:38:12 +0200 Subject: [PATCH] Init --- .editorconfig | 9 + .gitignore | 8 + LICENSE | 21 + README.md | 3 + examples/add.asm | 44 ++ examples/hello_world.asm | 41 ++ pdf/chapter_break.tex | 2 + pdf/pdf.sh | 19 + pdf/pygments.theme | 211 +++++++ shard.lock | 10 + shard.yml | 19 + src/cli/hence.cr | 200 +++++++ src/hence.cr | 1 + src/hence/assembler.cr | 83 +++ src/hence/emulator.cr | 247 ++++++++ src/hence/firmware.cr | 181 ++++++ src/hence/firmware/documentation.md.ecr | 56 ++ src/hence/firmware/firmwares.cr | 13 + src/hence/firmware/firmwares/default.yml | 622 +++++++++++++++++++++ src/hence/firmware/macros/get_firmwares.cr | 1 + src/hence/microcode.cr | 98 ++++ src/hence/opcode.cr | 9 + src/hence/parser.cr | 361 ++++++++++++ src/hence/utils.cr | 16 + src/hence/version.cr | 5 + 25 files changed, 2280 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/add.asm create mode 100644 examples/hello_world.asm create mode 100644 pdf/chapter_break.tex create mode 100644 pdf/pdf.sh create mode 100644 pdf/pygments.theme create mode 100644 shard.lock create mode 100644 shard.yml create mode 100644 src/cli/hence.cr create mode 100644 src/hence.cr create mode 100644 src/hence/assembler.cr create mode 100644 src/hence/emulator.cr create mode 100644 src/hence/firmware.cr create mode 100644 src/hence/firmware/documentation.md.ecr create mode 100644 src/hence/firmware/firmwares.cr create mode 100644 src/hence/firmware/firmwares/default.yml create mode 100644 src/hence/firmware/macros/get_firmwares.cr create mode 100644 src/hence/microcode.cr create mode 100644 src/hence/opcode.cr create mode 100644 src/hence/parser.cr create mode 100644 src/hence/utils.cr create mode 100644 src/hence/version.cr diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d22cc77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf +./examples/*.bin +examples/*.bin +/.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d380e85 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Dominic Grimm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2e0b1f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# hence + +A stack based CPU with microcode and custom firmware support. diff --git a/examples/add.asm b/examples/add.asm new file mode 100644 index 0000000..a2489d6 --- /dev/null +++ b/examples/add.asm @@ -0,0 +1,44 @@ +PROGRAM_SIZE = (32 * 1024) +INSTRUCTION_SIZE = 3 + +main_jump = (PROGRAM_SIZE - INSTRUCTION_SIZE - INSTRUCTION_SIZE) + +.org 0x0000 + +push main_jump +jmp + +data: + answer: + ANSWER_SIZE = 52 + .bw "The answer to life, the universe, and everything is ", 0, ANSWER_SIZE + +main: + push answer + loop: + dup + load + emit + + inc + dup + push (answer + ANSWER_SIZE) + neq + push loop + jif + drop + + push 40 + push 2 + add + print + + push "!" + emit + push "\n" + emit +stp + +.org main_jump + push main + jmp diff --git a/examples/hello_world.asm b/examples/hello_world.asm new file mode 100644 index 0000000..d6600e4 --- /dev/null +++ b/examples/hello_world.asm @@ -0,0 +1,41 @@ +; Constants +PROGRAM_SIZE = (32 * 1024) +INSTRUCTION_SIZE = 3 + +main_jump = (PROGRAM_SIZE - INSTRUCTION_SIZE - INSTRUCTION_SIZE) + +; Base organisation / offset +.org 0x0000 + +; Static data +push main_jump +jmp ; Skip data section as following data is not program code + +data: + ; Message data + msg: + MSG_SIZE = 12 ; Define size of message + .bw "Hello World!", 0, MSG_SIZE ; Embed byte words in binary + +; Main label +main: + push msg + loop: + dup + load + emit + + inc + dup + push (msg + MSG_SIZE) + neq + push loop + jif + push "\n" + emit +stp + +; Jump to main label +.org main_jump + push main + jmp diff --git a/pdf/chapter_break.tex b/pdf/chapter_break.tex new file mode 100644 index 0000000..1f0b4e3 --- /dev/null +++ b/pdf/chapter_break.tex @@ -0,0 +1,2 @@ +\usepackage{sectsty} +\sectionfont{\underline\clearpage} diff --git a/pdf/pdf.sh b/pdf/pdf.sh new file mode 100644 index 0000000..faa11b3 --- /dev/null +++ b/pdf/pdf.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# -*- coding: utf-8 -*- + +pandoc "$1" \ + -f gfm \ + -t pdf \ + -H pdf/chapter_break.tex \ + --wrap=none \ + --reference-links \ + -s \ + --toc \ + --toc-depth 4 \ + -V geometry:a4paper \ + -V mainfont="Vollkorn" \ + -V monofont="DejaVu Sans Mono" \ + -V linkcolor:blue \ + --highlight-style pdf/pygments.theme \ + --pdf-engine=xelatex \ + -o "$2" diff --git a/pdf/pygments.theme b/pdf/pygments.theme new file mode 100644 index 0000000..8211f85 --- /dev/null +++ b/pdf/pygments.theme @@ -0,0 +1,211 @@ +{ + "text-color": null, + "background-color": "#f8f8f8", + "line-number-color": "#aaaaaa", + "line-number-background-color": null, + "text-styles": { + "Other": { + "text-color": "#007020", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Attribute": { + "text-color": "#7d9029", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "SpecialString": { + "text-color": "#bb6688", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Annotation": { + "text-color": "#60a0b0", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Function": { + "text-color": "#06287e", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "String": { + "text-color": "#4070a0", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "ControlFlow": { + "text-color": "#007020", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "Operator": { + "text-color": "#666666", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Error": { + "text-color": "#ff0000", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "BaseN": { + "text-color": "#40a070", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Alert": { + "text-color": "#ff0000", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + }, + "Variable": { + "text-color": "#19177c", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "BuiltIn": { + "text-color": null, + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Extension": { + "text-color": null, + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Preprocessor": { + "text-color": "#bc7a00", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Information": { + "text-color": "#60a0b0", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "VerbatimString": { + "text-color": "#4070a0", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Warning": { + "text-color": "#60a0b0", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Documentation": { + "text-color": "#ba2121", + "background-color": null, + "bold": false, + "italic": true, + "underline": false + }, + "Import": { + "text-color": null, + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Char": { + "text-color": "#4070a0", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "DataType": { + "text-color": "#902000", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Float": { + "text-color": "#40a070", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Comment": { + "text-color": "#60a0b0", + "background-color": null, + "bold": false, + "italic": true, + "underline": false + }, + "CommentVar": { + "text-color": "#60a0b0", + "background-color": null, + "bold": true, + "italic": true, + "underline": false + }, + "Constant": { + "text-color": "#880000", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "SpecialChar": { + "text-color": "#4070a0", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "DecVal": { + "text-color": "#40a070", + "background-color": null, + "bold": false, + "italic": false, + "underline": false + }, + "Keyword": { + "text-color": "#007020", + "background-color": null, + "bold": true, + "italic": false, + "underline": false + } + } +} diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..19fac7d --- /dev/null +++ b/shard.lock @@ -0,0 +1,10 @@ +version: 2.0 +shards: + commander: + git: https://github.com/mrrooijen/commander.git + version: 0.4.0 + + version_from_shard: + git: https://github.com/hugopl/version_from_shard.git + version: 1.2.5 + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..05dd360 --- /dev/null +++ b/shard.yml @@ -0,0 +1,19 @@ +name: hence +version: 0.1.0 + +authors: + - Dominic Grimm + +targets: + hence: + main: src/cli/hence.cr + +crystal: 1.4.1 + +license: MIT + +dependencies: + commander: + github: mrrooijen/commander + version_from_shard: + github: hugopl/version_from_shard diff --git a/src/cli/hence.cr b/src/cli/hence.cr new file mode 100644 index 0000000..420a191 --- /dev/null +++ b/src/cli/hence.cr @@ -0,0 +1,200 @@ +require "commander" +require "../hence" + +CLI = Commander::Command.new do |cmd| + cmd.use = "hence" + cmd.long = "Hence assembler and emulator" + + cmd.run do + puts cmd.help + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "version" + cmd.short = "Prins current hence version" + cmd.long = cmd.short + + cmd.run do + puts Hence::VERSION + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "firmware" + cmd.short = "Firmware utility" + cmd.long = cmd.short + + cmd.flags.add do |flag| + flag.name = "file" + flag.long = "--file" + flag.default = false + flag.description = "Load firmware from file" + flag.persistent = true + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "ls" + cmd.short = "Lists available firmwares" + cmd.long = cmd.short + + cmd.run do + puts Hence::Firmware::FIRMWARES.keys.join("\n") + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "compile [out]" + cmd.short = "Compiles a firmware" + cmd.long = cmd.short + + cmd.flags.add do |flag| + flag.name = "dump" + flag.long = "--dump" + flag.default = false + flag.description = "Hexdumps the firmware" + end + + cmd.run do |opts, args| + bin = if opts.bool["file"] + Hence::Firmware.from_yaml(File.read(args[0])) + else + Hence::Firmware::FIRMWARES[args[0]] + end.compile(Hence::Microcode::MICROCODE_NAMES) + File.write(args[1], bin) if args[1]? + + puts bin.hexdump if opts.bool["dump"] + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "doc [out]" + cmd.short = "Generates documentation for a firmware" + cmd.long = cmd.short + + cmd.flags.add do |flag| + flag.name = "dump" + flag.long = "--dump" + flag.default = false + flag.description = "Hexdumps the firmware" + end + + cmd.run do |opts, args| + doc = if opts.bool["file"] + Hence::Firmware.from_yaml(File.read(args[0])) + else + Hence::Firmware::FIRMWARES[args[0]] + end.to_s + File.write(args[1], doc) if args[1]? + + puts doc if opts.bool["dump"] + end + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "parse " + cmd.short = "Parses source code" + cmd.long = cmd.short + + cmd.run do |_opts, args| + # ameba:disable Lint/DebugCalls + pp Hence::Parser.parse(File.read(args[0])) + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "assemble [out]" + cmd.short = "Compiles assembly code into a binary" + cmd.long = cmd.short + + cmd.flags.add do |flag| + flag.name = "firmware" + flag.short = "-f" + flag.long = "--firmware" + flag.default = "default" + flag.description = "Custom firmware" + end + + cmd.flags.add do |flag| + flag.name = "file" + flag.long = "--file" + flag.default = false + flag.description = "Load firmware from YAML definition file" + end + + cmd.flags.add do |flag| + flag.name = "dump" + flag.long = "--dump" + flag.default = false + flag.description = "Hexdumps the binary" + end + + cmd.run do |opts, args| + bin = Hence::Assembler.assemble( + Hence::Parser.parse(File.read(args[0])), + if opts.bool["file"] + Hence::Firmware.from_yaml(File.read(opts.string["file"])) + else + Hence::Firmware::FIRMWARES[opts.string["firmware"]] + end.build_opcodes[:names] + ) + + File.write(args[1], bin) if args[1]? + puts bin.hexdump if opts.bool["dump"] + end + end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "run " + cmd.short = "Runs a binary" + cmd.long = cmd.short + + cmd.flags.add do |flag| + flag.name = "firmware" + flag.short = "-f" + flag.long = "--firmware" + flag.default = "default" + flag.description = "Custom firmware" + end + + cmd.flags.add do |flag| + flag.name = "file" + flag.long = "--file" + flag.default = false + flag.description = "Load firmware from file" + end + + cmd.run do |opts, args| + bin = Bytes.new({{ 32 * 1024 }}) + File.open(args[0]) do |file| + file.gets_to_end.to_slice.copy_to(bin) + end + + Hence::Emulator.emulate( + Hence::Emulator::Data.new( + bin, + if opts.bool["file"] + buffer = Bytes.new({{ 32 * 1024 }}) + File.open(opts.string["firmware"]) do |file| + file.gets_to_end.to_slice.copy_to(buffer) + end + + buffer + else + Hence::Firmware::FIRMWARES[opts.string["firmware"]].compile(Hence::Microcode::MICROCODE_NAMES) + end + ) + ) + end + end +end + +Commander.run(CLI, ARGV) diff --git a/src/hence.cr b/src/hence.cr new file mode 100644 index 0000000..de617e7 --- /dev/null +++ b/src/hence.cr @@ -0,0 +1 @@ +require "./hence/*" diff --git a/src/hence/assembler.cr b/src/hence/assembler.cr new file mode 100644 index 0000000..63c8f03 --- /dev/null +++ b/src/hence/assembler.cr @@ -0,0 +1,83 @@ +module Hence + module Assembler + extend self + + def assemble(ast : Parser::AST, opcode_names : Hash(String, UInt8), data = Data.new) : Bytes + ast.body.each do |node| + data.constants["OFFSET"] = Parser::AST::NumberArg.new(data.offset) + + case node + when Parser::AST::ConstantDeclarationNode + raise "Constants can't be redefined" if data.constants[node.name]? + + data.constants[node.name] = node.value + when Parser::AST::LabelNode + raise "Labels can't be redefined" if data.constants[node.name]? + + data.constants[node.name] = Parser::AST::NumberArg.new(data.offset) + when Parser::AST::MacroCallNode + MACROS[node.name].call(data, node.args) + when Parser::AST::CallNode + data.bin[data.offset] = opcode_names[node.name] + arg = node.arg.try(&.resolve(data)) || Bytes.new(2) + data.bin[data.offset + 1] = arg[0] + data.bin[data.offset + 2] = arg[1] + + data.offset += 3 + else + raise "Unexpected node type: #{typeof(node)}" + end + + raise "Offset out of bounds: 0x#{data.offset.to_s(16).rjust(4, '0')} > 0x#{data.bin.size.to_s(16).rjust(4, '0')}" if data.offset > data.bin.size + end + + data.bin + end + + class Data + property bin + property offset + property constants + + def initialize( + @bin = Bytes.new({{ 32 * 1024 }}), + @offset = 0_u16, + @constants = {} of String => Parser::AST::Arg + ) + end + end + + MACROS = { + "org" => ->(data : Data, args : Array(Parser::AST::Arg)) do + data.offset = args[0].resolve_as_number(data) + end, + "org_add" => ->(data : Data, args : Array(Parser::AST::Arg)) do + data.offset += args[0].resolve_as_number(data) + end, + "org_sub" => ->(data : Data, args : Array(Parser::AST::Arg)) do + data.offset -= args[0].resolve_as_number(data) + end, + "bytes" => ->(data : Data, args : Array(Parser::AST::Arg)) do + args.each do |a| + a.resolve(data).each do |b| + data.bin[data.offset] = b + data.offset += 1 + end + end + end, + "bw" => ->(data : Data, args : Array(Parser::AST::Arg)) do + string_arg = String.new(args[0].resolve(data)).gsub(/\\n/, '\n').to_slice + pos_start = args[1]?.try(&.resolve_as_number(data)) || 0_u16 + pos_end = args[2]?.try(&.resolve_as_number(data)) || string_arg.size.to_u16 + + string_arg[pos_start...pos_end].each do |b| + data.bin[data.offset] = b + data.offset += 1 + end + end, + "include" => ->(_data : Data, _args : Array(Parser::AST::Arg)) do + raise "Unimplemented" + end, + } + end +end diff --git a/src/hence/emulator.cr b/src/hence/emulator.cr new file mode 100644 index 0000000..f9470bb --- /dev/null +++ b/src/hence/emulator.cr @@ -0,0 +1,247 @@ +module Hence + module Emulator + extend self + + def emulate(data : Data) : Data + until data.reg_pc == 0xffff_u16 + data.reg_opc = (data.get_memory(data.reg_pc) & 0xff_u8).to_u8 + data.reg_pc &+= 1 + data.reg_arg = data.get_memory(data.reg_pc) << 8 + data.reg_pc &+= 1 + data.reg_arg = data.reg_arg | (data.get_memory(data.reg_pc) & 0xff_u8) + data.reg_pc &+= 1 + + base_mcc = data.reg_opc &* 4 + data.reg_mccs = data.firmware[base_mcc].to_u16 << 8 + data.reg_mccs = data.reg_mccs | data.firmware[base_mcc &+ 1] + data.reg_mcce = data.firmware[base_mcc &+ 2].to_u16 << 8 + data.reg_mcce = data.reg_mcce | data.firmware[base_mcc &+ 3] + + until data.reg_mccs &+ data.reg_mccpc >= data.reg_mcce + base_mccpc = data.reg_mccs &+ data.reg_mccpc + data.reg_mcc = data.firmware[base_mccpc] + data.reg_mccarg = data.firmware[base_mccpc &+ 1].to_u16 << 8 + data.reg_mccarg = data.reg_mccarg | data.firmware[base_mccpc &+ 2] + data.reg_mccpc &+= 3 + + data.microcodes[data.reg_mcc].run(data) + end + + data.reg_mccpc = 0_u16 + data.reg_tmp = 0_u16 + data.reg_a = 0_u16 + data.reg_b = 0_u16 + data.reg_c = 0_u16 + data.reg_d = 0_u16 + end + + data + end + + class Data + property firmware + property microcodes + property reg_pc + property reg_opc + property reg_arg + property reg_mccpc + property reg_mccs + property reg_mcce + property reg_mcc + property reg_mccarg + property reg_tmp + property reg_sp + property reg_a + property reg_b + property reg_c + property reg_d + property reg_inp + property reg_out + + def initialize( + @program : Bytes, + @firmware : Bytes, + @microcodes = Microcode::MICROCODES, + @reg_pc = 0_u16, + @reg_opc = 0_u8, + @reg_arg = 0_u16, + @reg_mccpc = 0_u16, + @reg_mccs = 0_u16, + @reg_mcce = 0_u16, + @reg_mcc = 0_u8, + @reg_mccarg = 0_u16, + @reg_tmp = 0_u16, + @reg_sp = 0_u16, + @reg_a = 0_u16, + @reg_b = 0_u16, + @reg_c = 0_u16, + @reg_d = 0_u16, + @reg_inp = 0_u16, + @reg_out = 0_u16, + @stack = Slice(UInt16).new({{ 8 * 1024 }}), + @memory = Slice(UInt16).new({{ 24 * 1024 }}) + ) + end + + def get_register(index : UInt8) : UInt16 + case index + when 0x0_u8 + @reg_pc + when 0x1_u8 + @reg_opc.to_u16 + when 0x2_u8 + @reg_arg + when 0x3_u8 + @reg_mccpc + when 0x4_u8 + @reg_mccs + when 0x5_u8 + @reg_mcce + when 0x6_u8 + @reg_mcc.to_u16 + when 0x7_u8 + @reg_mccarg + when 0x8_u8 + @reg_tmp + when 0x9_u8 + @reg_sp + when 0xa_u8 + @reg_a + when 0xb_u8 + @reg_b + when 0xc_u8 + @reg_c + when 0xd_u8 + @reg_d + when 0xe_u8 + @reg_inp + when 0xf_u8 + @reg_out + else + raise "Invalid register index: #{index}" + end + end + + def set_register(index : UInt8, value : UInt16) : UInt16 + case index + when 0x0_u8 + @reg_pc = value + when 0x1_u8 + @reg_opc = value + when 0x2_u8 + @reg_arg = value + when 0x3_u8 + @reg_mccpc = value + when 0x4_u8 + @reg_mccs = value + when 0x5_u8 + @reg_mcce = value + when 0x6_u8 + @reg_mcc = value + when 0x7_u8 + @reg_mccarg = value + when 0x8_u8 + @reg_tmp = value + when 0x9_u8 + @reg_sp = value + when 0xa_u8 + @reg_a = value + when 0xb_u8 + @reg_b = value + when 0xc_u8 + @reg_c = value + when 0xd_u8 + @reg_d = value + when 0xe_u8 + @reg_inp = value + when 0xf_u8 + @reg_out = value + else + raise "Invalid register index: #{index}" + end + end + + def alu(op : UInt8) : UInt16 + @reg_tmp = + case op + when 0x00 # not + ~@reg_a + when 0x01 # and + @reg_a & @reg_b + when 0x02 # nand + ~(@reg_a & @reg_b) + when 0x03 # or + @reg_a | @reg_b + when 0x04 # nor + ~(@reg_a | @reg_b) + when 0x05 # xor + @reg_a ^ @reg_b + when 0x06 # xnor + ~(@reg_a ^ @reg_b) + when 0x07 # shl + @reg_a << @reg_b + when 0x08 # shr + @reg_a >> @reg_b + when 0x09 # add + @reg_a &+ @reg_b + when 0x0a # sub + @reg_a &- @reg_b + when 0x0b # mul + @reg_a &* @reg_b + when 0x0c # div + @reg_a // @reg_b + when 0x0d # mod + @reg_a % @reg_b + when 0x0e # cmp + if @reg_a < @reg_b + 1_u16 + elsif @reg_a > @reg_b + 2_u16 + else + 0_u16 + end + when 0x0f # eq + @reg_a == @reg_b ? 1_u16 : 0_u16 + when 0x10 # neq + @reg_a != @reg_b ? 1_u16 : 0_u16 + when 0x11 # lt + @reg_a < @reg_b ? 1_u16 : 0_u16 + when 0x12 # gt + @reg_a > @reg_b ? 1_u16 : 0_u16 + when 0x13 # leq + @reg_a <= @reg_b ? 1_u16 : 0_u16 + when 0x14 # geq + @reg_a >= @reg_b ? 1_u16 : 0_u16 + when 0x15 # bol + @reg_a == 1 ? 1_u16 : 0_u16 + when 0x16 # neg + @reg_a == 1 ? 0_u16 : 1_u16 + else + raise "Invalid ALU operation: 0x#{op.to_s(16).rjust(2, '0')}" + end + end + + def get_memory(address : UInt16) : UInt16 + if address < {{ 32 * 1024 }} + @program[address].to_u16 + elsif address < {{ 40 * 1024 }} + @stack[address - {{ 32 * 1024 }}] + else + @memory[address - {{ 40 * 1024 }}] + end + end + + def set_memory(address : UInt16, value : UInt16) : UInt16 + if address < {{ 32 * 1024 }} + 0_u16 + elsif address < {{ 40 * 1024 }} + @stack[address - {{ 32 * 1024 }}] = value + + value + else + @memory[address - {{ 40 * 1024 }}] = value + end + end + end + end +end diff --git a/src/hence/firmware.cr b/src/hence/firmware.cr new file mode 100644 index 0000000..4e28721 --- /dev/null +++ b/src/hence/firmware.cr @@ -0,0 +1,181 @@ +require "yaml" +require "ecr" + +require "./firmware/*" + +module Hence + class Firmware + include YAML::Serializable + + @[YAML::Field] + property name + + @[YAML::Field(emit_null: true)] + property description + + @[YAML::Field] + property authors + + @[YAML::Field] + property date + + @[YAML::Field] + property version + + @[YAML::Field] + property sections + + def initialize( + @name : String, + @description : String?, + @authors : Array(String), + @date : Time, + @version : String, + @sections : Array(Section) + ) + end + + def full_name : String + "#{@name}:#{@version}" + end + + ECR.def_to_s "#{__DIR__}/firmware/documentation.md.ecr" + + def build_opcodes : {names: Hash(String, UInt8), opcodes: Hash(UInt8, Opcode)} + names = {} of String => UInt8 + opcodes = {} of UInt8 => Opcode + i = 0_u8 + @sections.each do |s| + s.opcodes.each do |opc| + j = if opc.opcode + opc.opcode.not_nil! + else + x = i + i += 1 + + x + end + raise "Duplicate opcode: #{j}" if opcodes.has_key?(j) + + names[opc.name] = j + opcodes[j] = opc.to_opcode(opc.opcode ? nil : j) + end + end + + {names: names, opcodes: opcodes} + end + + def compile(microcode_names : Hash(String, UInt8), data = Assembler::Data.new) : Bytes + sorted_opcodes = build_opcodes[:opcodes].to_a.sort_by!(&.[0]) + data.offset = (sorted_opcodes.last[0].to_u16 + 1) * 4 + sorted_opcodes.each do |opc| + end_offset = data.offset + opc[1].microcode.body.size * 3 + base = opc[0] * 4 + data.bin[base] = (data.offset >> 8).to_u8 + data.bin[base + 1] = (data.offset & 0xff).to_u8 + data.bin[base + 2] = (end_offset >> 8).to_u8 + data.bin[base + 3] = (end_offset & 0xff).to_u8 + + i = 0_u16 + opc[1].microcode.body.each do |node| + j = data.offset + i + + case node + when Parser::AST::LabelNode + data.constants[node.name] = Parser::AST::NumberArg.new(data.offset &- 3) + when Parser::AST::CallNode + data.bin[j] = microcode_names[node.name] + if node.arg + arg = node.arg.not_nil!.resolve(data) + + data.bin[j + 1] = arg.not_nil![0] + data.bin[j + 2] = arg.not_nil![1] + end + + i += 3 + else + raise "Unexpected node type: #{typeof(node)}" + end + end + + data.offset = end_offset + end + + data.bin + end + + class Section + include YAML::Serializable + + @[YAML::Field] + property name + + @[YAML::Field(emit_null: true)] + property description + + @[YAML::Field] + property opcodes + + def initialize(@name : String, @description : String?, @opcodes : Array(Opcode)) + end + + class Opcode + include YAML::Serializable + + @[YAML::Field] + property name + + @[YAML::Field(emit_null: true)] + property description + + @[YAML::Field(emit_null: true)] + property example + + @[YAML::Field(emit_null: true)] + property opcode + + @[YAML::Field(emit_null: true)] + property arg + + @[YAML::Field] + property stack + + @[YAML::Field] + property microcode + + def initialize( + @name : String, + @description : String?, + @example : String?, + @opcode : UInt8?, + @arg : String?, + @stack : Stack, + @microcode : String + ) + end + + def to_opcode(opcode : UInt8? = nil) : ::Hence::Opcode + raise "Opcode already defined" if opcode && @opcode + + Hence::Opcode.new( + @name, + Parser.parse(@microcode) + ) + end + + class Stack + include YAML::Serializable + + @[YAML::Field] + property input + + @[YAML::Field] + property output + + def initialize(@input : Array(String), @output : Array(String)) + end + end + end + end + end +end diff --git a/src/hence/firmware/documentation.md.ecr b/src/hence/firmware/documentation.md.ecr new file mode 100644 index 0000000..a458d93 --- /dev/null +++ b/src/hence/firmware/documentation.md.ecr @@ -0,0 +1,56 @@ +<%- sorted_opcodes = build_opcodes[:opcodes].to_a.sort_by!(&.[0]) -%> +--- +title: <%=full_name %> +description: "<%= @description %>" +author: [<%= @authors.map { |a| "\"#{a}\"" }.join(", ") %>] +date: <%= @date.to_s("%Y-%m-%d") %> +version: "<%= @version %>" +--- + +# `<%= full_name %>` + +<%= @description %> + +## Opcodes + +Opcode | Name +---|--- +<%- sorted_opcodes.each do |opcode| -%> +`0x<%= opcode[0].to_s(16).rjust(2, '0') %>` | [`<%= opcode[1].name %>`](#<%= opcode[1].name %>) +<%- end -%> + +## Sections + +<%- @sections.each do |section| -%> +### <%= section.name %> + +<%= section.description %> + +<%- section.opcodes.each do |opcode| -%> +#### `<%= opcode.name %>` + +<%= opcode.description %> + +<% input = opcode.stack.input.join(", ") -%> +<% input += " " unless input.blank? -%> +<% output = opcode.stack.output.join(", ") -%> +<% output = " " + output unless output.blank? -%> +Opcode | Argument | Stack +---|---|--- +`0x<%= sorted_opcodes.find! { |opc| opc[1].name == opcode.name }[0].to_s(16).rjust(2, '0') %>` | <%= opcode.arg && "`#{opcode.arg}`" %> | <%= (opcode.stack.input.size > 0 && opcode.stack.output.size > 0) ? "`#{input}--#{output}`" : nil %> +<%- if opcode.example -%> + +```gnuassembler +; Example +<%= opcode.example -%> +``` +<%- end -%> + +```gnuassembler +; Microcode +<%= opcode.microcode -%> +``` + +<%- end -%> +<%- end -%> +> _Generated using the [hence](https://git.dergrimm.net/dergrimm/hence) firmware compiler._ diff --git a/src/hence/firmware/firmwares.cr b/src/hence/firmware/firmwares.cr new file mode 100644 index 0000000..cc5edd3 --- /dev/null +++ b/src/hence/firmware/firmwares.cr @@ -0,0 +1,13 @@ +module Hence + class Firmware + {% begin %} + {% base_dir = "#{__DIR__}/firmwares" %} + + FIRMWARES = { + {% for firmware_name in run("./macros/get_firmwares.cr", base_dir).stringify.lines %} + {{ firmware_name }} => Firmware.from_yaml({{ read_file("#{base_dir.id}/#{firmware_name.id}.yml") }}), + {% end %} + } of String => Firmware + {% end %} + end +end diff --git a/src/hence/firmware/firmwares/default.yml b/src/hence/firmware/firmwares/default.yml new file mode 100644 index 0000000..ef9e926 --- /dev/null +++ b/src/hence/firmware/firmwares/default.yml @@ -0,0 +1,622 @@ +--- +name: default +description: Default hence opcode firmware +authors: + - Dominic Grimm +date: 2022-05-14 +version: 0.1.0 + +sections: + - name: flow-control + description: Flow control + opcodes: + - name: nop + description: No operation + example: | + nop ; literally nothing but wasting precious cycles + opcode: null + arg: null + stack: + input: [] + output: [] + microcode: | + nop + - name: dbg + description: Debugs current stack + example: | + push 40 + push 2 + dbg ; => (2) [40, 2] + opcode: null + arg: null + stack: + input: [] + output: [] + microcode: | + dbg + - name: stp + description: Stops execution + example: | + stp ; program stops execution here. push will not be called + push 1 + opcode: null + arg: null + stack: + input: [] + output: [] + microcode: | + tmps 0xfffc + tmpl 0x0 + - name: jmp + description: Jumps to address + opcode: null + arg: null + stack: + input: [addr] + output: [] + microcode: | + drop + tmpl 0x0 + - name: jif + description: Jumps to address if condition is true (1) + opcode: null + arg: null + stack: + input: [cond, addr] + output: [] + microcode: | + drop + tmpl 0xc + drop + tmpl 0xa + tmpsr 0xc + tmplc 0x0 + - name: stack-manipulation + description: Stack manipulation + opcodes: + - name: push + description: Pushes value to stack + opcode: null + arg: x + stack: + input: [] + output: [x] + microcode: | + tmpsr 0x2 + push + - name: drop + description: Drops top element from stack + opcode: null + arg: null + stack: + input: [x] + output: [] + microcode: | + drop + - name: depth + description: Returns stack size / depth + opcode: null + arg: null + stack: + input: [] + output: [depth] + microcode: | + tmpsr 0x9 + push + - name: pick + description: Duplicates top nth element stack value + opcode: null + arg: null + stack: + input: ["n"] + output: [x] + microcode: | + drop + pick + push + - name: dup + description: Duplicates top element on stack + opcode: null + arg: null + stack: + input: [x] + output: [x, x] + microcode: | + pick + push + - name: swap + description: Swaps top two elements on stack + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x2, x1] + microcode: | + drop + tmpl 0xa + drop + tmpl 0xb + tmpsr 0xa + push + tmpsr 0xb + push + - name: over + description: Duplicates second top element onto stack + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x1, x2, x1] + microcode: | + tmps 1 + pick + push + - name: rot + description: Rotates top three elements on stack + opcode: null + arg: null + stack: + input: [x1, x2, x3] + output: [x2, x3, x1] + microcode: | + drop + tmpl 0xa + drop + tmpl 0xb + drop + tmpl 0xc + tmpsr 0xb + push + tmpsr 0xc + push + tmpsr 0xa + push + - name: nip + description: Drops the first item below the top of stack + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x2] + microcode: | + drop + tmpl 0xa + drop + tmpsr 0xa + push + - name: tuck + description: Copies the top stack item below the second stack item + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x2, x1, x2] + microcode: | + drop + tmpl 0xa + drop + tmpl 0xb + tmpsr 0xa + push + tmpsr 0xb + push + tmpsr 0xa + push + - name: memory-access + description: Memory access + opcodes: + - name: load + description: Loads value from memory + opcode: null + arg: null + stack: + input: [addr] + output: [x] + microcode: | + drop + load + push + - name: store + description: Stores value to memory + opcode: null + arg: null + stack: + input: [x, addr] + output: [] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + tmpsr 0xb + store + - name: io + description: IO access + opcodes: + - name: inp + description: Gets input from general IO + opcode: null + arg: null + stack: + input: [] + output: [x] + microcode: | + tmpsr 0xe + push + - name: out + description: Outputs value to general IO + opcode: null + arg: null + stack: + input: [x] + output: [] + microcode: | + drop + tmpl 0xf + - name: print + description: Prints number to STDOUT / LCD display + opcode: null + arg: null + stack: + input: [x] + output: [] + microcode: | + drop + print + - name: emit + description: Prints input as unicode char to STDOUT / LCD display + opcode: null + arg: null + stack: + input: [x] + output: [] + microcode: | + drop + emit + - name: arithmetic + description: Arithmetic + opcodes: + - name: not + description: Logical invert of top stack element + opcode: null + arg: null + stack: + input: [x] + output: [x] + microcode: | + drop + tmpl 0xa + alu 0x00 + push + - name: and + description: Logical and of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x01 + push + - name: nand + description: Logical nand of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x02 + push + - name: or + description: Logical or of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x03 + push + - name: nor + description: Logical nor of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x04 + push + - name: xor + description: Logical xor of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x05 + push + - name: xnor + description: Logical xnor of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x06 + push + - name: shl + description: Shifts top stack element left by n bits + opcode: null + arg: null + stack: + input: [x, n] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x07 + push + - name: shr + description: Shifts top stack element right by n bits + opcode: null + arg: null + stack: + input: [x, n] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x08 + push + - name: add + description: Adds top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x09 + push + - name: inc + description: Increments top stack element by 1 + opcode: null + arg: null + stack: + input: [x] + output: [x] + microcode: | + tmps 1 + tmpl 0xb + drop + tmpl 0xa + alu 0x09 + push + - name: sub + description: Subtracts top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0a + push + - name: dec + description: Decrements top stack element by 1 + opcode: null + arg: null + stack: + input: [x] + output: [x] + microcode: | + tmps 1 + tmpl 0xb + drop + tmpl 0xa + alu 0x0a + push + - name: mul + description: Multiplies top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0b + push + - name: div + description: Divides top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0c + push + - name: mod + description: Modulo of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0d + push + - name: cmp + description: Compares top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0e + push + - name: eq + description: Logical equality of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x0f + push + - name: neq + description: Logical inequality of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x10 + push + - name: lt + description: Logical less than of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x11 + push + - name: gt + description: Logical greater than of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x12 + push + - name: leq + description: Logical less than or equal to of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x13 + push + - name: geq + description: Logical greater than or equal to of top two stack elements + opcode: null + arg: null + stack: + input: [x1, x2] + output: [x] + microcode: | + drop + tmpl 0xb + drop + tmpl 0xa + alu 0x14 + push + - name: bol + description: Boolean of top stack element + opcode: null + arg: null + stack: + input: [x] + output: [x] + microcode: | + drop + tmpl 0xa + alu 0x15 + push + - name: neg + description: Negates top stack element + opcode: null + arg: null + stack: + input: [x] + output: [x] + microcode: | + drop + tmpl 0xa + alu 0x16 + push diff --git a/src/hence/firmware/macros/get_firmwares.cr b/src/hence/firmware/macros/get_firmwares.cr new file mode 100644 index 0000000..679aa0b --- /dev/null +++ b/src/hence/firmware/macros/get_firmwares.cr @@ -0,0 +1 @@ +print Dir[Path[ARGV[0], "*.yml"]].map { |f| File.basename(f, ".yml") }.join("\n") diff --git a/src/hence/microcode.cr b/src/hence/microcode.cr new file mode 100644 index 0000000..6ef2ed6 --- /dev/null +++ b/src/hence/microcode.cr @@ -0,0 +1,98 @@ +require "colorize" +require "string_scanner" + +module Hence + struct Microcode + def initialize(@runner : Emulator::Data ->) + end + + def initialize(&@runner : Emulator::Data ->) + end + + def run(data : Emulator::Data) : Emulator::Data + @runner.call(data) + + data + end + + MICROCODES = { + 0x00_u8 => Microcode.new { }, # nop + 0x01_u8 => Microcode.new do |d| # dbg + stack = if d.reg_sp == 0 + [] of UInt16 + else + d.reg_sp.times.map_with_index { |_, i| d.get_memory({{ 32_u16 * 1024 }} + i) } + end + + puts "#{"[DEBUG]".colorize(:red).mode(:bold)}: (#{d.reg_sp}) #{stack}" + print "Press enter to continue execution...".colorize.mode(:dim) + gets + end, + 0x02_u8 => Microcode.new do |d| # jmp + d.reg_mccpc = d.reg_tmp + end, + 0x03_u8 => Microcode.new do |d| # jif + if d.reg_tmp == 1 + d.reg_mccpc = d.reg_a + end + end, + 0x04_u8 => Microcode.new do |d| # tmpl + d.set_register((d.reg_mccarg & 0xff).to_u8, d.reg_tmp) + end, + 0x05_u8 => Microcode.new do |d| # tmplc + d.set_register((d.reg_mccarg & 0xff).to_u8, d.reg_tmp) if d.reg_a == 1 + end, + 0x06_u8 => Microcode.new do |d| # tmps + d.reg_tmp = d.reg_mccarg + end, + 0x07_u8 => Microcode.new do |d| # tmpsr + d.reg_tmp = d.get_register((d.reg_mccarg & 0xff).to_u8) + end, + 0x08_u8 => Microcode.new do |d| # push + d.set_memory({{ 32_u16 * 1024 }} + d.reg_sp, d.reg_tmp) + d.reg_sp += 1 + end, + 0x09_u8 => Microcode.new do |d| # drop + d.reg_tmp = d.get_memory({{ 32_u16 * 1024 }} + d.reg_sp - 1) + d.set_memory({{ 32_u16 * 1024 }} + d.reg_sp - 1, 0_u16) + d.reg_sp -= 1 if d.reg_sp.positive? + end, + 0x0a_u8 => Microcode.new do |d| # pick + d.reg_tmp = d.get_memory({{ 32_u16 * 1024 }} + d.reg_sp - 1 - d.reg_tmp) + end, + 0x0b_u8 => Microcode.new do |d| # alu + d.alu((d.reg_mccarg & 0xff).to_u8) + end, + 0x0c_u8 => Microcode.new do |d| # load + d.reg_tmp = d.get_memory(d.reg_tmp) + end, + 0x0d_u8 => Microcode.new do |d| # store + d.set_memory(d.reg_tmp, d.reg_a) + end, + 0x0e_u8 => Microcode.new do |d| # print + print d.reg_tmp + end, + 0x0f_u8 => Microcode.new do |d| # emit + print String.new(Bytes[d.reg_tmp]) + end, + } of UInt8 => Microcode + MICROCODE_NAMES = { + "nop" => 0x00_u8, + "dbg" => 0x01_u8, + "jmp" => 0x02_u8, + "jif" => 0x03_u8, + "tmpl" => 0x04_u8, + "tmplc" => 0x05_u8, + "tmps" => 0x06_u8, + "tmpsr" => 0x07_u8, + "push" => 0x08_u8, + "drop" => 0x09_u8, + "pick" => 0x0a_u8, + "alu" => 0x0b_u8, + "load" => 0x0c_u8, + "store" => 0x0d_u8, + "print" => 0x0e_u8, + "emit" => 0x0f_u8, + } of String => UInt8 + end +end diff --git a/src/hence/opcode.cr b/src/hence/opcode.cr new file mode 100644 index 0000000..b5d21be --- /dev/null +++ b/src/hence/opcode.cr @@ -0,0 +1,9 @@ +module Hence + struct Opcode + property name + property microcode + + def initialize(@name : String, @microcode : Parser::AST) + end + end +end diff --git a/src/hence/parser.cr b/src/hence/parser.cr new file mode 100644 index 0000000..39fed74 --- /dev/null +++ b/src/hence/parser.cr @@ -0,0 +1,361 @@ +require "string_scanner" + +module Hence + module Parser + extend self + + def parse(source : String) : AST + s = StringScanner.new(source.gsub(/;.*|@.*|#.*/, nil)) + body = [] of AST::Node + loop do + s.skip(/\s*/) + break if s.eos? + + body << + if constant_dirty_name = s.scan(/\S+[ \t]*=/) + constant_name = constant_dirty_name[..-2].rstrip + + s.skip(/[ \t]*/) + + constant_values = parse_args(s).not_nil! + raise "Constant can't have more than one value" if constant_values.size > 1 + + AST::ConstantDeclarationNode.new(constant_name, constant_values.first) + elsif label_dirty_name = s.scan(/\S+:/) + label_name = label_dirty_name.not_nil![..-2] + raise "Label name must be lowercase" if label_name != label_name.downcase + + AST::LabelNode.new(label_name) + # else + # call_name = s.scan(/\S+/).not_nil! + + # s.skip(/[ \t]*/) + + # if call_name.starts_with?('.') + # AST::MacroCallNode.new(call_name[1..], self.parse_args(s) || [] of AST::Arg) + # else + # call_args = self.parse_args(s) + # raise "Op code can't be called with more than one argument" if call_args && call_args.size > 1 + + # AST::CallNode.new(call_name, call_args.try(&.first?)) + # end + elsif macro_name_raw = s.scan(/\.\S+/) + s.skip(/[ \t]*/) + + AST::MacroCallNode.new(macro_name_raw[1..], parse_args(s) || [] of AST::Arg) + else + call_name = s.scan(/\S+/).not_nil! + + s.skip(/[ \t]*/) + + call_args = parse_args(s) || [] of AST::Arg + raise "Op code can't be called with more than one argument" if call_args && call_args.size > 1 + + AST::CallNode.new(call_name, call_args.first?) + end + end + + AST.new(body) + end + + def parse_binary_operation(op : String) : AST::BinaryOperation + case op + when "+" + AST::AddBinaryOperation + when "-" + AST::SubBinaryOperation + when "*" + AST::MulBinaryOperation + when "/" + AST::DivBinaryOperation + when "**" + AST::PowBinaryOperation + else + raise "Unexpected binary operation: #{op}" + end.new + end + + def parse_binary_expression_arg(scanner : StringScanner) : AST::BinaryExpressionArg? + scanner.skip(/[ \t]*/) + return unless scanner.scan(/\(/) + + content = scanner.scan_until(/\)/).not_nil![..-2].split(/\s/, remove_empty: true) + raise "Malformed binary expression" if content.size < 3 || content.size % 2 == 0 + + first_args = content.shift(3) + bin_expr = AST::BinaryExpressionArg.new( + parse_args(first_args[0] + "\n").not_nil!.first, + parse_args(first_args[2] + "\n").not_nil!.first, + parse_binary_operation(first_args[1]) + ) + + until content.empty? + args = content.shift(2).not_nil! + bin_expr = AST::BinaryExpressionArg.new( + bin_expr, + parse_args(args[1] + "\n").not_nil!.first, + parse_binary_operation(args[0]) + ) + end + + bin_expr + end + + def parse_binary_expression_arg(string : String) : AST::BinaryExpressionArg? + parse_binary_expression_arg(StringScanner.new(string)) + end + + def parse_args(scanner : StringScanner) : Array(AST::Arg)? + args_s = scanner.scan_until(/\n/) + + if args_s.try(&.blank?) + [] of AST::Arg + else + s = StringScanner.new(args_s.not_nil!.strip) + args = [] of AST::Arg + + until s.eos? + s.skip(/[ \t]*/) + raise "Can't seperate arguments because of no comma" if s.skip(/,/).nil? && args.size > 0 + s.skip(/[ \t]*/) + + args << + if s.check(/\(/) + parse_binary_expression_arg(s).not_nil! + elsif string_raw = s.scan(/".*"/) + AST::StringArg.new(string_raw[1..-2].gsub(/\\n/, '\n')) + elsif number = s.scan(/\d[\d\w]*/) + AST::NumberArg.new(number.to_u16(prefix: true, underscore: true, strict: true)) + else + AST::ConstantArg.new(s.scan(/\w+/).not_nil!) + end + end + + args + end + end + + def parse_args(string : String) : Array(AST::Arg)? + parse_args(StringScanner.new(string)) + end + + struct AST + property body + + def initialize(@body : Body) + end + + def to_s : String + @body.map(&.to_s).join('\n') + end + + abstract struct Node + abstract def to_s : String + end + + alias Body = Array(Node) + + struct LabelNode < Node + property name + + def initialize(@name : String) + end + + def to_s : String + "#{@name}:" + end + end + + abstract class Arg + abstract def resolve(data : Assembler::Data) : Bytes + + abstract def resolve_as_number(data : Assembler::Data) : UInt16 + + abstract def to_s : String + end + + class NumberArg < Arg + property number + + def initialize(@number : UInt16) + end + + def resolve(_data : Assembler::Data) : Bytes + n = Utils.split_uint16(@number) + Bytes[n[0], n[1]] + end + + def resolve_as_number(_data : Assembler::Data) : UInt16 + @number + end + + def to_s : String + @number.to_s + end + end + + class ConstantArg < Arg + property name + + def initialize(@name : String) + end + + def resolve(data : Assembler::Data) : Bytes + data.constants[@name].resolve(data) + end + + def resolve_as_number(data : Assembler::Data) : UInt16 + data.constants[@name].resolve_as_number(data) + end + + def to_s : String + @name + end + end + + class StringArg < Arg + property string + + def initialize(@string : String) + end + + def resolve(_data : Assembler::Data) : Bytes + if @string.bytesize == 1 + Bytes[0_u8, @string.bytes[0]] + else + Bytes.new(@string.bytes.to_unsafe, @string.bytesize) + end + end + + def resolve_as_number(_data : Assembler::Data) : UInt16 + slice = resolve(_data) + + Utils.merge_uint16(slice[0], slice[1]) + end + + def to_s : String + "\"#{@string}\"" + end + end + + abstract struct BinaryOperation + abstract def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + + abstract def to_s : String + end + + struct AddBinaryOperation < BinaryOperation + def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + NumberArg.new(left.resolve_as_number(data) &+ right.resolve_as_number(data)) + end + + def to_s : String + "+" + end + end + + struct SubBinaryOperation < BinaryOperation + def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + NumberArg.new(left.resolve_as_number(data) &- right.resolve_as_number(data)) + end + + def to_s : String + "-" + end + end + + struct MulBinaryOperation < BinaryOperation + def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + NumberArg.new(left.resolve_as_number(data) &* right.resolve_as_number(data)) + end + + def to_s : String + "*" + end + end + + struct DivBinaryOperation < BinaryOperation + def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + NumberArg.new(left.resolve_as_number(data) // right.resolve_as_number(data)) + end + + def to_s : String + "/" + end + end + + struct PowBinaryOperation < BinaryOperation + def resolve(left : Arg, right : Arg, data : Assembler::Data) : Arg + NumberArg.new(left.resolve_as_number(data) &** right.resolve_as_number(data)) + end + + def to_s : String + "**" + end + end + + class BinaryExpressionArg < Arg + property left + property right + property op + + def initialize(@left : Arg, @right : Arg, @op : BinaryOperation) + end + + def resolve(data : Assembler::Data) : Bytes + @op.resolve(@left, @right, data).resolve(data) + end + + def resolve_as_number(data : Assembler::Data) : UInt16 + @op.resolve(@left, @right, data).resolve_as_number(data) + end + + def to_s : String + # ameba:disable Lint/RedundantStringCoercion + "(#{@left.to_s} #{@op.to_s} #{@right.to_s})" + end + end + + struct ConstantDeclarationNode < Node + property name + property value + + def initialize(@name : String, @value : Arg) + end + + def to_s : String + "#{@name}" + end + end + + struct MacroCallNode < Node + property name + property args + + def initialize(@name : String, @args : Array(Arg)) + end + + def to_s : String + ".#{@name}" + end + end + + struct CallNode < Node + property name + property arg + + def initialize(@name : String, @arg : Arg? = nil) + end + + def to_s : String + if @arg + # ameba:disable Lint/RedundantStringCoercion + "#{@name} #{@arg.to_s}" + else + @name + end + end + end + end + end +end diff --git a/src/hence/utils.cr b/src/hence/utils.cr new file mode 100644 index 0000000..f69e1be --- /dev/null +++ b/src/hence/utils.cr @@ -0,0 +1,16 @@ +module Hence + module Utils + extend self + + macro split_uint16(x) + { + ({{ x }} >> 8).to_u8, + ({{ x }} & 0xff_u8).to_u8 + } + end + + macro merge_uint16(x, y) + ({{ x }}.to_u16 << 8) | {{ y }} + end + end +end diff --git a/src/hence/version.cr b/src/hence/version.cr new file mode 100644 index 0000000..f65670a --- /dev/null +++ b/src/hence/version.cr @@ -0,0 +1,5 @@ +require "version_from_shard" + +module Hence + VersionFromShard.declare +end