nan boxing

This commit is contained in:
prod2 2022-01-29 05:12:33 +01:00
parent 6961c91468
commit 2765daa55b
3 changed files with 150 additions and 100 deletions

View File

@ -7,6 +7,7 @@ import options
import scanner
import chunk
import value
import bitops # needed for value
import config
type
@ -351,7 +352,7 @@ proc expression(comp: Compiler) =
proc number(comp: Compiler) =
# assume the number is already advanced through
let value = comp.previous.text.parseFloat.toNdValue
let value = comp.previous.text.parseFloat.fromFloat()
comp.writeConstant(value)
when debugCompiler:
debugEcho &"Written constant (type: {value.ndType}, str repr: {$value}) to chunk"
@ -374,7 +375,7 @@ proc expNil(comp: Compiler) =
tkNil.genRule(expNil, nop, pcNone)
proc expString(comp: Compiler) =
let value = comp.previous.text[1..^2].toNdValue()
let value = comp.previous.text[1..^2].fromNimString()
comp.writeConstant(value)
when debugCompiler:
debugEcho &"Written constant (type: {value.ndType}, str repr: {$value}) to chunk"
@ -409,7 +410,7 @@ proc variable(comp: Compiler) =
setOp = opSetLocal
else:
# global
arg = comp.chunk.addConstant(name.toNdValue)
arg = comp.chunk.addConstant(name.fromNimString())
if comp.match(tkEqual):
# assignment (global/local)
@ -648,7 +649,7 @@ proc parseFunct(comp: Compiler) =
dec comp.stackIndex # the previous end scope did not put anything on the stack, it is jumped over
# get ndvalue functions:
let ntFunct = newNdFunction(functII)
let ntFunct = functII.fromFunct()
# end of function declaration:
comp.patchJump(jumpOverBody)
@ -707,7 +708,7 @@ proc parseVariable(comp: Compiler, msg: string): int =
0 # index to the constant is irrelevant if the var is local
else:
# global
comp.chunk.addConstant(name.toNdValue())
comp.chunk.addConstant(name.fromNimString())
proc defineVariable(comp: Compiler, index: int) =
## Generate code that moves the variable on the stack

212
value.nim
View File

@ -1,84 +1,131 @@
import strformat
import bitops
import types/ndstring
import types/stringutils
type
NdType* = enum
ntNil, ntBool, ntFloat, ntString,
ntFunct,
type
NdValue* = object
case ndType*: NdType:
of ntNil:
discard
of ntBool:
boolValue*: bool
of ntFloat:
floatValue*: float64
of ntString:
stringValue*: NdString
of ntFunct:
entryII*: int # entry instruction index
NdValue* = uint
NatReturn* = object
ok*: bool
msg*: string
# NaN boxing constants
# see https://craftinginterpreters.com/optimization.html
# bit layout:
# bit 63 is unused (can be used for more types in the future)
# bits 62-50 are all 1 if it's not a float
# bits 49-48 determine type:
# 00 -> nil or bool (singletons)
# 01 -> string
# 10 -> funct
const qNan* = 0x7ffc000000000000'u
const ndNil* = 0x7ffc000000000001'u
const ndTrue* = 0x7ffc000000000002'u
const ndFalse* = 0x7ffc000000000003'u
# 0111 1111 1111 11*01* 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
const tagString* = 0x7ffd000000000000'u
# 0111 1111 1111 11*10* 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
const tagFunct* = 0x7ffe000000000000'u
const mask48* = 0xffff000000000000'u
# converters
template isNil*(val: NdValue): bool =
val == ndNil
template isBool*(val: NdValue): bool =
(val.bitor(1'u)) == ndFalse
template isFloat*(val: NdValue): bool =
val.bitand(qNan) != qNan
template isString*(val: NdValue): bool =
val.bitand(mask48) == tagString
template isFunct*(val: NdValue): bool =
val.bitand(mask48) == tagFunct
# these assume that the type has been previously determined
template asBool*(val: NdValue): bool =
val == ndTrue
template asFloat*(val: NdValue): float =
cast[float64](val)
template asString*(val: NdValue): NdString =
cast[NdString](val.bitand(mask48.bitnot()))
template asFunct*(val: NdValue): int =
(val.bitand(mask48.bitnot())).int
template fromNil*(): NdValue =
ndNil
template fromBool*(val: bool): NdValue =
if val: ndTrue else: ndFalse
template fromFloat*(val: float): NdValue =
cast[NdValue](val)
template fromNdString*(val: NdString): NdValue =
cast[uint](val).bitor(tagString)
template fromNimString*(sval: string): NdValue =
fromNdString(newString(sval))
template fromFunct*(val: int): NdValue =
cast[uint](val).bitor(tagFunct)
# KON VALUE HELPERS, MUST BE DEFINED FOR EVERY KONVALUE
proc `$`*(val: NdValue): string =
case val.ndType:
of ntFloat:
return $val.floatValue
of ntBool:
return $val.boolValue
of ntNil:
return "nil"
of ntString:
return $val.stringValue
of ntFunct:
return &"Function object: {val.entryII}"
if val.isNil():
return "nil"
elif val == ndTrue:
return "true"
elif val == ndFalse:
return "false"
elif val.isFloat():
return $val.asFloat()
elif val.isFunct():
return &"Function object {val.asFunct()}"
elif val.isString():
return $val.asString()
proc isFalsey*(val: NdValue): bool =
val.ndType in {ntNil} or (val.ndType == ntBool and not val.boolValue)
val == ndNil or val == ndFalse
template isTruthy*(val: NdValue): bool =
not isFalsey(val)
not val.isFalsey()
proc equal*(val, right: NdValue): bool =
if val.ndType != right.ndType:
false
if val.isFloat() and right.isFloat():
val.asFloat() == right.asFloat()
else:
case val.ndType:
of ntFloat:
val.floatValue == right.floatValue
of ntBool:
val.boolValue == right.boolValue
of ntNil:
true
of ntString:
# TODO this was meant for nim strings, not ndStrings, FIXME!
val.stringValue == right.stringValue
of ntFunct:
val.entryII == right.entryII
val == right
# NIM VALUE TO KON VALUE WRAPPERS
proc toNdValue*(val: float): NdValue =
NdValue(ndType: ntFloat, floatValue: val)
proc toNdValue*(val: bool): NdValue =
NdValue(ndType: ntBool, boolValue: val)
proc toNdValue*(val: string): NdValue =
NdValue(ndType: ntString, stringValue: val.newString())
proc newNdFunction*(ii: int): NdValue =
NdValue(ndType: ntFunct, entryII: ii)
proc toNdValue*: NdValue =
NdValue(ndType: ntNil)
proc friendlyType*(val: NdValue): string =
if val == ndNil:
"nil"
elif val.isBool():
"bool"
elif val.isFloat():
"number"
elif val.isString():
"string"
elif val.isFunct():
"function"
else:
"unknown"
# NatReturn misc
@ -88,55 +135,54 @@ proc natError*(msg: string): NatReturn {.inline.} =
const natOk* = NatReturn(ok: true)
# OPERATIONS
# NOTE: these operations can return ktTypeError with a message if types are invalid
proc negate*(val: var NdValue): NatReturn {.inline.} =
if (val.ndType != ntFloat):
if not val.isFloat():
return natError("Operand must be a number.")
else:
val.floatValue = -val.floatValue
val = fromFloat(-val.asFloat())
return natOk
proc add*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val.floatValue += right.floatValue
elif val.ndType == ntString and right.ndType == ntString:
val.stringValue = val.stringValue & right.stringValue
if val.isFloat() and right.isFloat():
val = fromFloat(val.asFloat() + right.asFloat())
elif val.isString() and right.isString():
val = fromNdString(val.asString() & val.asString())
else:
return natError(&"Attempt to add types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to add types {val.friendlyType()} and {right.friendlyType()}.")
return natOk
proc subtract*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val.floatValue -= right.floatValue
if val.isFloat() and right.isFloat():
val = fromFloat(val.asFloat() - right.asFloat())
else:
return natError(&"Attempt to subtract types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to subtract types {val.friendlyType()} and {right.friendlyType()}.")
return natOk
proc multiply*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val.floatValue *= right.floatValue
if val.isFloat() and right.isFloat():
val = fromFloat(val.asFloat() * right.asFloat())
else:
return natError(&"Attempt to multiply types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to multiply types {val.friendlyType()} and {right.friendlyType()}.")
return natOk
proc divide*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val.floatValue /= right.floatValue
if val.isFloat() and right.isFloat():
val = fromFloat(val.asFloat() / right.asFloat())
else:
return natError(&"Attempt to divide types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to divide types {val.friendlyType()} and {right.friendlyType()}.")
return natOk
proc less*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val = toNdValue(val.floatValue < right.floatValue)
if val.isFloat() and right.isFloat():
val = fromBool(val.asFloat() < right.asFloat())
else:
return natError(&"Attempt to compare types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to compare types {val.friendlyType()} and {right.friendlyType()}.")
return natOk
proc greater*(val: var NdValue, right: NdValue): NatReturn {.inline.} =
if val.ndType == ntFloat and right.ndType == ntFloat:
val = toNdValue(val.floatValue > right.floatValue)
if val.isFloat() and right.isFloat():
val = fromBool(val.asFloat() > right.asFloat())
else:
return natError(&"Attempt to compare types {val.ndType} and {right.ndType}.")
return natError(&"Attempt to compare types {val.friendlyType()} and {right.friendlyType()}.")
return natOk

27
vm.nim
View File

@ -2,6 +2,8 @@ import strformat
import tables
import value
import bitops # needed for value's templates
import chunk
import config
import pointerutils
@ -10,6 +12,7 @@ import types/stack
import types/ndstring
import types/stringutils
when profileInstructions:
import times
import std/monotimes
@ -140,15 +143,15 @@ proc run*(chunk: Chunk): InterpretResult =
else:
ip = frames.pop().returnIp # remove frame that's over
of opTrue:
stack.add(toNdValue(true))
stack.add(fromBool(true))
of opFalse:
stack.add(toNdValue(false))
stack.add(fromBool(false))
of opNil:
stack.add(toNdValue())
of opNot:
stack.add(toNdValue(stack.pop().isFalsey))
stack.add(fromNil())
of opNot: # TODO hayago += optimization
stack.add(fromBool(stack.pop().isFalsey()))
of opEqual:
stack.add(toNdValue(stack.pop().equal(stack.pop())))
stack.add(fromBool(stack.pop().equal(stack.pop())))
of opLess:
let right = stack.pop()
let res = stack.peek().less(right)
@ -164,21 +167,21 @@ proc run*(chunk: Chunk): InterpretResult =
of opPrint:
echo $stack.peek()
of opDefineGlobal:
let name = $readConstant().stringValue
let name = $readConstant().asString()
if not globals.hasKey(name):
globals[name] = stack.pop()
else:
runtimeError("Attempt to redefine an existing global variable.")
break
of opGetGlobal:
let name = $readConstant().stringValue
let name = $readConstant().asString()
if globals.hasKey(name):
stack.add(globals[name])
else:
runtimeError(&"Undefined global variable {name}.")
break
of opSetGlobal:
let name = $readConstant().stringValue
let name = $readConstant().asString()
if globals.hasKey(name):
globals[name] = stack.peek()
else:
@ -218,16 +221,16 @@ proc run*(chunk: Chunk): InterpretResult =
# ... <ret val> <arg1> <arg2> <arg3>
let argcount = readUI8()
let funct = stack.getIndexNeg(argcount)
if funct.ndType != ntFunct:
if not funct.isFunct():
runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke
break
stack.setIndexNeg(argcount, toNdValue()) # replace the function with nil: this is the return value slot
stack.setIndexNeg(argcount, fromNil()) # replace the function with nil: this is the return value slot
# create new frame
frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip))
ip = chunk.code[0].unsafeAddr.padd(funct.entryII) # jump to the entry point
ip = chunk.code[0].unsafeAddr.padd(funct.asFunct()) # jump to the entry point
when profileInstructions:
durations[ins] += getMonoTime() - startTime