Grow stack automatically

This commit is contained in:
Eugen Wissner 2024-02-18 11:26:57 +01:00
parent df2494e145
commit 86d579e8d5
Signed by: belka
GPG Key ID: A27FDC1E8EE902C0
12 changed files with 262 additions and 146 deletions

5
CMakeLists.txt Normal file
View File

@ -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)

8
README
View File

@ -35,3 +35,11 @@ factor = ident | number | "(" expression ")";
"!" - Write a line. "!" - Write a line.
"?" - Read user input. "?" - Read user input.
"odd" - The only function, returns whether a number is odd. "odd" - The only function, returns whether a number is odd.
# Build and test
```sh
cmake -B build
make -C build
./build/bin/tester
```

View File

@ -39,40 +39,9 @@ file BINARY => SOURCES do |t|
sh({ 'DFLAGS' => (DFLAGS * ' ') }, 'dub', 'build', '--compiler=gdc') sh({ 'DFLAGS' => (DFLAGS * ' ') }, 'dub', 'build', '--compiler=gdc')
end end
task default: TESTS
task default: BINARY 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' desc 'Run unittest blocks'
task unittest: SOURCES do |t| task unittest: SOURCES do |t|
sh('dub', 'test', '--compiler=gdc-12') sh('dub', 'test', '--compiler=gdc-12')

8
TODO Normal file
View File

@ -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.

View File

@ -15,34 +15,35 @@ struct ASTMapping
alias Node = .Node; alias Node = .Node;
alias Definition = .Definition; alias Definition = .Definition;
alias VariableDeclaration = .VariableDeclaration; alias VariableDeclaration = .VariableDeclaration;
alias Statement = Array!(.Statement); alias Statement = .Operand;
alias BangStatement = .Expression; alias BangStatement = .Operand;
alias Block = .Definition; alias Block = .Definition;
alias Expression = .Expression; alias Expression = .Operand;
alias Number = .Number; alias Number = .Number;
alias Variable = .Variable; alias Variable = .Number;
alias BinaryExpression = .BinaryExpression; alias BinaryExpression = .Variable;
} }
/** /**
* IR visitor. * IR visitor.
*/ */
extern(C++)
interface IRVisitor interface IRVisitor
{ {
abstract void visit(Node) @nogc; abstract void visit(Node) @nogc;
abstract void visit(Definition) @nogc; abstract void visit(Definition) @nogc;
abstract void visit(Expression) @nogc; abstract void visit(Operand) @nogc;
abstract void visit(Statement) @nogc; abstract void visit(BinaryExpression) @nogc;
abstract void visit(Variable) @nogc; abstract void visit(Variable) @nogc;
abstract void visit(VariableDeclaration) @nogc; abstract void visit(VariableDeclaration) @nogc;
abstract void visit(Number) @nogc; abstract void visit(Number) @nogc;
abstract void visit(BinaryExpression) @nogc;
} }
/** /**
* AST node. * AST node.
*/ */
extern(C++)
abstract class Node abstract class Node
{ {
abstract void accept(IRVisitor) @nogc; abstract void accept(IRVisitor) @nogc;
@ -51,11 +52,13 @@ abstract class Node
/** /**
* Definition. * Definition.
*/ */
extern(C++)
class Definition : Node class Definition : Node
{ {
char[] identifier; char[] identifier;
Array!Statement statements; Array!BinaryExpression statements;
Array!VariableDeclaration variableDeclarations; Array!VariableDeclaration variableDeclarations;
Operand result;
override void accept(IRVisitor visitor) @nogc 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 override void accept(IRVisitor visitor) @nogc
{ {
@ -81,7 +80,8 @@ abstract class Expression : Node
} }
} }
class Number : Expression extern(C++)
class Number : Operand
{ {
int value; int value;
@ -91,7 +91,8 @@ class Number : Expression
} }
} }
class Variable : Expression extern(C++)
class Variable : Operand
{ {
size_t counter; size_t counter;
@ -101,6 +102,7 @@ class Variable : Expression
} }
} }
extern(C++)
class VariableDeclaration : Node class VariableDeclaration : Node
{ {
String identifier; 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; BinaryOperator operator;
this(Expression lhs, Expression rhs, BinaryOperator operator) this(Operand lhs, Operand rhs, BinaryOperator operator)
@nogc @nogc
{ {
this.lhs = lhs; 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 final class TransformVisitor : parser.ParserVisitor!ASTMapping
{ {
private HashTable!(String, int) constants; private HashTable!(String, int) constants;
private Array!BinaryExpression statements;
ASTMapping.Node visit(parser.Node node) @nogc ASTMapping.Node visit(parser.Node node) @nogc
{ {
@ -151,7 +171,7 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping
ASTMapping.BangStatement visit(parser.BangStatement statement) @nogc ASTMapping.BangStatement visit(parser.BangStatement statement) @nogc
{ {
assert(false, "Not implemented"); return statement.expression.accept(this);
} }
ASTMapping.Block visit(parser.Block block) @nogc ASTMapping.Block visit(parser.Block block) @nogc
@ -159,7 +179,9 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping
auto target = defaultAllocator.make!Definition; auto target = defaultAllocator.make!Definition;
this.constants = transformConstants(block.definitions); 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); target.variableDeclarations = transformVariableDeclarations(block.variableDeclarations);
return target; return target;
@ -167,41 +189,45 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping
ASTMapping.Expression visit(parser.Expression expression) @nogc 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 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 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 ASTMapping.BinaryExpression visit(parser.BinaryExpression 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
{ {
auto target = defaultAllocator.make!BinaryExpression( auto target = defaultAllocator.make!BinaryExpression(
expression(binaryExpression.lhs, statements), binaryExpression.lhs.accept(this),
expression(binaryExpression.rhs, statements), binaryExpression.rhs.accept(this),
binaryExpression.operator binaryExpression.operator
); );
statements.insertBack(target);
auto newStatement = defaultAllocator.make!Statement;
newStatement.expression = target;
statements.insertBack(newStatement);
auto newVariable = defaultAllocator.make!Variable; auto newVariable = defaultAllocator.make!Variable;
newVariable.counter = statements.length; newVariable.counter = statements.length;
@ -209,38 +235,18 @@ final class TransformVisitor : parser.ParserVisitor!ASTMapping
return newVariable; return newVariable;
} }
private Expression expression(parser.Expression expression, private Number transformNumber(parser.Number number) @nogc
ref Array!Statement statements) @nogc
{ {
if ((cast(parser.Number) expression) !is null) return defaultAllocator.make!Number(number.value);
{
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;
} }
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) 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 private HashTable!(String, int) transformConstants(ref Array!(parser.Definition) definitions) @nogc

View File

@ -8,6 +8,7 @@ import std.range;
import tanya.container.array; import tanya.container.array;
import tanya.container.string; import tanya.container.string;
extern(C++)
struct Token struct Token
{ {
enum Type enum Type
@ -71,7 +72,7 @@ struct Token
} }
@property auto value(Type type)() @nogc nothrow pure @trusted @property auto value(Type type)() @nogc nothrow pure @trusted
in (ofType(type)) in (ofType(type), "Expected type: " ~ type.stringof)
{ {
static if (type == Type.number) static if (type == Type.number)
{ {
@ -99,14 +100,17 @@ struct Token
/** /**
* Range over the source text that keeps track of the current position. * Range over the source text that keeps track of the current position.
*/ */
extern(C++)
struct Source struct Source
{ {
char[] buffer; char* buffer_;
size_t length_;
Position position; 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(); @disable this();
@ -116,54 +120,50 @@ struct Source
return this.length == 0; return this.length == 0;
} }
char front() @nogc nothrow pure @safe char front() @nogc nothrow pure
in (!empty) in (!empty)
{ {
return this.buffer[0]; return this.buffer_[0];
} }
void popFront() @nogc nothrow pure @safe void popFront() @nogc nothrow pure
in (!empty) in (!empty)
{ {
this.buffer = buffer[1 .. $]; ++this.buffer_;
--this.length_;
++this.position.column; ++this.position.column;
} }
void breakLine() @nogc nothrow pure @safe void breakLine() @nogc nothrow pure
in (!empty) in (!empty)
{ {
this.buffer = buffer[1 .. $]; ++this.buffer_;
--this.length_;
++this.position.line; ++this.position.line;
this.position.column = 1; this.position.column = 1;
} }
@property size_t length() const @nogc nothrow pure @safe @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) in (index < length)
{ {
return this.buffer[index]; return this.buffer_[index];
} }
char[] opSlice(size_t i, size_t j) @nogc nothrow pure @safe char* buffer() @nogc nothrow pure
in
{ {
assert(i <= j); return this.buffer_;
assert(j <= length);
}
do
{
return this.buffer[i .. j];
} }
} }
Result!(Array!Token) lex(char[] buffer) @nogc Result!(Array!Token) lex(char[] buffer) @nogc
{ {
Array!Token tokens; Array!Token tokens;
auto source = Source(buffer); auto source = Source(buffer.ptr, buffer.length);
while (!source.empty) while (!source.empty)
{ {
@ -218,17 +218,17 @@ Result!(Array!Token) lex(char[] buffer) @nogc
{ {
++i; ++i;
} }
if (source[0 .. i] == "const") if (source.buffer[0 .. i] == "const")
{ {
tokens.insertBack(Token(Token.Type.let, source.position)); 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)); tokens.insertBack(Token(Token.Type.var, source.position));
} }
else else
{ {
auto identifier = String(source[0 .. i]); auto identifier = String(source.buffer[0 .. i]);
tokens.insertBack(Token(Token.Type.identifier, identifier, source.position)); tokens.insertBack(Token(Token.Type.identifier, identifier, source.position));
} }
source.popFrontN(i); source.popFrontN(i);

View File

@ -1,5 +1,6 @@
module elna.riscv; module elna.riscv;
import core.stdc.stdlib;
import elna.extended; import elna.extended;
import elna.ir; import elna.ir;
import elna.result; import elna.result;
@ -7,7 +8,6 @@ import std.algorithm;
import std.typecons; import std.typecons;
import tanya.container.array; import tanya.container.array;
import tanya.container.string; import tanya.container.string;
import tanya.memory.allocator;
enum XRegister : ubyte enum XRegister : ubyte
{ {
@ -140,14 +140,14 @@ struct Instruction
return this; 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 return scope @nogc
{ {
this.instruction |= (imm1 << 7) this.instruction |= ((imm1 & 0b11111) << 7)
| (funct3 << 12) | (funct3 << 12)
| (rs1 << 15) | (rs1 << 15)
| (rs2 << 20) | (rs2 << 20)
| (imm2 << 25); | ((imm1 & 0b111111100000) << 20);
return this; return this;
} }
@ -178,6 +178,7 @@ struct Instruction
} }
} }
extern(C++)
class RiscVVisitor : IRVisitor class RiscVVisitor : IRVisitor
{ {
Array!Instruction instructions; Array!Instruction instructions;
@ -191,22 +192,24 @@ class RiscVVisitor : IRVisitor
override void visit(Definition definition) @nogc override void visit(Definition definition) @nogc
{ {
const uint stackSize = cast(uint) (definition.statements.length * 4 + 12);
// Prologue. // Prologue.
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.opImm) Instruction(BaseOpcode.opImm)
.i(XRegister.sp, Funct3.addi, XRegister.sp, cast(uint) -32) .i(XRegister.sp, Funct3.addi, XRegister.sp, -stackSize)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.store) Instruction(BaseOpcode.store)
.s(28, Funct3.sw, XRegister.sp, XRegister.s0) .s(stackSize - 4, Funct3.sw, XRegister.sp, XRegister.s0)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.store) Instruction(BaseOpcode.store)
.s(24, Funct3.sw, XRegister.sp, XRegister.ra) .s(stackSize - 8, Funct3.sw, XRegister.sp, XRegister.ra)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.opImm) Instruction(BaseOpcode.opImm)
.i(XRegister.s0, Funct3.addi, XRegister.sp, 32) .i(XRegister.s0, Funct3.addi, XRegister.sp, stackSize)
); );
foreach (statement; definition.statements[]) foreach (statement; definition.statements[])
@ -217,6 +220,10 @@ class RiscVVisitor : IRVisitor
{ {
variableDeclaration.accept(this); variableDeclaration.accept(this);
} }
this.registerInUse = true;
definition.result.accept(this);
this.registerInUse = false;
// Print the result. // Print the result.
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.opImm) Instruction(BaseOpcode.opImm)
@ -247,15 +254,15 @@ class RiscVVisitor : IRVisitor
// Epilogue. // Epilogue.
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.load) Instruction(BaseOpcode.load)
.i(XRegister.s0, Funct3.lw, XRegister.sp, 28) .i(XRegister.s0, Funct3.lw, XRegister.sp, stackSize - 4)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.load) Instruction(BaseOpcode.load)
.i(XRegister.ra, Funct3.lw, XRegister.sp, 24) .i(XRegister.ra, Funct3.lw, XRegister.sp, stackSize - 8)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.opImm) Instruction(BaseOpcode.opImm)
.i(XRegister.sp, Funct3.addi, XRegister.sp, 32) .i(XRegister.sp, Funct3.addi, XRegister.sp, stackSize)
); );
this.instructions.insertBack( this.instructions.insertBack(
Instruction(BaseOpcode.jalr) Instruction(BaseOpcode.jalr)
@ -263,13 +270,16 @@ class RiscVVisitor : IRVisitor
); );
} }
override void visit(Expression) @nogc override void visit(Operand operand) @nogc
{ {
} if ((cast(Variable) operand) !is null)
{
override void visit(Statement statement) @nogc return (cast(Variable) operand).accept(this);
{ }
statement.expression.accept(this); if ((cast(Number) operand) !is null)
{
return (cast(Number) operand).accept(this);
}
} }
override void visit(Variable variable) @nogc override void visit(Variable variable) @nogc
@ -334,10 +344,12 @@ Symbol writeNext(Definition ast) @nogc
{ {
Array!Instruction instructions; Array!Instruction instructions;
Array!Reference references; 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) scope (exit)
{ {
defaultAllocator.dispose(visitor); visitor.__xdtor();
free(cast(void*) visitor);
} }
visitor.visit(ast); visitor.visit(ast);

1
tests/7_member_sum.eln Normal file
View File

@ -0,0 +1 @@
! 3 + 4 + 5 + 1 + 2 + 4 + 3

View File

@ -0,0 +1 @@
22

View File

@ -0,0 +1 @@
3

2
tests/print_number.eln Normal file
View File

@ -0,0 +1,2 @@
! 3
.

103
tests/runner.cpp Executable file
View File

@ -0,0 +1,103 @@
#include <boost/process.hpp>
#include <iostream>
#include <filesystem>
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();
}