Base structure for some methods ready

This commit is contained in:
nocturn9x 2020-10-27 14:38:28 +01:00
parent 09e543d786
commit ef08fee976
4 changed files with 279 additions and 177 deletions

View File

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

View File

@ -23,7 +23,7 @@
import segfaults
import config
# import config
# when DEBUG_TRACE_ALLOCATION:
# import util/debug # TODO: Recursive dependency

View File

@ -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 = "<code object>"
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)

View File

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