mirror of https://github.com/japl-lang/japl.git
Added the retNative enum for native functions to handle singletons properly, minor refactoring in the parser and the vm
This commit is contained in:
parent
c882281944
commit
3eeffabe7d
2
build.py
2
build.py
|
@ -209,7 +209,7 @@ if __name__ == "__main__":
|
|||
logging.error("Invalid parameter for --options")
|
||||
exit()
|
||||
build(args.path, flags, options, args.override_config, args.skip_tests)
|
||||
if not args.keep_results_file:
|
||||
if not args.keep_results_file and not args.skip_tests:
|
||||
try:
|
||||
os.remove("testresults.txt")
|
||||
except Exception as error:
|
||||
|
|
|
@ -20,12 +20,10 @@ import parseopt
|
|||
import os
|
||||
import config
|
||||
import vm
|
||||
import stdlib
|
||||
|
||||
|
||||
proc repl() =
|
||||
var bytecodeVM = initVM()
|
||||
bytecodeVM.stdlibInit()
|
||||
echo JAPL_VERSION_STRING
|
||||
echo &"[Nim {NimVersion} on {hostOs} ({hostCPU})]"
|
||||
when DEBUG_TRACE_VM:
|
||||
|
@ -65,6 +63,7 @@ proc main(file: var string = "", fromString: bool = false) =
|
|||
var source: string
|
||||
if file == "":
|
||||
repl()
|
||||
return # We exit after the REPL has ran
|
||||
elif not fromString:
|
||||
var sourceFile: File
|
||||
try:
|
||||
|
@ -80,7 +79,6 @@ proc main(file: var string = "", fromString: bool = false) =
|
|||
source = file
|
||||
file = "<string>"
|
||||
var bytecodeVM = initVM()
|
||||
bytecodeVM.stdlibInit()
|
||||
when DEBUG_TRACE_VM:
|
||||
echo "Debugger enabled, expect verbose output\n"
|
||||
echo "==== VM Constants ====\n"
|
||||
|
|
|
@ -205,9 +205,6 @@ proc scanToken(self: Lexer) =
|
|||
return
|
||||
elif single == '\n':
|
||||
self.line += 1
|
||||
# TODO: Fix this to only emit semicolons where needed
|
||||
# if self.tokens[^1].kind != TokenType.SEMICOLON:
|
||||
# self.tokens.add(self.createToken(TOKENS[';']))
|
||||
elif single in ['"', '\'']:
|
||||
self.parseString(single)
|
||||
elif single.isDigit():
|
||||
|
@ -247,8 +244,6 @@ proc lex*(self: Lexer): seq[Token] =
|
|||
while not self.done():
|
||||
self.start = self.current
|
||||
self.scanToken()
|
||||
# if self.tokens[^1].kind != TokenType.SEMICOLON:
|
||||
# self.tokens.add(self.createToken(TOKENS[';']))
|
||||
self.tokens.add(Token(kind: TokenType.EOF, lexeme: "EOF", line: self.line))
|
||||
return self.tokens
|
||||
|
||||
|
|
|
@ -14,20 +14,20 @@
|
|||
|
||||
# Implementations of builtin functions and modules
|
||||
|
||||
import vm
|
||||
import types/native
|
||||
import types/baseObject
|
||||
import types/numbers
|
||||
import types/methods
|
||||
import types/japlString
|
||||
import types/exception
|
||||
import types/native
|
||||
|
||||
import times
|
||||
import math
|
||||
import strformat
|
||||
|
||||
|
||||
proc natPrint(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
||||
|
||||
proc natPrint*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
|
||||
## Native function print
|
||||
## Prints an object representation
|
||||
## to stdout. If more than one argument
|
||||
|
@ -44,10 +44,10 @@ proc natPrint(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
|||
# Note: we return nil and not asNil() because
|
||||
# the VM will later use its own cached pointer
|
||||
# to nil
|
||||
return (ok: true, result: nil)
|
||||
return (kind: retNative.Nil, result: nil)
|
||||
|
||||
|
||||
proc natClock(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
||||
proc natClock*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
|
||||
## Native function clock
|
||||
## Returns the current unix
|
||||
## time (also known as epoch)
|
||||
|
@ -55,10 +55,10 @@ proc natClock(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
|||
|
||||
# TODO: Move this to a separate module once we have imports
|
||||
|
||||
result = (ok: true, result: getTime().toUnixFloat().asFloat())
|
||||
result = (kind: retNative.Object, result: getTime().toUnixFloat().asFloat())
|
||||
|
||||
|
||||
proc natRound(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
||||
proc natRound*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
|
||||
## Rounds a floating point number to a given
|
||||
## precision (when precision == 0, this function drops the
|
||||
## decimal part and returns an integer). Note that when
|
||||
|
@ -68,53 +68,39 @@ proc natRound(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
|||
var precision = 0
|
||||
if len(args) notin 1..2:
|
||||
# Here we need to return immediately to exit the procedure
|
||||
return (ok: false, result: newTypeError(&"function 'round' takes from 1 to 2 arguments, got {len(args)}"))
|
||||
return (kind: retNative.Exception, result: newTypeError(&"function 'round' takes from 1 to 2 arguments, got {len(args)}"))
|
||||
elif len(args) == 2:
|
||||
if not args[1].isInt():
|
||||
return (ok: false, result: newTypeError(&"precision must be of type 'int', not '{args[1].typeName()}'"))
|
||||
return (kind: retNative.Exception, result: newTypeError(&"precision must be of type 'int', not '{args[1].typeName()}'"))
|
||||
else:
|
||||
precision = args[1].toInt()
|
||||
if args[0].kind notin {ObjectType.Integer, ObjectType.Float}:
|
||||
return (ok: false, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
|
||||
return (kind: retNative.Exception, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
|
||||
if precision < 0:
|
||||
result = (ok: false, result: newTypeError(&"precision must be positive"))
|
||||
result = (kind: retNative.Exception, result: newTypeError(&"precision must be positive"))
|
||||
else:
|
||||
if args[0].isInt():
|
||||
result = (ok: true, result: args[0])
|
||||
result = (kind: retNative.Object, result: args[0])
|
||||
elif precision == 0:
|
||||
result = (ok: true, result: int(args[0].toFloat()).asInt())
|
||||
result = (kind: retNative.Object, result: int(args[0].toFloat()).asInt())
|
||||
else:
|
||||
result = (ok: true, result: round(args[0].toFloat(), precision).asFloat())
|
||||
result = (kind: retNative.Object, result: round(args[0].toFloat(), precision).asFloat())
|
||||
|
||||
|
||||
proc natToInt(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
||||
proc natToInt*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
|
||||
## Drops the decimal part of a float and returns an integer.
|
||||
## If the value is already an integer, the same object is returned
|
||||
if args[0].isInt():
|
||||
result = (ok: true, result: args[0])
|
||||
result = (kind: retNative.Object, result: args[0])
|
||||
elif args[0].isFloat():
|
||||
result = (ok: true, result: int(args[0].toFloat()).asInt())
|
||||
result = (kind: retNative.Object, result: int(args[0].toFloat()).asInt())
|
||||
else:
|
||||
result = (ok: false, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
|
||||
result = (kind: retNative.Exception, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
|
||||
|
||||
|
||||
proc natType(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
|
||||
proc natType*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
|
||||
## Returns the type of a given object as a string
|
||||
result = (ok: true, result: args[0].typeName().asStr())
|
||||
|
||||
|
||||
proc stdlibInit*(vm: VM) =
|
||||
## Initializes the VM's standard library by defining builtin
|
||||
## functions that do not require imports. An arity of -1
|
||||
## means that the function is variadic (or that it can
|
||||
## take a different number of arguments according to
|
||||
## how it's called) and should be handled by the nim
|
||||
## procedure accordingly
|
||||
vm.defineGlobal("print", newNative("print", natPrint, -1))
|
||||
vm.defineGlobal("clock", newNative("clock", natClock, 0))
|
||||
vm.defineGlobal("round", newNative("round", natRound, -1))
|
||||
vm.defineGlobal("toInt", newNative("toInt", natToInt, 1))
|
||||
vm.defineGlobal("type", newNative("type", natType, 1))
|
||||
result = (kind: retNative.Object, result: args[0].typeName().asStr())
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -59,5 +59,4 @@ proc newObj*(): ptr Obj =
|
|||
proc asObj*(self: ptr Obj): ptr Obj =
|
||||
## Casts a specific JAPL object into a generic
|
||||
## pointer to Obj
|
||||
|
||||
result = cast[ptr Obj](self)
|
||||
|
|
|
@ -18,6 +18,22 @@ import baseObject
|
|||
import japlString
|
||||
|
||||
|
||||
type retNative* {.pure.} = enum
|
||||
## Defines all the possible native
|
||||
## return types (this is useful
|
||||
## to keep singletons actually
|
||||
## singletons and makes it easier
|
||||
## to bring back exceptions as well)
|
||||
False,
|
||||
True,
|
||||
Inf,
|
||||
nInf,
|
||||
Nil,
|
||||
NotANumber,
|
||||
Object,
|
||||
Exception
|
||||
|
||||
|
||||
type
|
||||
Native* = object of Obj
|
||||
## A native object
|
||||
|
@ -25,10 +41,10 @@ type
|
|||
arity*: int # The number of required parameters
|
||||
optionals*: int # The number of optional parameters
|
||||
defaults*: seq[ptr Obj] # List of default arguments, in order
|
||||
nimproc*: proc (args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] # The function's body
|
||||
nimproc*: proc (args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] # The function's body
|
||||
|
||||
|
||||
proc newNative*(name: string, nimproc: proc(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj], arity: int = 0): ptr Native =
|
||||
proc newNative*(name: string, nimproc: proc(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj], arity: int = 0): ptr Native =
|
||||
## Allocates a new native object with the given
|
||||
## bytecode chunk and arity. If the name is an empty string
|
||||
## (the default), the function will be an
|
||||
|
|
77
src/vm.nim
77
src/vm.nim
|
@ -22,6 +22,7 @@ import strformat
|
|||
import tables
|
||||
import std/enumerate
|
||||
## Our modules
|
||||
import stdlib
|
||||
import memory
|
||||
import config
|
||||
import compiler
|
||||
|
@ -59,7 +60,7 @@ type
|
|||
stackTop*: int
|
||||
objects*: seq[ptr Obj]
|
||||
globals*: Table[string, ptr Obj]
|
||||
cached: array[5, ptr Obj]
|
||||
cached: array[6, ptr Obj]
|
||||
file*: string
|
||||
|
||||
|
||||
|
@ -243,21 +244,27 @@ proc call(self: VM, native: ptr Native, argCount: int): bool =
|
|||
for i in countup(slot, self.stack.high()):
|
||||
args.add(self.stack[i])
|
||||
let nativeResult = native.nimproc(args)
|
||||
if not nativeResult.ok:
|
||||
self.error(cast[ptr JaplException](nativeResult.result))
|
||||
return false
|
||||
# assumes that all native procs behave well, and if not ok, they
|
||||
# only return japl exceptions
|
||||
for i in countup(slot - 1, self.stack.high()):
|
||||
discard self.pop() # TODO once stack is a custom datatype,
|
||||
# just reduce its length
|
||||
if nativeResult.result == nil:
|
||||
# Since nil is a singleton, natives
|
||||
# don't reallocate it and we need to
|
||||
# reuse our cached object instead
|
||||
self.push(self.cached[2])
|
||||
else:
|
||||
self.push(nativeResult.result)
|
||||
case nativeResult.kind:
|
||||
of retNative.True:
|
||||
self.push(self.getBoolean(true))
|
||||
of retNative.False:
|
||||
self.push(self.getBoolean(false))
|
||||
of retNative.Object:
|
||||
self.push(nativeResult.result)
|
||||
of retNative.Nil:
|
||||
self.push(self.cached[2])
|
||||
of retNative.Inf:
|
||||
self.push(self.cached[3])
|
||||
of retNative.nInf:
|
||||
self.push(self.cached[4])
|
||||
of retNative.NotANumber:
|
||||
self.push(self.cached[5])
|
||||
of retNative.Exception:
|
||||
self.error(cast[ptr JaplException](nativeResult.result))
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
@ -322,30 +329,25 @@ proc run(self: VM, repl: bool): InterpretResult =
|
|||
## them one at a time: this is the runtime's
|
||||
## main loop
|
||||
var frame = self.frames[self.frameCount - 1]
|
||||
var instruction: uint8
|
||||
var opcode: OpCode
|
||||
when DEBUG_TRACE_VM:
|
||||
var iteration: int = 0
|
||||
while true:
|
||||
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
|
||||
instruction = frame.readByte()
|
||||
opcode = OpCode(instruction)
|
||||
opcode = OpCode(frame.readByte())
|
||||
when DEBUG_TRACE_VM: # Insight inside the VM
|
||||
stdout.write("Current VM stack status: [")
|
||||
iteration += 1
|
||||
stdout.write(&"Iteration N. {iteration}\nCurrent VM stack status: [")
|
||||
for i, v in self.stack:
|
||||
stdout.write(stringify(v))
|
||||
if i < self.stack.high():
|
||||
stdout.write(", ")
|
||||
stdout.write("]\n")
|
||||
stdout.write("Current global scope status: {")
|
||||
stdout.write("]\nCurrent global scope status: {")
|
||||
for i, (k, v) in enumerate(self.globals.pairs()):
|
||||
stdout.write("'")
|
||||
stdout.write(k)
|
||||
stdout.write("'")
|
||||
stdout.write(": ")
|
||||
stdout.write(stringify(v))
|
||||
stdout.write(&"'{k}': {stringify(v)}")
|
||||
if i < self.globals.len() - 1:
|
||||
stdout.write(", ")
|
||||
stdout.write("}\n")
|
||||
stdout.write("Current frame type: ")
|
||||
stdout.write("}\nCurrent frame type: ")
|
||||
if frame.function.name == nil:
|
||||
stdout.write("main\n")
|
||||
else:
|
||||
|
@ -358,8 +360,7 @@ proc run(self: VM, repl: bool): InterpretResult =
|
|||
stdout.write(stringify(e))
|
||||
if i < frame.function.chunk.consts.high():
|
||||
stdout.write(", ")
|
||||
stdout.write("]\n")
|
||||
stdout.write("Current frame stack status: ")
|
||||
stdout.write("]\nCurrent frame stack status: ")
|
||||
stdout.write("[")
|
||||
for i, e in frame.getView():
|
||||
stdout.write(stringify(e))
|
||||
|
@ -704,8 +705,27 @@ proc initCache(self: VM) =
|
|||
false.asBool().asObj(),
|
||||
asNil().asObj(),
|
||||
asInf().asObj(),
|
||||
nil,
|
||||
asNan().asObj()
|
||||
]
|
||||
# We cache -inf as well
|
||||
let nInf = asInf()
|
||||
nInf.isNegative = true
|
||||
self.cached[4] = nInf.asObj()
|
||||
|
||||
|
||||
proc stdlibInit*(vm: VM) =
|
||||
## Initializes the VM's standard library by defining builtin
|
||||
## functions that do not require imports. An arity of -1
|
||||
## means that the function is variadic (or that it can
|
||||
## take a different number of arguments according to
|
||||
## how it's called) and should be handled by the nim
|
||||
## procedure accordingly
|
||||
vm.defineGlobal("print", newNative("print", natPrint, -1))
|
||||
vm.defineGlobal("clock", newNative("clock", natClock, 0))
|
||||
vm.defineGlobal("round", newNative("round", natRound, -1))
|
||||
vm.defineGlobal("toInt", newNative("toInt", natToInt, 1))
|
||||
vm.defineGlobal("type", newNative("type", natType, 1))
|
||||
|
||||
|
||||
proc initVM*(): VM =
|
||||
|
@ -714,6 +734,7 @@ proc initVM*(): VM =
|
|||
var globals: Table[string, ptr Obj] = initTable[string, ptr Obj]()
|
||||
result = VM(lastPop: asNil(), objects: @[], globals: globals, source: "", file: "")
|
||||
result.initCache()
|
||||
result.stdlibInit()
|
||||
|
||||
|
||||
proc interpret*(self: VM, source: string, repl: bool = false, file: string): InterpretResult =
|
||||
|
|
|
@ -15,7 +15,7 @@ print(1 or 2 or 3 or 4);//output:1
|
|||
print(1 and 2 and 3 and 4);//output:4
|
||||
print(1 and 2 or 3 and 4);//output:2
|
||||
print(1 and false or 3 and 4);//output:4
|
||||
print(!0);//output:true
|
||||
print(!1);//output:false
|
||||
print(!1 and !2);//output:false
|
||||
print(!(1 and 0));//output:true
|
||||
print(not 0);//output:true
|
||||
print(not 1);//output:false
|
||||
print(not 1 and not 2);//output:false
|
||||
print(not (1 and 0));//output:true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
var x = 4;
|
||||
var y = x;
|
||||
|
||||
print(x is y);//output:false
|
||||
print(x is y);//output:true
|
||||
print(x is x);//output:true
|
||||
print(x is 4);//output:false
|
||||
|
||||
|
|
Loading…
Reference in New Issue