diff --git a/Makefile b/Makefile index ccf9b59..8139e55 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ obj/vm.o: src/vm/vm.cpp src/vm/vm.h src/number.h # The specs are built with Catch, # a cool C++ testing framework. -VM_SPECS=obj/vm/bool.spec.o obj/vm/number.spec.o obj/vm/load.spec.o obj/vm/call.spec.o obj/vm/eq.spec.o obj/vm/when.spec.o +VM_SPECS=obj/vm/bool.spec.o obj/vm/number.spec.o obj/vm/load.spec.o obj/vm/call.spec.o obj/vm/eq.spec.o obj/vm/when.spec.o obj/vm/block.spec.o E2E_SPECS=obj/e2e/bool.spec.o obj/e2e/language.spec.o obj/e2e/number.spec.o obj/e2e/value.spec.o obj/e2e/def.spec.o obj/e2e/block.spec.o tmp/spec: spec/spec.cpp $(VM_SPECS) $(E2E_SPECS) tmp/cli.o obj/exec.o obj/parser.o tmp/parse.o obj/codegen.o obj/bytecode.o obj/vm.o tmp/lex.cpp diff --git a/spec/e2e/block.spec.cpp b/spec/e2e/block.spec.cpp index edfc91e..c0ef63e 100644 --- a/spec/e2e/block.spec.cpp +++ b/spec/e2e/block.spec.cpp @@ -1,12 +1,12 @@ #include "spec/spec.h" -// TEST_CASE("Block set/get") { -// REQUIRE(resultOf("x = {}; x") == "{}"); -// REQUIRE(resultOf("x = { a = 1 }; x.a") == "1"); -// } +TEST_CASE("Block set/get") { + REQUIRE(resultOf("x = {}; x")->toString() == "{}"); + REQUIRE(resultOf("x = { a = 1 }; x.a")->asNumber() == fn::Number(0, 1)); +} // TEST_CASE("Assignment outside of block") { -// REQUIRE(resultOf("x = {}; x.y = 1; x.y") == "1"); +// REQUIRE(resultOf("x = {}; x.y = 1; x.y")->asNumber() == fn::Number(0, 1)); // } // TEST_CASE("Nested blocks") { diff --git a/spec/e2e/def.spec.cpp b/spec/e2e/def.spec.cpp index 22e9b3c..66a9be9 100644 --- a/spec/e2e/def.spec.cpp +++ b/spec/e2e/def.spec.cpp @@ -21,7 +21,7 @@ TEST_CASE("Fn set/call") { // } // x.y(1) -// )") == "2"); +// )")->asNumber() == fn::Number(0, 2)); // } // TEST_CASE("Fn call within block") { @@ -31,7 +31,7 @@ TEST_CASE("Fn set/call") { // } // (x(1)).foo(1) -// )") == "2"); +// )")->asNumber() == fn::Number(0, 2)); // } // There was a time when we weren't passing params diff --git a/spec/vm/block.spec.cpp b/spec/vm/block.spec.cpp new file mode 100644 index 0000000..ea71d2d --- /dev/null +++ b/spec/vm/block.spec.cpp @@ -0,0 +1,34 @@ +#include "spec/spec.h" + +TEST_CASE("NEW_FRAME COMPRESS returns to original frame") { + bytecode::CodeBlob instructions = bytecode::CodeBlob{ + bytecode::iTrue(), + bytecode::iNewFrame(), + bytecode::iCompress() + }; + + REQUIRE(resultOf(instructions)->asBool() == true); +} + +TEST_CASE("NEW_FRAME COMPRESS EXPAND uses new frame") { + bytecode::CodeBlob instructions = bytecode::CodeBlob{ + bytecode::iNewFrame(), + bytecode::iTrue(), + bytecode::iCompress(), + bytecode::iExpand() + }; + + REQUIRE(resultOf(instructions)->asBool() == true); +} + +TEST_CASE("Dereference") { + bytecode::CodeBlob instructions = bytecode::CodeBlob{ + bytecode::iNewFrame(), + bytecode::iTrue(), + bytecode::iCompress(), + bytecode::iExpand(), + bytecode::iReturnLast() + }; + + REQUIRE(resultOf(instructions)->asBool() == true); +} diff --git a/src/ast.h b/src/ast.h index 978a2ba..a91147f 100644 --- a/src/ast.h +++ b/src/ast.h @@ -117,6 +117,36 @@ namespace fn { namespace ast { } }; + // Represents a collection of statements which, + // when executed, return a value. + // + // They can be thought of as zero-arity, immediately executed + // function closures, if you're feeling masochistic. + class BlockValue : public Value { + public: + std::vector statements; + + BlockValue(std::vector statements) { this->statements = statements; } + BlockValue() { this->statements = std::vector(); } + ~BlockValue() { + for(auto statement : this->statements) { delete statement; } + } + + std::string asString(int indent) override { + std::string result = "(BLOCKVALUE [\n"; + + for(auto statement: this->statements) { + result += std::string(indent + 2, ' ') + + statement->asString(indent + 2) + + "\n"; + } + + result += std::string(indent, ' ') + "])"; + + return result; + } + }; + // Represents a boolean value. class Bool : public Value { public: diff --git a/src/bytecode.cpp b/src/bytecode.cpp index dbd326b..668aee5 100644 --- a/src/bytecode.cpp +++ b/src/bytecode.cpp @@ -67,4 +67,8 @@ namespace fn { namespace bytecode { CodeBlob iJumpIfLastFalse(InstructionIndex jump) { return CodeBlob{FN_OP_FALSE_JUMP, jump}; } + CodeBlob iNewFrame() { return CodeBlob{FN_OP_NEW_FRAME}; } + CodeBlob iCompress() { return CodeBlob{FN_OP_COMPRESS}; } + CodeBlob iExpand() { return CodeBlob{FN_OP_EXPAND}; } + }} diff --git a/src/bytecode.h b/src/bytecode.h index d176791..bc7891c 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -48,6 +48,10 @@ namespace fn { namespace bytecode { #define FN_OP_FALSE_JUMP (fn::bytecode::OpCode)(60) + #define FN_OP_NEW_FRAME (fn::bytecode::OpCode)(70) + #define FN_OP_COMPRESS (fn::bytecode::OpCode)(71) + #define FN_OP_EXPAND (fn::bytecode::OpCode)(72) + // Instruction references are given by this type. // TODO: Expand to 32-bit. @@ -142,4 +146,8 @@ namespace fn { namespace bytecode { CodeBlob iJumpIfLastFalse(InstructionIndex jump); + CodeBlob iNewFrame(); + CodeBlob iCompress(); + CodeBlob iExpand(); + }} diff --git a/src/codegen/codegen.cpp b/src/codegen/codegen.cpp index ca27362..8f95e06 100644 --- a/src/codegen/codegen.cpp +++ b/src/codegen/codegen.cpp @@ -21,6 +21,7 @@ bytecode::CodeBlob CodeGenerator::digest(ast::Statement* statement) { try_digest_as(ast::Deref); try_digest_as(ast::Assignment); try_digest_as(ast::Block); + try_digest_as(ast::BlockValue); try_digest_as(ast::Bool); try_digest_as(ast::Number); try_digest_as(ast::String); @@ -37,13 +38,11 @@ bytecode::CodeBlob CodeGenerator::digest(ast::Id* id) { } bytecode::CodeBlob CodeGenerator::digest(ast::Deref* deref) { - // bytecode::CodeBlob blob = this->digest(deref->parent); - // TODO: Push scope! - // blob.append(this->digest(deref->child)); - // TODO: Pop scope! - // return blob; - - return this->digest(deref->child); + bytecode::CodeBlob blob = this->digest(deref->parent); + blob.append(bytecode::iExpand()); + blob.append(this->digest(deref->child)); + blob.append(bytecode::iReturnLast()); + return blob; } bytecode::CodeBlob CodeGenerator::digest(ast::Assignment* assignment) { @@ -72,13 +71,19 @@ bytecode::CodeBlob CodeGenerator::digest(ast::Assignment* assignment) { bytecode::CodeBlob CodeGenerator::digest(ast::Block* block) { bytecode::CodeBlob blockBlob = bytecode::CodeBlob(); - - // TODO: Push new scope? for (auto statement : block->statements) { blockBlob.append(this->digest(statement)); } - // TODO: Pop scope into variable? + return blockBlob; +} +bytecode::CodeBlob CodeGenerator::digest(ast::BlockValue* block) { + bytecode::CodeBlob blockBlob = bytecode::CodeBlob(); + blockBlob.append(bytecode::iNewFrame()); + for (auto statement : block->statements) { + blockBlob.append(this->digest(statement)); + } + blockBlob.append(bytecode::iCompress()); return blockBlob; } diff --git a/src/codegen/codegen.h b/src/codegen/codegen.h index d8c056c..724597e 100644 --- a/src/codegen/codegen.h +++ b/src/codegen/codegen.h @@ -32,6 +32,7 @@ namespace fn { bytecode::CodeBlob digest(ast::Deref* deref); bytecode::CodeBlob digest(ast::Assignment* assignment); bytecode::CodeBlob digest(ast::Block* block); + bytecode::CodeBlob digest(ast::BlockValue* block); bytecode::CodeBlob digest(ast::Bool* b); bytecode::CodeBlob digest(ast::Number* n); bytecode::CodeBlob digest(ast::String* s); diff --git a/src/parser/bison.cpp b/src/parser/bison.cpp index 45d2001..d198890 100644 --- a/src/parser/bison.cpp +++ b/src/parser/bison.cpp @@ -24,6 +24,7 @@ // AST elements fn::ast::Block* v_block; + fn::ast::BlockValue* v_blockvalue; fn::ast::Statement* v_statement; fn::ast::Reference* v_reference; fn::ast::Id* v_id; @@ -49,6 +50,7 @@ // Non-terminal symbols. %type program block +%type blockvalue %type statements %type statement %type reference @@ -106,7 +108,7 @@ | infixOperation | reference | functionCall -| block +| blockvalue | functionDef | conditional ; @@ -180,6 +182,12 @@ delete $2; } +blockvalue: + '{' statements '}' { + $$ = new fn::ast::BlockValue(*$2); + delete $2; + } + conditional: TWHEN '{' conditions '}' { $$ = new fn::ast::Conditional(*$3); diff --git a/src/vm/callframe.h b/src/vm/callframe.h new file mode 100644 index 0000000..69f7076 --- /dev/null +++ b/src/vm/callframe.h @@ -0,0 +1,54 @@ +// The Fn VM is a basic, hand-made virtual machine. +// Using a handful of VM instructions, +// this VM is capable of running any Fn program. + +#pragma once + +#include // std::vector +#include // std::unordered_map + +#define INITIAL_VALUE_MAP_CAPACITY 4 +#define INITIAL_SYMBOL_TABLE_CAPACITY 16 + +namespace fn { + namespace vm { + typedef std::vector ValueMap; + typedef std::unordered_map SymbolTable; + + class CallFrame { + public: + // Denotes the place the program counter will be set to + // when RETURN_LAST is hit. + bytecode::InstructionIndex returnCounter; + + // The values defined in the given frame. + vm::ValueMap values; + + // The symbols defined in the given frame. + vm::SymbolTable symbols; + + CallFrame(bytecode::InstructionIndex returnCounter) : CallFrame() { + this->returnCounter = returnCounter; + } + + CallFrame() { + // The [] operator returns 0 if a value + // is not found. To make sure that's not + // confused with the first element, we push + // a NULL value to the 0 position. + this->values = vm::ValueMap(); + this->values.reserve(INITIAL_VALUE_MAP_CAPACITY); + this->values.push_back(NULL); + + this->symbols = vm::SymbolTable(); + this->symbols.reserve(INITIAL_SYMBOL_TABLE_CAPACITY); + } + + ~CallFrame() { + for(auto value : this->values) { + if (value != NULL) { delete value; } + } + } + }; + } +} diff --git a/src/vm/value.h b/src/vm/value.h index 606a1c4..066d560 100644 --- a/src/vm/value.h +++ b/src/vm/value.h @@ -9,6 +9,9 @@ namespace fn { namespace vm { + // Forward declaration + class CallFrame; + class Value { public: virtual ~Value() {}; @@ -17,6 +20,7 @@ namespace fn { namespace vm { virtual bytecode::InstructionIndex getCallCounterPos() { throw "Not callable!"; } virtual bool isDef() { return false; } virtual Def asDef() { throw "Not a Def!"; } + virtual CallFrame* asCallFrame() { throw "Not a CallFrame!"; } virtual bool eq(Value* other) = 0; virtual std::string toString() = 0; }; @@ -77,4 +81,19 @@ namespace fn { namespace vm { } }; + class CallFrameValue : public Value { + protected: + CallFrame* value; + public: + CallFrameValue(CallFrame* value) : value(value) {} + ~CallFrameValue() = default; + CallFrame* asCallFrame() override { return this->value; } + std::string toString() override { + return "{}"; + } + bool eq(Value* other) override { + return false; + } + }; + }} diff --git a/src/vm/vm.cpp b/src/vm/vm.cpp index ac8a0fb..4f2093f 100644 --- a/src/vm/vm.cpp +++ b/src/vm/vm.cpp @@ -107,6 +107,21 @@ vm::Value* VM::run(bytecode::CodeByte instructions[], size_t num_bytes) { // counter is moved by jumpIfLastFalse break; + case FN_OP_NEW_FRAME: + this->newFrame(); + this->counter += 1; + break; + + case FN_OP_COMPRESS: + this->compress(); + this->counter += 1; + break; + + case FN_OP_EXPAND: + this->expand(); + this->counter += 1; + break; + default: throw "Unexpected opcode"; // TODO: Make this more meaningful @@ -337,17 +352,19 @@ void VM::load(bytecode::CodeByte value[]) { // Check each frame, top to bottom. vm::Value* foundValue = NULL; for(auto frame = this->callStack.rbegin(); frame != this->callStack.rend(); ++frame) { - if ((*frame)->symbols[name] != NULL) { - foundValue = (*frame)->symbols[name]; - break; + foundValue = (*frame)->symbols[name]; + if (foundValue != NULL) { + // Cache the value of the requested symbol. + this->currentFrame->symbols[name] = foundValue; + break; } } - if (foundValue != NULL) { - this->pushValue(foundValue); - } else { + if (foundValue == NULL) { throw "Cannot find variable"; } + + this->pushValue(foundValue); } // DECLARE_DEF [LENGTH (1)] [NUM_ARGS (1)] [ARG_NAMES (1)*] @@ -391,8 +408,7 @@ void VM::call() { vm::Def def = this->popValue()->asDef(); // Push a CallFrame onto the stack. - vm::CallFrame* frame = new vm::CallFrame(); - frame->returnCounter = this->counter + 1; + vm::CallFrame* frame = new vm::CallFrame(this->counter + 1); // Set up ValueStack and SymbolTable std::vector argNames = def.args; @@ -452,3 +468,34 @@ void VM::jumpIfLastFalse(bytecode::CodeByte value[]) { DEBUG("FALSE_JUMP(" << std::to_string(jump) << ") = NO JUMP"); this->counter += FALSE_JUMP_BYTES; } + +// NEW_FRAME +// (1 byte) +// +// Push a new frame onto the frame stack. +void VM::newFrame() { + DEBUG("NEW_FRAME"); + this->pushFrame(new vm::CallFrame()); +} + +// COMPRESS +// (1 byte) +// +// Pop the current frame and push it on the value stack. +void VM::compress() { + DEBUG("COMPRESS"); + vm::CallFrame* compressedFrame = this->currentFrame; + this->popFrame(); + this->currentFrame->values.push_back(new vm::CallFrameValue(compressedFrame)); +} + +// EXPAND +// (1 byte) +// +// Pop the value stack and push it onto the frame stack. +void VM::expand() { + DEBUG("EXPAND"); + vm::CallFrame* expandedFrame = this->currentFrame->values.back()->asCallFrame(); + this->currentFrame->values.pop_back(); + this->pushFrame(expandedFrame); +} diff --git a/src/vm/vm.h b/src/vm/vm.h index 41230c9..4df496a 100644 --- a/src/vm/vm.h +++ b/src/vm/vm.h @@ -5,52 +5,18 @@ #pragma once #include "stdlib.h" // size_t -#include // std::vector -#include // std::stack -#include // std::unordered_map #include "src/bytecode.h" // CodeByte #include "src/vm/value.h" // vm::Value #include "src/vm/def.h" // vm::Def #include "src/number.h" // Number -namespace fn { - - namespace vm { - typedef std::vector ValueMap; - typedef std::unordered_map SymbolTable; - - class CallFrame { - public: - // Denotes the place the program counter will be set to - // when RETURN_LAST is hit. - bytecode::InstructionIndex returnCounter; - - // The values defined in the given frame. - vm::ValueMap values; - - // The symbols defined in the given frame. - vm::SymbolTable symbols; +#include "src/vm/callframe.h" // vm::CallFrame - CallFrame(bytecode::InstructionIndex returnCounter) { - this->returnCounter = returnCounter; +#define INITIAL_VALUE_MAP_CAPACITY 4 +#define INITIAL_SYMBOL_TABLE_CAPACITY 16 - // The [] operator returns 0 if a value - // is not found. To make sure that's not - // confused with the first element, we push - // a NULL value to the 0 position. - this->values = vm::ValueMap{ NULL }; - } - - CallFrame() : CallFrame(-1) {} - - ~CallFrame() { - for(auto value : this->values) { - if (value != NULL) { delete value; } - } - } - }; - } +namespace fn { // VM is the virtual machine that instructions are run in. class VM { @@ -109,6 +75,11 @@ namespace fn { void returnLast(); void jumpIfLastFalse(bytecode::CodeByte[]); + + // Frame manipulation. + void newFrame(); + void compress(); + void expand(); }; }