Skip to content

Commit

Permalink
Initial spook
Browse files Browse the repository at this point in the history
  • Loading branch information
jsannemo committed Sep 11, 2020
0 parents commit 2839739
Show file tree
Hide file tree
Showing 23 changed files with 1,708 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.idea
javadoc/
out/
*.iml
target/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Johan Sannemo

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.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
![Spoooky!](https://github.com/jsannemo/spooky-vm/blob/master/spook.png?raw=true)

So spooky.
91 changes: 91 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>se.jsannemo</groupId>
<artifactId>spooky-vm</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>13</maven.compiler.source>
<maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne</arg>
</compilerArgs>
<annotationProcessors>
<annotationProcessor>com.google.auto.value.processor.AutoValueProcessor</annotationProcessor>
<annotationProcessor>com.google.auto.value.processor.AutoOneOfProcessor</annotationProcessor>
</annotationProcessors>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.3.3</version>
</path>
<path>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value</artifactId>
<version>1.6.6</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>3.1.12.2</version>
<dependencies>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs</artifactId>
<version>4.0.0-beta4</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
Binary file added spook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions src/main/java/se/jsannemo/spooky/compiler/Assembler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package se.jsannemo.spooky.compiler;

import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import se.jsannemo.spooky.vm.code.ExecutableParser;
import se.jsannemo.spooky.vm.code.Instructions.Instruction;

/** Assembler for VM instructions into binary code. */
public final class Assembler {

/**
* Assembles {@code instructions} into a binary format that can be parsed by {@link
* ExecutableParser#fromBinary(byte[])}.
*/
public static byte[] assemble(ImmutableList<Instruction> instructions) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
for (Instruction inst : instructions) {
inst.writeBinary(bos);
}
} catch (IOException ioe) {
throw new AssertionError("ByteArrayOutputStream shouldn't throw IOException");
}
return bos.toByteArray();
}
}
7 changes: 7 additions & 0 deletions src/main/java/se/jsannemo/spooky/vm/ExternCall.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package se.jsannemo.spooky.vm;

/** The signature of an external call that should be supported in the VM. */
@FunctionalInterface
public interface ExternCall {
void call(SpookyVm vm) throws VmException;
}
174 changes: 174 additions & 0 deletions src/main/java/se/jsannemo/spooky/vm/SpookyVm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package se.jsannemo.spooky.vm;

import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableMap;
import se.jsannemo.spooky.vm.code.Executable;
import se.jsannemo.spooky.vm.code.Instructions.Add;
import se.jsannemo.spooky.vm.code.Instructions.Const;
import se.jsannemo.spooky.vm.code.Instructions.Div;
import se.jsannemo.spooky.vm.code.Instructions.Extern;
import se.jsannemo.spooky.vm.code.Instructions.Instruction;
import se.jsannemo.spooky.vm.code.Instructions.Jump;
import se.jsannemo.spooky.vm.code.Instructions.LessThan;
import se.jsannemo.spooky.vm.code.Instructions.Move;
import se.jsannemo.spooky.vm.code.Instructions.Mul;
import se.jsannemo.spooky.vm.code.Instructions.Sub;

/**
* A virtual machine, executing parsed Spooky code.
*
* <p>The Spooky code is given to the VM in the form of a {@link Executable}. It will start executing
* the first instruction in the text segment.
*/
public final class SpookyVm {

private final ImmutableMap<String, ExternCall> externs;
private final int[] memory;
/**
* The value of the instruction pointer, with the index of the text instructions in the current
* executable that should be executed.
*/
private int ip;
/** The executable that we are currently executing instructions in. */
private Executable curExecutable;

private SpookyVm(Executable executable, ImmutableMap<String, ExternCall> externs, int memoryCells) {
this.externs = externs;
this.curExecutable = executable;
this.ip = 0;
this.memory = new int[memoryCells];
}

/**
* Executes the current instruction of the VM, advancing the instruction pointer afterwards.
*
* <p>If the instruction pointer points to an invalid instruction (i.e. one that is smaller or
* larger than the amount of instructions in the current executable), nothing happens.
*
* @throws VmException if the instruction caused a run-time fault in the VM.
*/
public void executeInstruction() throws VmException {
// Halt VM in case if an out-of-bounds instruction.
if (ip < 0 || ip >= curExecutable.text().size()) {
throw new VmException("Instruction pointer out-of-bounds");
}
Instruction ins = curExecutable.text().get(ip++);
checkState(ins.isExecutable());
if (ins instanceof Move) {
Move mov = (Move) ins;
setM(getM(mov.target()), getM(getM(mov.source())));
}
if (ins instanceof Const) {
Const cnst = (Const) ins;
setM(cnst.target(), cnst.value());
}
if (ins instanceof Add) {
Add add = (Add) ins;
setM(add.target(), getM(add.op1()) + getM(add.op2()));
}
if (ins instanceof Sub) {
Sub sub = (Sub) ins;
setM(sub.target(), getM(sub.op1()) - getM(sub.op2()));
}
if (ins instanceof Mul) {
Mul mul = (Mul) ins;
setM(mul.target(), getM(mul.op1()) * getM(mul.op2()));
}
if (ins instanceof Div) {
Div div = (Div) ins;
setM(div.target(), getM(div.op1()) / getM(div.op2()));
}
if (ins instanceof LessThan) {
LessThan lt = (LessThan) ins;
setM(lt.target(), getM(lt.op1()) < getM(lt.op2()) ? 1 : 0);
}
if (ins instanceof Jump) {
Jump jmp = (Jump) ins;
if (getM(jmp.flag()) == 0) {
ip = getM(jmp.addr());
}
}
if (ins instanceof Extern) {
Extern ext = (Extern) ins;
callExtern(ext.name());
}
}

private void callExtern(String extern) throws VmException {
if (!externs.containsKey(extern)) {
throw new VmException("Attempted to call non-existent extern " + extern);
}
externs.get(extern).call(this);
}

/**
* Returns the memory at position {@code pos}.
*
* <p>If {@code pos < 0}, the index {@code -(pos + 1)} of the data segment is returned. Otherwise,
* the position {@code pos} from the main memory is returned.
*
* @throws VmException if {@code pos} is invalid.
*/
public int getM(int pos) throws VmException {
if (0 <= pos && pos < memory.length) {
return memory[pos];
}
if (-this.curExecutable.data().size() <= pos && pos < 0) {
return this.curExecutable.data().get(-(pos + 1));
}
throw new VmException("Memory position " + pos + " is out of bounds");
}

/**
* Sets the memory at position {@code pos} in the main memory.
*
* @throws VmException if {@code pos} is invalid.
*/
public void setM(int pos, int value) throws VmException {
if (pos < 0 || pos >= memory.length) {
throw new VmException("Memory position " + pos + " is out of bounds");
}
memory[pos] = value;
}

/** Returns a new builder for {@link SpookyVm} instances. */
public static Builder newBuilder(Executable executable) {
return new Builder(executable);
}

/** A builder for {@link SpookyVm} instances. */
public static class Builder {
private final Executable executable;
private final ImmutableMap.Builder<String, ExternCall> externBuilder = ImmutableMap.builder();
private int memoryCells;

private Builder(Executable executable) {
this.executable = executable;
memoryCells = 0;
}

/** Make available an external call named {@code name} invoking {@code callback} when called. */
public Builder addExtern(String name, ExternCall callback) {
externBuilder.put(name, callback);
return this;
}

/** Add the external calls that the standard library provides. */
public Builder addStdLib() {
externBuilder.put("random", StdLib::random);
externBuilder.put("print", StdLib::print);
return this;
}

/** Set the memory size in (integer-sized) cells. */
public Builder setMemorySize(int memoryCells) {
this.memoryCells = memoryCells;
return this;
}

public SpookyVm build() {
return new SpookyVm(executable, externBuilder.build(), memoryCells);
}
}
}
46 changes: 46 additions & 0 deletions src/main/java/se/jsannemo/spooky/vm/StdLib.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package se.jsannemo.spooky.vm;

import java.util.Random;

/**
* {@link StdLib} provides utility methods to implement extern functions with the same calling
* convention as the Spooky standard library.
*/
public final class StdLib {

private static final int STACK_PTR = 0;
private static final Random RANDOM = new Random();

private StdLib() {}

/**
* Pops the last argument for the function from the stack and returns the value.
*
* @throws VmException if the pop causes the stack to underflow.
*/
public static int popArg(SpookyVm vm) throws VmException {
int sp = vm.getM(STACK_PTR);
int val = vm.getM(sp - 1);
vm.setM(STACK_PTR, sp - 1);
return val;
}

/**
* Pushes a return value for a function to the stack.
*
* @throws VmException if the push causes the stack to overflow.
*/
public static void pushArg(SpookyVm vm, int val) throws VmException {
int sp = vm.getM(STACK_PTR);
vm.setM(sp, val);
vm.setM(STACK_PTR, sp + 1);
}

static void random(SpookyVm vm) throws VmException {
pushArg(vm, RANDOM.nextInt());
}

static void print(SpookyVm vm) throws VmException {
System.out.print((char) popArg(vm));
}
}
8 changes: 8 additions & 0 deletions src/main/java/se/jsannemo/spooky/vm/VmException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package se.jsannemo.spooky.vm;

/** Exception signifying a run-time error in the program executing in the VM. */
public final class VmException extends Throwable {
VmException(String s) {
super(s);
}
}
Loading

0 comments on commit 2839739

Please sign in to comment.