From ef08fee9764ca86fbc348388f0b74755012b7b18 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Tue, 27 Oct 2020 14:38:28 +0100 Subject: [PATCH] Base structure for some methods ready --- src/config.nim | 2 +- src/memory.nim | 2 +- src/types/jobject.nim | 400 ++++++++++++++++++++++++++---------------- src/vm.nim | 52 +++--- 4 files changed, 279 insertions(+), 177 deletions(-) diff --git a/src/config.nim b/src/config.nim index 2260678..3f8f5fc 100644 --- a/src/config.nim +++ b/src/config.nim @@ -20,7 +20,7 @@ const ARRAY_GROW_FACTOR = 2 # How much extra memory to allocate for dynamic ar const FRAMES_MAX* = 400 # TODO: Inspect why the VM crashes if this exceeds 400 const JAPL_VERSION* = "0.2.0" const JAPL_RELEASE* = "alpha" -const DEBUG_TRACE_VM* = false # Traces VM execution +const DEBUG_TRACE_VM* = false # Traces VM execution const DEBUG_TRACE_GC* = false # Traces the garbage collector (TODO) const DEBUG_TRACE_ALLOCATION* = false # Traces memory allocation/deallocation (WIP) const DEBUG_TRACE_COMPILER* = false # Traces the compiler diff --git a/src/memory.nim b/src/memory.nim index e0240c9..75861ea 100644 --- a/src/memory.nim +++ b/src/memory.nim @@ -23,7 +23,7 @@ import segfaults -import config +# import config # when DEBUG_TRACE_ALLOCATION: # import util/debug # TODO: Recursive dependency diff --git a/src/types/jobject.nim b/src/types/jobject.nim index 7b9e53c..55fe604 100644 --- a/src/types/jobject.nim +++ b/src/types/jobject.nim @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -## JAPL' type system +## The JAPL's type system. In JAPL, all entities are +## objects, and are always references to a memory location +## somewhere in the heap import ../memory import strformat type - NotImplementedError = object of CatchableError + NotImplementedError* = object of CatchableError ## Raised when a given operation is unsupported ## on a given type Chunk* = ref object # TODO: This shouldn't be here, but Function needs it. Consider refactoring @@ -43,6 +45,7 @@ type ## from this base type kind*: ObjectType hashValue*: uint64 + isHashable: bool # This is false for unhashable objects String* = object of Obj ## A string object str*: ptr UncheckedArray[char] # TODO -> Unicode support @@ -95,7 +98,8 @@ proc newChunk*(): Chunk = result = Chunk(consts: @[], code: @[], lines: @[]) -## Utilities that bridge nim and JAPL types +## Utilities that bridge nim and JAPL types or to inspect +## JAPL objects proc objType*(obj: ptr Obj): ObjectType = @@ -170,7 +174,7 @@ proc toFloat*(obj: ptr Obj): float = ## Converts a JAPL float to a nim float result = cast[ptr Float](obj).floatValue -# TODO ambiguous naming: conflict with toString(obj: obj) that does JAPL->JAPL + proc toStr*(obj: ptr Obj): string = ## Converts a JAPL string into a nim string var strObj = cast[ptr String](obj) @@ -182,26 +186,28 @@ proc asInt*(n: int): ptr Integer = ## Converts a nim int into a JAPL int result = allocateObj(Integer, ObjectType.Integer) result.intValue = n + result.isHashable = true proc asFloat*(n: float): ptr Float = ## Converts a nim float into a JAPL float result = allocateObj(Float, ObjectType.Float) result.floatValue = n + result.isHashable = true proc asBool*(b: bool): ptr Bool = ## Converts a nim bool into a JAPL bool result = allocateObj(Bool, ObjectType.Bool) result.boolValue = b + result.isHashable = true - -proc asNil*(): ptr Nil = +proc asNil*(): ptr Nil = ## Creates a nil object result = allocateObj(Nil, ObjectType.Nil) -proc asNan*(): ptr NotANumber = +proc asNan*(): ptr NotANumber = ## Creates a nan object result = allocateObj(NotANumber, ObjectType.NotANumber) @@ -216,29 +222,124 @@ proc newObj*(): ptr Obj = result = allocateObj(Obj, ObjectType.BaseObject) -## JAPL procs implementations below +proc newString*(str: string): ptr String +proc asStr*(s: string): ptr String # Forward declarations + + + +# Functions constructors and procedures + +type + FunctionType* {.pure.} = enum + ## All code in JAPL is compiled + ## as if it was inside some sort + ## of function. To differentiate + ## between actual functions and + ## the top-level code, this tiny + ## enum is used to tell the two + ## contexts apart when compiling + Func, Script + + +proc newFunction*(name: string = "", chunk: Chunk = newChunk(), arity: int = 0): ptr Function = + ## Allocates a new function object with the given + ## bytecode chunk and arity. If the name is an empty string + ## (the default), the function will be an + ## anonymous code object + # TODO: Add lambdas + # TODO: Add support for optional parameters + result = allocateObj(Function, ObjectType.Function) + if name.len > 1: + result.name = newString(name) + else: + result.name = nil + result.arity = arity + result.chunk = chunk + result.isHashable = false + + +proc newIndexError*(message: string): ptr JAPLException = + result = allocateObj(JAPLException, ObjectType.Exception) + result.errName = "IndexError".asStr() + result.message = message.asStr() + + +proc newReferenceError*(message: string): ptr JAPLException = + result = allocateObj(JAPLException, ObjectType.Exception) + result.errName = "ReferenceError".asStr() + result.message = message.asStr() + + +proc newInterruptedError*(message: string): ptr JAPLException = + result = allocateObj(JAPLException, ObjectType.Exception) + result.errName = "InterruptedError".asStr() + result.message = message.asStr() + + +proc newRecursionError*(message: string): ptr JAPLException = + result = allocateObj(JAPLException, ObjectType.Exception) + result.errName = "RecursionError".asStr() + result.message = message.asStr() + + + +proc newTypeError*(message: string): ptr JAPLException = + result = allocateObj(JAPLException, ObjectType.Exception) + result.errName = "TypeError".asStr() + result.message = message.asStr() + + +## Exposed object methods used in the JAPL runtime +## are defined and implemented below + # Implementations for typeName -proc typeName*(self: ptr Obj): string = - ## Returns the name of the object type - result = "object" - -proc typeName*(self: ptr Function): string = +proc typeName(self: ptr Function): string = result = "function" -proc typeName*(self: ptr String): string = + +proc typeName(self: ptr String): string = return "string" -proc typeName*(self: ptr Integer): string = + +proc typeName(self: ptr Integer): string = result = "integer" -proc typeName*(self: ptr Float): string = + +proc typeName(self: ptr Float): string = result = "float" -proc typeName*(self: ptr Bool): string = + +proc typeName(self: ptr Bool): string = result = "bool" + +proc typeName*(self: ptr Obj): string = + ## Returns the name of the object's type + case self.kind: + of ObjectType.BaseObject: + result = "object" + of ObjectType.String: + result = cast[ptr String](self).typeName() + of ObjectType.Integer: + result = cast[ptr Integer](self).typeName() + of ObjectType.Float: + result = cast[ptr Float](self).typeName() + of ObjectType.Bool: + result = cast[ptr Bool](self).typeName() + of ObjectType.Function: + result = cast[ptr Function](self).typeName() + of ObjectType.Infinity: + result = cast[ptr Infinity](self).typeName() + of ObjectType.NotANumber: + result = cast[ptr NotANumber](self).typeName() + of ObjectType.Nil: + result = cast[ptr Nil](self).typeName() + else: + discard # TODO + + # Implementations for stringify proc stringify*(self: ptr Integer): string = @@ -269,6 +370,7 @@ proc stringify(self: ptr Infinity): string = else: result = "inf" + proc stringify(self: ptr Nil): string = result = "nil" @@ -283,7 +385,8 @@ proc stringify*(self: ptr Function): string = else: result = "" -proc stringify*(self: ptr Obj): string = + +proc stringify*(self: ptr Obj): string = ## Returns a string representation of the ## given object case self.kind: @@ -328,11 +431,11 @@ proc isFalsey(self: ptr Float): bool = result = self.floatValue == 0.0 -proc isFalsey(self: ptr Bool): bool = +proc isFalsey(self: ptr Bool): bool = result = not self.boolValue -proc isFalsey(self: ptr Infinity): bool = +proc isFalsey(self: ptr Infinity): bool = result = false @@ -402,7 +505,7 @@ proc hash(self: ptr Nil): uint64 = # TODO: Arbitrary hash seems a bad idea result = 2u -proc hash(self: ptr Function): uint64 = +proc hash(self: ptr Function): uint64 = # TODO: Hashable? raise newException(NotImplementedError, "unhashable type 'function'") @@ -411,6 +514,8 @@ proc hash*(self: ptr Obj): uint64 = ## Returns the hash of the object using ## the FNV-1a algorithm (or a predefined value). ## Raises an error if the object is not hashable + if not self.isHashable: + raise newException(NotImplementedError, &"unhashable type '{self.typeName}'") case self.kind: of ObjectType.BaseObject: result = 2166136261u # Constant hash @@ -508,143 +613,140 @@ proc eq*(self, other: ptr Obj): bool = discard # TODO -## String constructors and converters +proc sum(self: ptr String, other: ptr Obj): ptr String = + if other.kind == ObjectType.String: + var other = cast[ptr String](other) + var selfStr = self.toStr() + var otherStr = other.toStr() + result = (selfStr & otherStr).asStr() + else: + raise newException(NotImplementedError, &"unsupported binary operator '+' for objects of type '{self.typeName()}' and '{other.typeName()}'") + + +proc sum(self: ptr Integer, other: ptr Obj): ptr Obj = # This can yield a float! + case other.kind: + of ObjectType.Integer: + result = (self.toInt() + cast[ptr Integer](other).toInt()).asInt() + of ObjectType.Float: + let res = ((float self.toInt()) + cast[ptr Float](other).toFloat()) + if res == system.Inf: + result = asInf() + elif res == -system.Inf: + let negInf = asInf() + negInf.isNegative = true + result = negInf + else: + result = res.asFloat() + of ObjectType.NotANumber: + result = asNan() + of ObjectType.Infinity: + result = cast[ptr Infinity](other) + else: + raise newException(NotImplementedError, &"unsupported binary operator '+' for objects of type '{self.typeName()}' and '{other.typeName()}'") + + +proc sum(self: ptr Float, other: ptr Obj): ptr Obj = + case other.kind: + of ObjectType.Integer: + result = (self.toFloat() + float cast[ptr Integer](other).toInt()).asFloat() + of ObjectType.Float: + let res = (self.toFloat() + cast[ptr Float](other).toFloat()) + if res == system.Inf: + result = asInf() + elif res == -system.Inf: + let negInf = asInf() + negInf.isNegative = true + result = negInf + else: + result = res.asFloat() + of ObjectType.NotANumber: + result = asNan() + of ObjectType.Infinity: + result = cast[ptr Infinity](other) + else: + raise newException(NotImplementedError, &"unsupported binary operator '+' for objects of type '{self.typeName()}' and '{other.typeName()}'") + + +proc sum*(self, other: ptr Obj): ptr Obj = + ## Returns the result of self + other + ## or raises NotImplementedError if the operation is unsupported + case self.kind: + of ObjectType.String: # Here we don't cast other (yet) + # because binary operators can mix types together, + # like in "hello" * 5, or 3.5 * 8. Casting that + # later allows for finer error reporting and keeps + # these methods as generic as possible + result = cast[ptr String](self).sum(other) + of ObjectType.Integer: + result = cast[ptr Integer](self).sum(other) + of ObjectType.Float: + result = cast[ptr Float](self).sum(other) + else: + raise newException(NotImplementedError, &"unsupported binary operator '+' for objects of type '{self.typeName()}' and '{other.typeName()}'") + + +proc sub(self, other: ptr Obj): ptr Obj = + ## Returns the result of self - other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc mul(self, other: ptr Obj): ptr Obj = + ## Returns the result of self * other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc trueDiv(self, other: ptr Obj): ptr Obj = + ## Returns the result of self / other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc exp(self, other: ptr Obj): ptr Obj = + ## Returns the result of self ** other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc binaryAnd(self, other: ptr Obj): ptr Obj = + ## Returns the result of self & other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc binaryOr(self, other: ptr Obj): ptr Obj = + ## Returns the result of self | other + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc binaryNot(self: ptr Obj): ptr Obj = + ## Returns the result of ~self + ## or raises NotImplementedError if the operation is unsupported + result = nil + + +proc binaryXor(self, other: ptr Obj): ptr Obj = + ## Returns the result of self ^ other + ## or raises NotImplementedError if the operation is unsupported + result = nil + proc newString*(str: string): ptr String = # TODO -> Unicode + # TODO -> Move this into asStr result = allocateObj(String, ObjectType.String) result.str = allocate(UncheckedArray[char], char, len(str)) for i in 0..len(str) - 1: result.str[i] = str[i] result.len = len(str) result.hashValue = result.hash() + result.isHashable = true -proc asStr*(s: string): ptr Obj = - ## Converts a nim string into a + +proc asStr*(s: string): ptr String = + + ## Converts a nim string into a ## JAPL string result = newString(s) - -# End of string object procs - - -# Functions constructors and procedures - -type - FunctionType* {.pure.} = enum - ## All code in JAPL is compiled - ## as if it was inside some sort - ## of function. To differentiate - ## between actual functions and - ## the top-level code, this tiny - ## enum is used to tell the two - ## contexts apart when compiling - Func, Script - - -proc newFunction*(name: string = "", chunk: Chunk = newChunk(), arity: int = 0): ptr Function = - ## Allocates a new function object with the given - ## bytecode chunk and arity. If the name is an empty string - ## (the default), the function will be an - ## anonymous code object - # TODO: Add lambdas - # TODO: Add support for optional parameters - result = allocateObj(Function, ObjectType.Function) - if name.len > 1: - result.name = newString(name) - else: - result.name = nil - result.arity = arity - result.chunk = chunk - - -proc bool*(obj: ptr Obj): bool = - ## Returns wheter the object should - ## be considered a falsey obj - ## or not. Returns true if the - ## object is truthy, or false - ## if it is falsey - result = false - - -proc add(self, other: ptr Obj): ptr Obj = - ## Returns the result of self + other - ## or nil if the operation is unsupported - result = nil # Not defined for base objects! - - -proc sub(self, other: ptr Obj): ptr Obj = - ## Returns the result of self - other - ## or nil if the operation is unsupported - result = nil - - -proc mul(self, other: ptr Obj): ptr Obj = - ## Returns the result of self * other - ## or nil if the operation is unsupported - result = nil - - -proc trueDiv(self, other: ptr Obj): ptr Obj = - ## Returns the result of self / other - ## or nil if the operation is unsupported - result = nil - - -proc exp(self, other: ptr Obj): ptr Obj = - ## Returns the result of self ** other - ## or nil if the operation is unsupported - result = nil - - -proc binaryAnd(self, other: ptr Obj): ptr Obj = - ## Returns the result of self & other - ## or nil if the operation is unsupported - result = nil - - -proc binaryOr(self, other: ptr Obj): ptr Obj = - ## Returns the result of self | other - ## or nil if the operation is unsupported - result = nil - - -proc binaryNot(self: ptr Obj): ptr Obj = - ## Returns the result of ~self - ## or nil if the operation is unsupported - result = nil - - -proc binaryXor(self, other: ptr Obj): ptr Obj = - ## Returns the result of self ^ other - ## or nil if the operation is unsupported - result = nil - -proc newIndexError*(message: string): ptr JAPLException = - result = allocateObj(JAPLException, ObjectType.Exception) - result.errName = newString("IndexError") - result.message = newString(message) - - -proc newReferenceError*(message: string): ptr JAPLException = - result = allocateObj(JAPLException, ObjectType.Exception) - result.errName = newString("ReferenceError") - result.message = newString(message) - - -proc newInterruptedError*(message: string): ptr JAPLException = - result = allocateObj(JAPLException, ObjectType.Exception) - result.errName = newString("InterruptedError") - result.message = newString(message) - - -proc newRecursionError*(message: string): ptr JAPLException = - result = allocateObj(JAPLException, ObjectType.Exception) - result.errName = newString("RecursionError") - result.message = newString(message) - - - -proc newTypeError*(message: string): ptr JAPLException = - result = allocateObj(JAPLException, ObjectType.Exception) - result.errName = newString("TypeError") - result.message = newString(message) diff --git a/src/vm.nim b/src/vm.nim index a4aefe1..0f806c9 100644 --- a/src/vm.nim +++ b/src/vm.nim @@ -225,7 +225,7 @@ proc callObject(self: var VM, callee: ptr Obj, argCount: uint8): bool = else: # TODO: Classes discard # Unreachable else: - self.error(newTypeError(&"object of type '{callee.typeName}' is not callable")) + self.error(newTypeError(&"object of type '{callee.typeName()}' is not callable")) return false @@ -289,22 +289,27 @@ proc run(self: var VM, repl: bool): InterpretResult = stdout.write(&"Current frame count: {self.frameCount}\n") stdout.write("Current frame stack status: ") stdout.write("[") - for e in self.stack[frame.slot..self.stackTop - 1]: + for e in frame.getView(): stdout.write(stringify(e)) stdout.write(", ") stdout.write("]\n") discard disassembleInstruction(frame.function.chunk, frame.ip - 1) case opcode: # Main OpCodes dispatcher of OpCode.Constant: - var constant: ptr Obj = readConstant() - self.push(constant) + self.push(readConstant()) of OpCode.ConstantLong: - var constant: ptr Obj = readLongConstant() - self.push(constant) + self.push(readLongConstant()) of OpCode.Negate: # TODO: Call appropriate methods discard + # self.push(self.pop().negate()) of OpCode.Add: - discard + var left = self.pop() + var right = self.pop() + try: + self.push(right.sum(left)) + except NotImplementedError: + self.error(newTypeError(getCurrentExceptionMsg())) + return RuntimeError of OpCode.Shl: discard of OpCode.Shr: @@ -340,7 +345,11 @@ proc run(self: var VM, repl: bool): InterpretResult = of OpCode.Not: self.push(self.pop().isFalsey().asBool()) of OpCode.Equal: + # Here order doesn't matter, because if a == b + # then b == a (at least in *most* languages, sigh) self.push(self.pop().eq(self.pop()).asBool()) + # Doesn't this chain of calls look beautifully + # intuitive? of OpCode.Less: discard of OpCode.Greater: @@ -353,11 +362,9 @@ proc run(self: var VM, repl: bool): InterpretResult = return RuntimeError of OpCode.DefineGlobal: if frame.function.chunk.consts.len > 255: - var constant = readLongConstant().toStr() - self.globals[constant] = self.peek(0) + self.globals[readLongConstant().toStr()] = self.peek(0) else: - var constant = readConstant().toStr() - self.globals[constant] = self.peek(0) + self.globals[readConstant().toStr()] = self.peek(0) discard self.pop() # This will help when we have a custom GC of OpCode.GetGlobal: if frame.function.chunk.consts.len > 255: @@ -407,18 +414,14 @@ proc run(self: var VM, repl: bool): InterpretResult = self.globals.del(constant) of OpCode.GetLocal: if frame.len > 255: - var slot = readBytes() - self.push(frame[slot]) + self.push(frame[readBytes()]) else: - var slot = readByte() - self.push(frame[int slot]) + self.push(frame[int readByte()]) of OpCode.SetLocal: if frame.len > 255: - var slot = readBytes() - frame[slot] = self.peek(0) + frame[readBytes()] = self.peek(0) else: - var slot = readByte() - frame[int slot] = self.peek(0) + frame[int readByte()] = self.peek(0) of OpCode.DeleteLocal: # Unused due to GC potential issues if frame.len > 255: @@ -430,15 +433,12 @@ proc run(self: var VM, repl: bool): InterpretResult = of OpCode.Pop: self.lastPop = self.pop() of OpCode.JumpIfFalse: - var offset = readShort() if isFalsey(self.peek(0)): - frame.ip += int offset + frame.ip += int readShort() of OpCode.Jump: - var offset = readShort() - frame.ip += int offset + frame.ip += int readShort() of OpCode.Loop: - var offset = readShort() - frame.ip -= int offset + frame.ip -= int readShort() of OpCode.Call: var argCount = readByte() if not self.callObject(self.peek(int argCount), argCount): @@ -450,7 +450,7 @@ proc run(self: var VM, repl: bool): InterpretResult = var retResult = self.pop() if repl: if not self.lastPop.isNil() and self.frameCount == 1: # This is to avoid - # useless output with recursive calls + # This avoids unwanted output with recursive calls echo stringify(self.lastPop) self.lastPop = asNil() self.frameCount -= 1