From 86d579e8d5481dfc34fc71bfc37dcd4200c6bf38 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Sun, 18 Feb 2024 11:26:57 +0100 Subject: [PATCH] Grow stack automatically --- CMakeLists.txt | 5 + README | 8 ++ Rakefile | 33 +------ TODO | 8 ++ source/elna/ir.d | 146 +++++++++++++++------------- source/elna/lexer.d | 50 +++++----- source/elna/riscv.d | 50 ++++++---- tests/7_member_sum.eln | 1 + tests/expectations/7_member_sum.txt | 1 + tests/expectations/print_number.txt | 1 + tests/print_number.eln | 2 + tests/runner.cpp | 103 ++++++++++++++++++++ 12 files changed, 262 insertions(+), 146 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 TODO create mode 100644 tests/7_member_sum.eln create mode 100644 tests/expectations/7_member_sum.txt create mode 100644 tests/expectations/print_number.txt create mode 100644 tests/print_number.eln create mode 100755 tests/runner.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..66c650c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.21) +project(Elna) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +add_executable(tester tests/runner.cpp) diff --git a/README b/README index b6e03eb..e496ae7 100644 --- a/README +++ b/README @@ -35,3 +35,11 @@ factor = ident | number | "(" expression ")"; "!" - Write a line. "?" - Read user input. "odd" - The only function, returns whether a number is odd. + +# Build and test + +```sh +cmake -B build +make -C build +./build/bin/tester +``` diff --git a/Rakefile b/Rakefile index 025ed5e..7670654 100644 --- a/Rakefile +++ b/Rakefile @@ -39,40 +39,9 @@ file BINARY => SOURCES do |t| sh({ 'DFLAGS' => (DFLAGS * ' ') }, 'dub', 'build', '--compiler=gdc') end +task default: TESTS task default: BINARY -desc 'Run all tests and check the results' -task test: TESTS -task test: BINARY do - TESTS.each do |test| - expected = Pathname - .new(test) - .sub_ext('.txt') - .sub(/^build\/[[:alpha:]]+\//, 'tests/expectations/') - .to_path - - puts "Running #{test}" - if test.include? '/riscv/' - spike = [ - '/opt/riscv/bin/spike', - '--isa=RV32IMAC', - '/opt/riscv/riscv32-unknown-elf/bin/pk', - test - ] - diff = ['diff', '-Nur', '--color', expected, '-'] - tail = ['tail', '-n', '1'] - - last_stdout, wait_threads = Open3.pipeline_r spike, tail, diff - else - raise 'Unsupported test platform' - end - print last_stdout.read - last_stdout.close - - fail unless wait_threads.last.value.exitstatus.zero? - end -end - desc 'Run unittest blocks' task unittest: SOURCES do |t| sh('dub', 'test', '--compiler=gdc-12') diff --git a/TODO b/TODO new file mode 100644 index 0000000..e202e19 --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +- Mark all classes as extern(C++) to interface with C++. +- Some nodes assert(false, "Not implemented") because their logic is handled + in the parent nodes. Move this logic to the nodes themselves. +- Implement multiplication and division. +- Split tester.cpp into a header and implementation files. +- The generate function in backend.d contains the whole compilation logic. + It's not a backend task. +- Output assembly. diff --git a/source/elna/ir.d b/source/elna/ir.d index edb70fa..13582ba 100644 --- a/source/elna/ir.d +++ b/source/elna/ir.d @@ -15,34 +15,35 @@ struct ASTMapping alias Node = .Node; alias Definition = .Definition; alias VariableDeclaration = .VariableDeclaration; - alias Statement = Array!(.Statement); - alias BangStatement = .Expression; + alias Statement = .Operand; + alias BangStatement = .Operand; alias Block = .Definition; - alias Expression = .Expression; + alias Expression = .Operand; alias Number = .Number; - alias Variable = .Variable; - alias BinaryExpression = .BinaryExpression; + alias Variable = .Number; + alias BinaryExpression = .Variable; } /** * IR visitor. */ +extern(C++) interface IRVisitor { abstract void visit(Node) @nogc; abstract void visit(Definition) @nogc; - abstract void visit(Expression) @nogc; - abstract void visit(Statement) @nogc; + abstract void visit(Operand) @nogc; + abstract void visit(BinaryExpression) @nogc; abstract void visit(Variable) @nogc; abstract void visit(VariableDeclaration) @nogc; abstract void visit(Number) @nogc; - abstract void visit(BinaryExpression) @nogc; } /** * AST node. */ +extern(C++) abstract class Node { abstract void accept(IRVisitor) @nogc; @@ -51,11 +52,13 @@ abstract class Node /** * Definition. */ +extern(C++) class Definition : Node { char[] identifier; - Array!Statement statements; + Array!BinaryExpression statements; Array!VariableDeclaration variableDeclarations; + Operand result; override void accept(IRVisitor visitor) @nogc { @@ -63,17 +66,13 @@ class Definition : Node } } -class Statement : Node +extern(C++) +abstract class Statement : Node { - BinaryExpression expression; - - override void accept(IRVisitor visitor) @nogc - { - visitor.visit(this); - } } -abstract class Expression : Node +extern(C++) +abstract class Operand : Node { override void accept(IRVisitor visitor) @nogc { @@ -81,7 +80,8 @@ abstract class Expression : Node } } -class Number : Expression +extern(C++) +class Number : Operand { int value; @@ -91,7 +91,8 @@ class Number : Expression } } -class Variable : Expression +extern(C++) +class Variable : Operand { size_t counter; @@ -101,6 +102,7 @@ class Variable : Expression } } +extern(C++) class VariableDeclaration : Node { String identifier; @@ -111,12 +113,13 @@ class VariableDeclaration : Node } } -class BinaryExpression : Node +extern(C++) +class BinaryExpression : Statement { - Expression lhs, rhs; + Operand lhs, rhs; BinaryOperator operator; - this(Expression lhs, Expression rhs, BinaryOperator operator) + this(Operand lhs, Operand rhs, BinaryOperator operator) @nogc { this.lhs = lhs; @@ -130,9 +133,26 @@ class BinaryExpression : Node } } +extern(C++) +class BangExpression : Statement +{ + Operand operand; + + this(Operand operand) + { + this.operand = operand; + } + + override void accept(IRVisitor visitor) @nogc + { + visitor.visit(this); + } +} + final class TransformVisitor : parser.ParserVisitor!ASTMapping { private HashTable!(String, int) constants; + private Array!BinaryExpression statements; ASTMapping.Node visit(parser.Node node) @nogc { @@ -151,7 +171,7 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping ASTMapping.BangStatement visit(parser.BangStatement statement) @nogc { - assert(false, "Not implemented"); + return statement.expression.accept(this); } ASTMapping.Block visit(parser.Block block) @nogc @@ -159,7 +179,9 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping auto target = defaultAllocator.make!Definition; this.constants = transformConstants(block.definitions); - target.statements = block.statement.accept(this); + target.result = block.statement.accept(this); + target.statements = this.statements; + target.variableDeclarations = transformVariableDeclarations(block.variableDeclarations); return target; @@ -167,41 +189,45 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping ASTMapping.Expression visit(parser.Expression expression) @nogc { - assert(false, "Not implemented"); + if ((cast(parser.Number) expression) !is null) + { + return (cast(parser.Number) expression).accept(this); + } + if ((cast(parser.Variable) expression) !is null) + { + return (cast(parser.Variable) expression).accept(this); + } + else if ((cast(parser.BinaryExpression) expression) !is null) + { + return (cast(parser.BinaryExpression) expression).accept(this); + } + assert(false, "Invalid expression type"); } ASTMapping.Number visit(parser.Number number) @nogc { - assert(false, "Not implemented"); + auto numberExpression = defaultAllocator.make!Number; + numberExpression.value = number.value; + + return numberExpression; } ASTMapping.Variable visit(parser.Variable variable) @nogc { - assert(false, "Not implemented"); + auto numberExpression = defaultAllocator.make!Number; + numberExpression.value = this.constants[variable.identifier]; + + return numberExpression; } - ASTMapping.BinaryExpression visit(parser.BinaryExpression) @nogc - { - assert(false, "Not implemented"); - } - - private Number transformNumber(parser.Number number) @nogc - { - return defaultAllocator.make!Number(number.value); - } - - private Variable binaryExpression(parser.BinaryExpression binaryExpression, - ref Array!Statement statements) @nogc + ASTMapping.BinaryExpression visit(parser.BinaryExpression binaryExpression) @nogc { auto target = defaultAllocator.make!BinaryExpression( - expression(binaryExpression.lhs, statements), - expression(binaryExpression.rhs, statements), + binaryExpression.lhs.accept(this), + binaryExpression.rhs.accept(this), binaryExpression.operator ); - - auto newStatement = defaultAllocator.make!Statement; - newStatement.expression = target; - statements.insertBack(newStatement); + statements.insertBack(target); auto newVariable = defaultAllocator.make!Variable; newVariable.counter = statements.length; @@ -209,38 +235,18 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping return newVariable; } - private Expression expression(parser.Expression expression, - ref Array!Statement statements) @nogc + private Number transformNumber(parser.Number number) @nogc { - if ((cast(parser.Number) expression) !is null) - { - auto numberExpression = defaultAllocator.make!Number; - numberExpression.value = (cast(parser.Number) expression).value; - - return numberExpression; - } - if ((cast(parser.Variable) expression) !is null) - { - auto numberExpression = defaultAllocator.make!Number; - numberExpression.value = this.constants[(cast(parser.Variable) expression).identifier]; - - return numberExpression; - } - else if ((cast(parser.BinaryExpression) expression) !is null) - { - return binaryExpression(cast(parser.BinaryExpression) expression, statements); - } - return null; + return defaultAllocator.make!Number(number.value); } - override Array!Statement visit(parser.Statement statement) @nogc + override Operand visit(parser.Statement statement) @nogc { - typeof(return) statements; if ((cast(parser.BangStatement) statement) !is null) { - expression((cast(parser.BangStatement) statement).expression, statements); + return (cast(parser.BangStatement) statement).accept(this); } - return statements; + assert(false, "Invalid statement type"); } private HashTable!(String, int) transformConstants(ref Array!(parser.Definition) definitions) @nogc diff --git a/source/elna/lexer.d b/source/elna/lexer.d index 83fb873..a38a0ee 100644 --- a/source/elna/lexer.d +++ b/source/elna/lexer.d @@ -8,6 +8,7 @@ import std.range; import tanya.container.array; import tanya.container.string; +extern(C++) struct Token { enum Type @@ -71,7 +72,7 @@ struct Token } @property auto value(Type type)() @nogc nothrow pure @trusted - in (ofType(type)) + in (ofType(type), "Expected type: " ~ type.stringof) { static if (type == Type.number) { @@ -99,14 +100,17 @@ struct Token /** * Range over the source text that keeps track of the current position. */ +extern(C++) struct Source { - char[] buffer; + char* buffer_; + size_t length_; Position position; - this(char[] buffer) @nogc nothrow pure @safe + this(char* buffer, const size_t length) @nogc nothrow pure { - this.buffer = buffer; + this.buffer_ = buffer; + this.length_ = length; } @disable this(); @@ -116,54 +120,50 @@ struct Source return this.length == 0; } - char front() @nogc nothrow pure @safe + char front() @nogc nothrow pure in (!empty) { - return this.buffer[0]; + return this.buffer_[0]; } - void popFront() @nogc nothrow pure @safe + void popFront() @nogc nothrow pure in (!empty) { - this.buffer = buffer[1 .. $]; + ++this.buffer_; + --this.length_; ++this.position.column; } - void breakLine() @nogc nothrow pure @safe + void breakLine() @nogc nothrow pure in (!empty) { - this.buffer = buffer[1 .. $]; + ++this.buffer_; + --this.length_; ++this.position.line; this.position.column = 1; } @property size_t length() const @nogc nothrow pure @safe { - return this.buffer.length; + return this.length_; } - char opIndex(size_t index) @nogc nothrow pure @safe + char opIndex(size_t index) @nogc nothrow pure in (index < length) { - return this.buffer[index]; + return this.buffer_[index]; } - char[] opSlice(size_t i, size_t j) @nogc nothrow pure @safe - in + char* buffer() @nogc nothrow pure { - assert(i <= j); - assert(j <= length); - } - do - { - return this.buffer[i .. j]; + return this.buffer_; } } Result!(Array!Token) lex(char[] buffer) @nogc { Array!Token tokens; - auto source = Source(buffer); + auto source = Source(buffer.ptr, buffer.length); while (!source.empty) { @@ -218,17 +218,17 @@ Result!(Array!Token) lex(char[] buffer) @nogc { ++i; } - if (source[0 .. i] == "const") + if (source.buffer[0 .. i] == "const") { tokens.insertBack(Token(Token.Type.let, source.position)); } - else if (source[0 .. i] == "var") + else if (source.buffer[0 .. i] == "var") { tokens.insertBack(Token(Token.Type.var, source.position)); } else { - auto identifier = String(source[0 .. i]); + auto identifier = String(source.buffer[0 .. i]); tokens.insertBack(Token(Token.Type.identifier, identifier, source.position)); } source.popFrontN(i); diff --git a/source/elna/riscv.d b/source/elna/riscv.d index 8fcfb2d..a41b7cf 100644 --- a/source/elna/riscv.d +++ b/source/elna/riscv.d @@ -1,5 +1,6 @@ module elna.riscv; +import core.stdc.stdlib; import elna.extended; import elna.ir; import elna.result; @@ -7,7 +8,6 @@ import std.algorithm; import std.typecons; import tanya.container.array; import tanya.container.string; -import tanya.memory.allocator; enum XRegister : ubyte { @@ -140,14 +140,14 @@ struct Instruction return this; } - ref Instruction s(uint imm1, Funct3 funct3, XRegister rs1, XRegister rs2, uint imm2 = 0) + ref Instruction s(uint imm1, Funct3 funct3, XRegister rs1, XRegister rs2) return scope @nogc { - this.instruction |= (imm1 << 7) + this.instruction |= ((imm1 & 0b11111) << 7) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) - | (imm2 << 25); + | ((imm1 & 0b111111100000) << 20); return this; } @@ -178,6 +178,7 @@ struct Instruction } } +extern(C++) class RiscVVisitor : IRVisitor { Array!Instruction instructions; @@ -191,22 +192,24 @@ class RiscVVisitor : IRVisitor override void visit(Definition definition) @nogc { + const uint stackSize = cast(uint) (definition.statements.length * 4 + 12); + // Prologue. this.instructions.insertBack( Instruction(BaseOpcode.opImm) - .i(XRegister.sp, Funct3.addi, XRegister.sp, cast(uint) -32) + .i(XRegister.sp, Funct3.addi, XRegister.sp, -stackSize) ); this.instructions.insertBack( Instruction(BaseOpcode.store) - .s(28, Funct3.sw, XRegister.sp, XRegister.s0) + .s(stackSize - 4, Funct3.sw, XRegister.sp, XRegister.s0) ); this.instructions.insertBack( Instruction(BaseOpcode.store) - .s(24, Funct3.sw, XRegister.sp, XRegister.ra) + .s(stackSize - 8, Funct3.sw, XRegister.sp, XRegister.ra) ); this.instructions.insertBack( Instruction(BaseOpcode.opImm) - .i(XRegister.s0, Funct3.addi, XRegister.sp, 32) + .i(XRegister.s0, Funct3.addi, XRegister.sp, stackSize) ); foreach (statement; definition.statements[]) @@ -217,6 +220,10 @@ class RiscVVisitor : IRVisitor { variableDeclaration.accept(this); } + this.registerInUse = true; + definition.result.accept(this); + this.registerInUse = false; + // Print the result. this.instructions.insertBack( Instruction(BaseOpcode.opImm) @@ -247,15 +254,15 @@ class RiscVVisitor : IRVisitor // Epilogue. this.instructions.insertBack( Instruction(BaseOpcode.load) - .i(XRegister.s0, Funct3.lw, XRegister.sp, 28) + .i(XRegister.s0, Funct3.lw, XRegister.sp, stackSize - 4) ); this.instructions.insertBack( Instruction(BaseOpcode.load) - .i(XRegister.ra, Funct3.lw, XRegister.sp, 24) + .i(XRegister.ra, Funct3.lw, XRegister.sp, stackSize - 8) ); this.instructions.insertBack( Instruction(BaseOpcode.opImm) - .i(XRegister.sp, Funct3.addi, XRegister.sp, 32) + .i(XRegister.sp, Funct3.addi, XRegister.sp, stackSize) ); this.instructions.insertBack( Instruction(BaseOpcode.jalr) @@ -263,13 +270,16 @@ class RiscVVisitor : IRVisitor ); } - override void visit(Expression) @nogc + override void visit(Operand operand) @nogc { - } - - override void visit(Statement statement) @nogc - { - statement.expression.accept(this); + if ((cast(Variable) operand) !is null) + { + return (cast(Variable) operand).accept(this); + } + if ((cast(Number) operand) !is null) + { + return (cast(Number) operand).accept(this); + } } override void visit(Variable variable) @nogc @@ -334,10 +344,12 @@ Symbol writeNext(Definition ast) @nogc { Array!Instruction instructions; Array!Reference references; - auto visitor = defaultAllocator.make!RiscVVisitor; + auto visitor = cast(RiscVVisitor) malloc(__traits(classInstanceSize, RiscVVisitor)); + (cast(void*) visitor)[0 .. __traits(classInstanceSize, RiscVVisitor)] = __traits(initSymbol, RiscVVisitor)[]; scope (exit) { - defaultAllocator.dispose(visitor); + visitor.__xdtor(); + free(cast(void*) visitor); } visitor.visit(ast); diff --git a/tests/7_member_sum.eln b/tests/7_member_sum.eln new file mode 100644 index 0000000..a54a60a --- /dev/null +++ b/tests/7_member_sum.eln @@ -0,0 +1 @@ +! 3 + 4 + 5 + 1 + 2 + 4 + 3 diff --git a/tests/expectations/7_member_sum.txt b/tests/expectations/7_member_sum.txt new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/tests/expectations/7_member_sum.txt @@ -0,0 +1 @@ +22 diff --git a/tests/expectations/print_number.txt b/tests/expectations/print_number.txt new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/tests/expectations/print_number.txt @@ -0,0 +1 @@ +3 diff --git a/tests/print_number.eln b/tests/print_number.eln new file mode 100644 index 0000000..51b5ca0 --- /dev/null +++ b/tests/print_number.eln @@ -0,0 +1,2 @@ +! 3 +. diff --git a/tests/runner.cpp b/tests/runner.cpp new file mode 100755 index 0000000..2c15737 --- /dev/null +++ b/tests/runner.cpp @@ -0,0 +1,103 @@ +#include + +#include +#include + +class test_results final +{ + std::uint32_t m_total{ 0 }; + std::uint32_t m_passed{ 0 }; + +public: + test_results() = default; + + std::uint32_t total() const noexcept + { + return m_total; + } + + std::uint32_t passed() const noexcept + { + return m_passed; + } + + std::uint32_t failed() const noexcept + { + return m_total - m_passed; + } + + int exit_code() const noexcept + { + return m_total == m_passed ? EXIT_SUCCESS : EXIT_FAILURE; + } + + void add_exit_code(const int exit_code) noexcept + { + ++m_total; + if (exit_code == 0) + { + ++m_passed; + } + } +}; + +int run_test(const std::filesystem::directory_entry& test_entry) +{ + const std::filesystem::path test_filename = test_entry.path().filename(); + + std::filesystem::path test_binary = "build/riscv" / test_filename; + test_binary.replace_extension(); + + std::filesystem::path expectation_path = test_entry.path().parent_path() / "expectations" / test_filename; + expectation_path.replace_extension(".txt"); + + std::cout << "Running " << test_binary << std::endl; + + boost::process::pipe pipe_stream; + boost::process::child vm( + "/opt/riscv/bin/spike", "--isa=RV32IMAC", + "/opt/riscv/riscv32-unknown-elf/bin/pk", + test_binary.string(), + boost::process::std_out > pipe_stream + ); + boost::process::child diff( + "/usr/bin/diff", "-Nur", "--color", + expectation_path.string(), "-", + boost::process::std_in < pipe_stream + ); + vm.wait(); + diff.wait(); + + return diff.exit_code(); +} + +test_results run_in_path(const std::filesystem::path test_directory) +{ + test_results results; + + for (const auto& test_entry : std::filesystem::directory_iterator(test_directory)) + { + if (test_entry.path().extension() != ".eln" || !test_entry.is_regular_file()) + { + continue; + } + results.add_exit_code(run_test(test_entry)); + } + return results; +} + +int main() +{ + boost::process::system("rake"); + + std::cout << "Run all tests and check the results" << std::endl; + std::filesystem::path test_directory{ "tests" }; + const auto results = run_in_path(test_directory); + + std::cout << std::endl; + std::cout << results.total() << " tests run, " + << results.passed() << " passed, " + << results.failed() << " failed." << std::endl; + + return results.exit_code(); +}