Skip to content

Commit

Permalink
Add move semantics for heap specifier (#609)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcauberer authored Jul 22, 2024
1 parent c56e451 commit 2561ef2
Show file tree
Hide file tree
Showing 24 changed files with 253 additions and 168 deletions.
2 changes: 1 addition & 1 deletion .run/spice build.run.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="spice build" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="build -O0 -d -g ../../media/test-project/test.spice" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="Spice" TARGET_NAME="spice" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="Spice" RUN_TARGET_NAME="spice">
<configuration default="false" name="spice build" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="build -O0 -d -ir -g ../../media/test-project/test.spice" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="Spice" TARGET_NAME="spice" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="Spice" RUN_TARGET_NAME="spice">
<envs>
<env name="LLVM_ADDITIONAL_FLAGS" value="-lole32 -lws2_32" />
<env name="LLVM_BUILD_INCLUDE_DIR" value="$PROJECT_DIR$/../llvm-project-latest/build/include" />
Expand Down
13 changes: 8 additions & 5 deletions media/specs/heap-specifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
- [x] Add tests for this feature
- [x] Generate default dtor for structs with heap fields where free is called on those fields
- [x] Make copy ctor mandatory for structs with heap fields
- [ ] Allow assignment from heap to non-heap pointers to create non-owning pointers
- [ ] Perform a swap when assigning from heap to heap pointers
- [x] Allow assignment from heap to non-heap pointers to create non-owning pointers
- [ ] Perform an ownership transfer when assigning from heap to heap pointers
- [ ] Deallocate local heap variables with sDealloc when they go out of scope

## Syntax

Expand All @@ -20,7 +21,7 @@ type TestStruct struct {
// As local variable
f<int> foo() {
heap int* ptr = sAlloc(sizeof(type int));
heap int* ptr = sNew<int>();
*ptr = 5;
return ptr;
}
Expand Down Expand Up @@ -53,9 +54,11 @@ When assigning a `heap` pointer to a non-heap pointer, the non-heap pointer beco
that the non-heap pointer does not have to free its memory, as it does not own it. The memory is still owned by the
`heap` pointer, and the memory will be freed when the `heap` pointer goes out of scope.

When assigning a `heap` pointer to a reference to a `heap` pointer, it results in a non-owning reference.

When assigning a `heap` pointer to another `heap` pointer, the memory is not copied, but the ownership gets
transferred to the left hand side variable. This means that the original `heap` pointer will get the value `nil` and is
no longer responsible for freeing the memory. This is done to prevent double freeing of the memory.
transferred to the left hand side variable. This means that the original `heap` variable is no longer responsible for
freeing the memory, instead the new `heap` variable is. This is done to prevent double freeing of the underlying memory.

Assigning a non-heap pointer to a `heap` pointer is not allowed, as the non-heap pointer does not own the memory which
it points to.
Expand Down
115 changes: 57 additions & 58 deletions media/test-project/test.spice
Original file line number Diff line number Diff line change
@@ -1,66 +1,65 @@
import "std/data/map";
import "std/data/binary-tree";

f<int> main() {
Map<int, string> map;
assert map.getSize() == 0l;
assert map.isEmpty();
map.insert(1, "Hello");
assert map.getSize() == 1l;
assert !map.isEmpty();
map.insert(2, "World");
assert map.getSize() == 2l;
map.insert(3, "Foo");
assert map.getSize() == 3l;
map.insert(4, "Bar");
assert map.getSize() == 4l;
assert map.contains(1);
assert map.contains(2);
assert map.contains(3);
assert map.contains(4);
assert map.get(1) == "Hello";
assert map.get(2) == "World";
assert map.get(3) == "Foo";
assert map.get(4) == "Bar";
map.remove(2);
assert map.getSize() == 3l;
assert !map.contains(2);
assert !map.isEmpty();
map.remove(1);
assert map.getSize() == 2l;
assert !map.contains(1);
assert !map.isEmpty();
string& foo = map.get(3);
assert foo == "Foo";
foo = "Baz";
assert map.get(3) == "Baz";
Result<string> bar = map.getSafe(4);
assert bar.isOk();
assert bar.unwrap() == "Bar";
Result<string> baz = map.getSafe(5);
assert baz.isErr();
map.remove(3);
assert map.getSize() == 1l;
assert !map.contains(3);
assert !map.isEmpty();
map.remove(4);
assert map.getSize() == 0l;
assert !map.contains(4);
assert map.isEmpty();
BinaryTree<int> tree;
tree.insert(5);
tree.insert(3);
tree.insert(7);
tree.insert(2);
assert tree.contains(5);
assert tree.contains(3);
assert tree.contains(7);
assert tree.contains(2);
assert !tree.contains(1);
assert !tree.contains(4);
assert !tree.contains(6);
assert !tree.contains(8);
tree.delete(3);
assert tree.contains(5);
assert !tree.contains(3);
assert tree.contains(7);
assert tree.contains(2);
assert !tree.contains(1);
assert !tree.contains(4);
assert !tree.contains(6);
assert !tree.contains(8);
tree.delete(5);
assert !tree.contains(5);
assert !tree.contains(3);
assert tree.contains(7);
assert tree.contains(2);
assert !tree.contains(1);
assert !tree.contains(4);
assert !tree.contains(6);
assert !tree.contains(8);
tree.delete(7);
assert !tree.contains(5);
assert !tree.contains(3);
assert !tree.contains(7);
assert tree.contains(2);
assert !tree.contains(1);
assert !tree.contains(4);
assert !tree.contains(6);
assert !tree.contains(8);
tree.delete(2);
assert !tree.contains(5);
assert !tree.contains(3);
assert !tree.contains(7);
assert !tree.contains(2);
assert !tree.contains(1);
assert !tree.contains(4);
assert !tree.contains(6);
assert !tree.contains(8);

printf("All assertions passed!\n");
}

/*import "std/os/env";
import "std/io/filepath";
import "bootstrap/ast/ast-nodes";
import "bootstrap/lexer/lexer";
import "bootstrap/parser/parser";
/*p foo(heap int* ptr) {
*ptr = 3;
}

f<int> main() {
String filePathString = getEnv("SPICE_STD_DIR") + "/../test/test-files/bootstrap-compiler/standalone-parser-test/test-file.spice";
FilePath filePath = FilePath(filePathString);
Lexer lexer = Lexer(filePath);
Parser parser = Parser(lexer);
ASTEntryNode* ast = parser.parse();
assert ast != nil<ASTEntryNode*>;
printf("All assertions passed!\n");
heap int* ptr = sNew<int>();
*ptr = 5;
foo(ptr);
}*/
1 change: 1 addition & 0 deletions src/SourceFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class SourceFile {
const SourceFile *getRootSourceFile() const;
bool isRT(RuntimeModule runtimeModule) const;
ALWAYS_INLINE bool isStringRT() const { return isRT(STRING_RT); }
ALWAYS_INLINE bool isMemoryRT() const { return isRT(MEMORY_RT); }
ALWAYS_INLINE bool isRttiRT() const { return isRT(RTTI_RT); }

// Public fields
Expand Down
11 changes: 8 additions & 3 deletions src/ast/ASTNodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,12 @@ class AnonymousBlockStmtNode : public StmtNode {

class StmtLstNode : public ASTNode {
public:
// Structs
struct ResourcesForManifestationToCleanup {
std::vector<std::pair<SymbolTableEntry *, Function *>> dtorFunctionsToCall;
std::vector<SymbolTableEntry *> heapVarsToFree;
};

// Constructors
using ASTNode::ASTNode;

Expand All @@ -835,13 +841,12 @@ class StmtLstNode : public ASTNode {

// Other methods
[[nodiscard]] bool returnsOnAllControlPaths(bool *doSetPredecessorsUnreachable) const override;
void customItemsInitialization(size_t manifestationCount) override { dtorFunctions.resize(manifestationCount); }
void customItemsInitialization(size_t manifestationCount) override { resourcesToCleanup.resize(manifestationCount); }
[[nodiscard]] bool isStmtLstNode() const override { return true; }

// Public members
size_t complexity = 0;
// Outer vector: manifestation index; inner vector: list of dtor functions
std::vector<std::vector<std::pair<SymbolTableEntry *, Function *>>> dtorFunctions;
std::vector<ResourcesForManifestationToCleanup> resourcesToCleanup;
};

// ========================================================= TypeLstNode =========================================================
Expand Down
11 changes: 10 additions & 1 deletion src/irgenerator/GenImplicit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ void IRGenerator::generateScopeCleanup(const StmtLstNode *node) const {
return;

// Call all dtor functions
for (auto [entry, dtor] : node->dtorFunctions.at(manIdx))
const StmtLstNode::ResourcesForManifestationToCleanup& resourcesToCleanup = node->resourcesToCleanup.at(manIdx);
for (auto [entry, dtor] : resourcesToCleanup.dtorFunctionsToCall)
generateCtorOrDtorCall(entry, dtor, {});

// Deallocate all heap variables that go out of scope and are currently owned
for (SymbolTableEntry *entry : resourcesToCleanup.heapVarsToFree)
generateDeallocCall(entry->getAddress());

// Generate lifetime end markers
if (cliOptions.useLifetimeMarkers) {
std::vector<SymbolTableEntry *> vars = currentScope->getVarsGoingOutOfScope();
Expand Down Expand Up @@ -147,6 +152,10 @@ void IRGenerator::generateCtorOrDtorCall(llvm::Value *structAddr, const Function
}

void IRGenerator::generateDeallocCall(llvm::Value *variableAddress) const {
// Abort if the address is not set. This can happen when leaving the scope of a dtor, which already freed the heap memory
if (!variableAddress)
return;

// In case of string runtime, call free manually. Otherwise, use the memory_rt implementation of sDealloc()
if (sourceFile->isStringRT()) {
llvm::Function *freeFct = stdFunctionManager.getFreeFct();
Expand Down
6 changes: 3 additions & 3 deletions src/irgenerator/IRGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ LLVMExprResult IRGenerator::doAssignment(llvm::Value *lhsAddress, SymbolTableEnt
// Create shallow copy
llvm::Type *rhsType = rhsSTypeNonRef.toLLVMType(sourceFile);
const std::string copyName = lhsEntry ? lhsEntry->name : "";
llvm::Value *newAddress = createShallowCopy(rhsAddress, rhsType, lhsAddress, copyName, lhsEntry && lhsEntry->isVolatile);
llvm::Value *newAddress = generateShallowCopy(rhsAddress, rhsType, lhsAddress, copyName, lhsEntry && lhsEntry->isVolatile);
// Set address of lhs to the copy
if (lhsEntry && lhsEntry->scope->type != ScopeType::STRUCT && lhsEntry->scope->type != ScopeType::INTERFACE)
lhsEntry->updateAddress(newAddress);
Expand Down Expand Up @@ -490,8 +490,8 @@ LLVMExprResult IRGenerator::doAssignment(llvm::Value *lhsAddress, SymbolTableEnt
return LLVMExprResult{.value = rhsValue, .ptr = lhsAddress, .entry = lhsEntry};
}

llvm::Value *IRGenerator::createShallowCopy(llvm::Value *oldAddress, llvm::Type *varType, llvm::Value *targetAddress,
const std::string &name /*=""*/, bool isVolatile /*=false*/) {
llvm::Value *IRGenerator::generateShallowCopy(llvm::Value *oldAddress, llvm::Type *varType, llvm::Value *targetAddress,
const std::string &name /*=""*/, bool isVolatile /*=false*/) {
// Retrieve size to copy
const llvm::TypeSize typeSize = module->getDataLayout().getTypeAllocSize(varType);

Expand Down
2 changes: 1 addition & 1 deletion src/irgenerator/IRGenerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class IRGenerator : private CompilerPass, public ParallelizableASTVisitor {
LLVMExprResult doAssignment(llvm::Value *lhsAddress, SymbolTableEntry *lhsEntry, const ASTNode *rhsNode, bool isDecl = false);
LLVMExprResult doAssignment(llvm::Value *lhsAddress, SymbolTableEntry *lhsEntry, LLVMExprResult &rhs, const QualType &rhsSType,
bool isDecl);
llvm::Value *createShallowCopy(llvm::Value *oldAddress, llvm::Type *varType, llvm::Value *targetAddress,
llvm::Value *generateShallowCopy(llvm::Value *oldAddress, llvm::Type *varType, llvm::Value *targetAddress,
const std::string &name = "", bool isVolatile = false);
void autoDeReferencePtr(llvm::Value *&ptr, QualType &symbolType) const;
llvm::GlobalVariable *createGlobalConst(const std::string &baseName, llvm::Constant *constant);
Expand Down
16 changes: 16 additions & 0 deletions src/symboltablebuilder/Lifecycle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const char *Lifecycle::getCurrentStateName() const {
return "declared";
case INITIALIZED:
return "initialized";
case MOVED:
return "moved";
default:
return "dead";
}
Expand All @@ -55,4 +57,18 @@ bool Lifecycle::isDeclared() const { return getCurrentState() == DECLARED; }
*/
bool Lifecycle::isInitialized() const { return getCurrentState() == INITIALIZED; }

/**
* Check if the symbol was moved
*
* @return Moved or not
*/
bool Lifecycle::wasMoved() const { return getCurrentState() == MOVED; }

/**
* Check if the symbol is in an owning state
*
* @return Owning state or not
*/
bool Lifecycle::isInOwningState() const { return isDeclared() || isInitialized(); }

} // namespace spice::compiler
14 changes: 7 additions & 7 deletions src/symboltablebuilder/Lifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ namespace spice::compiler {
class ASTNode;

enum LifecycleState : uint8_t {
DEAD,
DECLARED,
INITIALIZED,
DEAD, // The symbol is not alive yet (e.g. not declared yet)
DECLARED, // The symbol is declared but hasn't got a value yet (in debug mode declaration comes with default initialization)
INITIALIZED, // The symbol is initialized with a value
MOVED, // The symbol was moved away (can still be accessed, but does not own its memory anymore)
};

/**
Expand All @@ -30,10 +31,7 @@ struct LifecycleEvent {
* A lifecycle represents a collection of timely events that occur for a symbol.
*
* Usually a lifecycle looks e.g. like this:
* - dead
* - declared
* - initialized
* - dead
* f<int
*/
class Lifecycle {
public:
Expand All @@ -44,6 +42,8 @@ class Lifecycle {
[[nodiscard]] [[maybe_unused]] bool isDead() const;
[[nodiscard]] [[maybe_unused]] bool isDeclared() const;
[[nodiscard]] bool isInitialized() const;
[[nodiscard]] bool wasMoved() const;
[[nodiscard]] bool isInOwningState() const;

private:
// Private members
Expand Down
15 changes: 7 additions & 8 deletions src/symboltablebuilder/SymbolTableEntry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ void SymbolTableEntry::updateType(const QualType &newType, [[maybe_unused]] bool
/**
* Update the state of the current symbol
*
* @throws SemanticError When trying to re-assign a constant variable
* @throws runtime_error When the state of the symbol is set to initialized before a concrete type was set
* @throws CompilerError When the state of the symbol is set to initialized before a concrete type was set
*
* @param newState New state of the current symbol
* @param node AST node where the update takes place
Expand All @@ -40,12 +39,12 @@ void SymbolTableEntry::updateType(const QualType &newType, [[maybe_unused]] bool
void SymbolTableEntry::updateState(const LifecycleState &newState, const ASTNode *node, bool force) {
const LifecycleState oldState = lifecycle.getCurrentState();
// Check if this is a constant variable and is already initialized
if (newState != DEAD && oldState != DECLARED && qualType.isConst() && !force) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Not re-assignable variable '" + name + "'"); // GCOV_EXCL_LINE
if (newState == DEAD && oldState == DECLARED) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Cannot destruct uninitialized variable '" + name + "'"); // GCOV_EXCL_LINE
if (newState == DEAD && oldState == DEAD) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Cannot destruct already freed variable '" + name + "'"); // GCOV_EXCL_LINE
if (newState != DEAD && oldState != DECLARED && qualType.isConst() && !force) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Not re-assignable variable '" + name + "'"); // GCOV_EXCL_LINE
if (newState == DEAD && oldState == DECLARED) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Cannot destroy uninitialized variable '" + name + "'"); // GCOV_EXCL_LINE
if (newState == DEAD && oldState == DEAD) // GCOV_EXCL_LINE
throw CompilerError(INTERNAL_ERROR, "Cannot destroy already destroyed variable '" + name + "'"); // GCOV_EXCL_LINE
lifecycle.addEvent({newState, node});
}

Expand Down
1 change: 1 addition & 0 deletions src/symboltablebuilder/SymbolTableEntry.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class SymbolTableEntry {
void pushAddress(llvm::Value *address);
void popAddress();
[[nodiscard]] bool isField() const;
[[nodiscard]] const Lifecycle &getLifecycle() const { return lifecycle; }
[[nodiscard]] bool isInitialized() const { return lifecycle.isInitialized(); }
[[nodiscard]] nlohmann::ordered_json toJSON() const;

Expand Down
Loading

0 comments on commit 2561ef2

Please sign in to comment.