cleanup
This commit is contained in:
parent
ada87cb01b
commit
88cc276100
116
chunk.nim
116
chunk.nim
|
@ -1,116 +0,0 @@
|
||||||
import strformat
|
|
||||||
import strutils
|
|
||||||
|
|
||||||
import value
|
|
||||||
|
|
||||||
type
|
|
||||||
OpCode* = enum
|
|
||||||
opReturn, opCall, # functions
|
|
||||||
opPop, # pop
|
|
||||||
opPrint, # print
|
|
||||||
opNegate, opNot # unary
|
|
||||||
opAdd, opSubtract, opMultiply, opDivide, # math
|
|
||||||
opEqual, opGreater, opLess, # comparison
|
|
||||||
opTrue, opFalse, opNil, # literal
|
|
||||||
opConstant, # constant
|
|
||||||
opDefineGlobal, opGetGlobal, opSetGlobal, # globals (uses constants)
|
|
||||||
opGetLocal, opSetLocal, # locals
|
|
||||||
opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop, # jumps
|
|
||||||
|
|
||||||
Chunk* = object
|
|
||||||
code*: seq[uint8]
|
|
||||||
constants*: seq[KonValue]
|
|
||||||
lines*: seq[int]
|
|
||||||
name*: string # name of the module/chunk/files
|
|
||||||
|
|
||||||
DoubleUint8* = array[2, uint8]
|
|
||||||
|
|
||||||
const argSize* = 2
|
|
||||||
const argMax*: int = 256*256
|
|
||||||
|
|
||||||
proc initChunk*(name: string): Chunk =
|
|
||||||
Chunk(code: @[], name: name, lines: @[], constants: @[])
|
|
||||||
|
|
||||||
proc writeChunk*(ch: var Chunk, code: uint8, line: int) =
|
|
||||||
ch.code.add(code)
|
|
||||||
ch.lines.add(line)
|
|
||||||
|
|
||||||
proc writeChunk*(ch: var Chunk, code: OpCode, line: int) =
|
|
||||||
ch.code.add(code.uint8)
|
|
||||||
ch.lines.add(line)
|
|
||||||
|
|
||||||
proc writeChunk*(ch: var Chunk, code: DoubleUint8, line: int) =
|
|
||||||
for c in code:
|
|
||||||
ch.code.add(c)
|
|
||||||
ch.lines.add(line)
|
|
||||||
|
|
||||||
proc len*(ch: Chunk): int =
|
|
||||||
ch.code.len
|
|
||||||
|
|
||||||
proc toDU8*(integ: int): DoubleUint8 =
|
|
||||||
cast[ptr array[2, uint8]](integ.unsafeAddr)[]
|
|
||||||
|
|
||||||
proc toInt*(du8: DoubleUint8): int =
|
|
||||||
cast[uint16](du8).int
|
|
||||||
|
|
||||||
proc DU8ptrToInt*(du8: ptr uint8): int =
|
|
||||||
cast[ptr uint16](du8)[].int
|
|
||||||
|
|
||||||
proc addConstant*(ch: var Chunk, constant: KonValue): int =
|
|
||||||
ch.constants.add(constant)
|
|
||||||
ch.constants.high
|
|
||||||
|
|
||||||
proc writeConstant*(ch: var Chunk, constant: KonValue, line: int): int =
|
|
||||||
result = ch.addConstant(constant)
|
|
||||||
ch.writeChunk(opConstant, line)
|
|
||||||
ch.writeChunk(result.toDU8, line)
|
|
||||||
|
|
||||||
const simpleInstructions = {
|
|
||||||
opReturn,
|
|
||||||
opPop,
|
|
||||||
opPrint,
|
|
||||||
opNegate, opNot,
|
|
||||||
opAdd, opSubtract, opMultiply, opDivide,
|
|
||||||
opEqual, opGreater, opLess,
|
|
||||||
opTrue, opFalse, opNil
|
|
||||||
}
|
|
||||||
const constantInstructions = {
|
|
||||||
opConstant,
|
|
||||||
opDefineGlobal, opGetGlobal, opSetGlobal,
|
|
||||||
}
|
|
||||||
const argInstructions = {
|
|
||||||
opCall,
|
|
||||||
opGetLocal, opSetLocal,
|
|
||||||
opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
proc disassembleChunk*(ch: Chunk) =
|
|
||||||
echo &"== Chunk {ch.name} begin =="
|
|
||||||
echo "index line instruction"
|
|
||||||
var c: int = 0
|
|
||||||
var lastLine = -1
|
|
||||||
while c < ch.code.len:
|
|
||||||
template instruction: uint8 = ch.code[c]
|
|
||||||
template line: int = ch.lines[c]
|
|
||||||
template double: DoubleUint8 = [ch.code[c+1], ch.code[c+2]]
|
|
||||||
let cFmt = &"{c:04}"
|
|
||||||
let lineFmt = if lastLine == line: " | " else: &"{line:04}"
|
|
||||||
try:
|
|
||||||
write stdout, &"[{cFmt}] {lineFmt} {instruction.OpCode} ({instruction.toHex(2)}"
|
|
||||||
case instruction.OpCode:
|
|
||||||
of simpleInstructions:
|
|
||||||
write stdout, ")\n"
|
|
||||||
of argInstructions:
|
|
||||||
write stdout, &" {double[0].toHex(2)} {double[1].toHex(2)})\n"
|
|
||||||
c += 2
|
|
||||||
of constantInstructions:
|
|
||||||
let i = double.toInt
|
|
||||||
write stdout, &" {double[0].toHex(2)} {double[1].toHex(2)})\n"
|
|
||||||
echo &" points to constant {ch.constants[i]} (i: {i})"
|
|
||||||
c += 2
|
|
||||||
except:
|
|
||||||
echo &"[{cFmt}] {lineFmt} Unknown opcode {instruction}"
|
|
||||||
c.inc
|
|
||||||
echo &"== Chunk {ch.name} end =="
|
|
||||||
|
|
762
compiler.nim
762
compiler.nim
|
@ -1,762 +0,0 @@
|
||||||
import strformat
|
|
||||||
import strutils
|
|
||||||
import sugar
|
|
||||||
import tables
|
|
||||||
import options
|
|
||||||
|
|
||||||
import scanner
|
|
||||||
import chunk
|
|
||||||
import value
|
|
||||||
import config
|
|
||||||
|
|
||||||
type
|
|
||||||
Local = object
|
|
||||||
name: string # name of this local
|
|
||||||
index: int # what is its index in the stack (0 is the stack bottom - nil in the main function)
|
|
||||||
depth: int # depth of this local
|
|
||||||
# if depth is -1, the variable cannot be referenced yet
|
|
||||||
# its depth will be set once its first ever value is determined
|
|
||||||
|
|
||||||
Scope = ref object
|
|
||||||
labels: seq[string]
|
|
||||||
#depth: int
|
|
||||||
goalStackIndex: int # the stack count it started with plus 1
|
|
||||||
jumps: seq[int] # jumps to be patched that jump to the end
|
|
||||||
function: bool # if true, it is a function
|
|
||||||
|
|
||||||
Compiler = ref object
|
|
||||||
#
|
|
||||||
# input
|
|
||||||
#
|
|
||||||
scanner: Scanner
|
|
||||||
source: string
|
|
||||||
#
|
|
||||||
# state
|
|
||||||
#
|
|
||||||
previous: Token
|
|
||||||
current: Token
|
|
||||||
canAssign: bool
|
|
||||||
locals: seq[Local]
|
|
||||||
scopes: seq[Scope]
|
|
||||||
stackIndex: int # how large the stack is
|
|
||||||
# when there's an error both are set
|
|
||||||
# panic mode can be turned off e.g. at block boundaries
|
|
||||||
panicMode: bool
|
|
||||||
#
|
|
||||||
# output
|
|
||||||
#
|
|
||||||
chunk*: Chunk
|
|
||||||
hadError*: bool
|
|
||||||
|
|
||||||
Precedence = enum
|
|
||||||
pcNone, pcAssignment, pcOr, pcAnd, pcEquality, pcComparison,
|
|
||||||
pcTerm, pcFactor, pcUnary, pcCall, pcPrimary
|
|
||||||
# pcUnary applies to all prefix operators regardless of this enum's value
|
|
||||||
# changing pcUnary's position can change the priority of all unary ops
|
|
||||||
#
|
|
||||||
# Note: unary only rules should have precedence pcNone!!!
|
|
||||||
|
|
||||||
ParseRule = object
|
|
||||||
name: string # debug purposes only
|
|
||||||
prefix: (Compiler) -> void
|
|
||||||
infix: (Compiler) -> void
|
|
||||||
prec: Precedence # only relevant to infix, prefix always has pcUnary
|
|
||||||
|
|
||||||
proc newScope(comp: Compiler, function: bool): Scope =
|
|
||||||
result.new()
|
|
||||||
#result.depth = comp.scopes.len + 1
|
|
||||||
result.function = function
|
|
||||||
result.goalStackIndex = comp.stackIndex + 1
|
|
||||||
comp.scopes.add(result)
|
|
||||||
|
|
||||||
# HELPERS FOR THE COMPILER TYPE
|
|
||||||
|
|
||||||
proc newCompiler*(name: string, source: string): Compiler =
|
|
||||||
result = new(Compiler)
|
|
||||||
result.chunk = initChunk(name)
|
|
||||||
result.source = source
|
|
||||||
result.hadError = false
|
|
||||||
result.panicMode = false
|
|
||||||
result.canAssign = true
|
|
||||||
result.locals = @[]
|
|
||||||
result.scopes = @[]
|
|
||||||
|
|
||||||
proc errorAt(comp: Compiler, line: int, msg: string, at: string = "") =
|
|
||||||
if comp.panicMode:
|
|
||||||
return
|
|
||||||
write stderr, &"[line {line}] Error "
|
|
||||||
if at.len > 0:
|
|
||||||
write stderr, &"at {at} "
|
|
||||||
write stderr, msg
|
|
||||||
write stderr, "\n"
|
|
||||||
comp.hadError = true
|
|
||||||
comp.panicMode = true
|
|
||||||
|
|
||||||
proc error(comp: Compiler, msg: string) =
|
|
||||||
## create a simple error message
|
|
||||||
comp.errorAt(comp.previous.line, msg)
|
|
||||||
|
|
||||||
proc errorAtCurrent(comp: Compiler, msg: string) =
|
|
||||||
comp.errorAt(comp.current.line, msg)
|
|
||||||
|
|
||||||
proc advance(comp: Compiler) =
|
|
||||||
comp.previous = comp.current
|
|
||||||
while true:
|
|
||||||
comp.current = comp.scanner.scanToken()
|
|
||||||
when debugScanner:
|
|
||||||
comp.current.debugPrint()
|
|
||||||
if (comp.current.tokenType != tkError):
|
|
||||||
break
|
|
||||||
comp.errorAtCurrent(comp.current.text)
|
|
||||||
|
|
||||||
proc match(comp: Compiler, tokenType: TokenType): bool =
|
|
||||||
if comp.current.tokenType == tokenType:
|
|
||||||
comp.advance()
|
|
||||||
true
|
|
||||||
else:
|
|
||||||
false
|
|
||||||
|
|
||||||
proc consume(comp: Compiler, tokenType: TokenType, msg: string) =
|
|
||||||
if comp.current.tokenType == tokenType:
|
|
||||||
comp.advance()
|
|
||||||
else:
|
|
||||||
comp.errorAtCurrent(msg)
|
|
||||||
|
|
||||||
proc synchronize(comp: Compiler) =
|
|
||||||
comp.panicMode = false
|
|
||||||
while comp.current.tokenType != tkEof:
|
|
||||||
if comp.previous.tokenType in {tkSemicolon, tkRightBrace}:
|
|
||||||
return
|
|
||||||
if comp.current.tokenType in {tkFunct, tkVar, tkFor, tkIf, tkWhile}:
|
|
||||||
return
|
|
||||||
comp.advance()
|
|
||||||
|
|
||||||
proc writeChunk(comp: Compiler, dStackIndex: int, ch: OpCode | DoubleUint8) =
|
|
||||||
comp.stackIndex += dStackIndex
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"new stackindex: {comp.stackIndex}, delta: {dStackIndex} due to {ch.repr}"
|
|
||||||
comp.chunk.writeChunk(ch, comp.previous.line)
|
|
||||||
|
|
||||||
proc writeConstant(comp: Compiler, constant: KonValue) =
|
|
||||||
comp.stackIndex.inc
|
|
||||||
let index = comp.chunk.writeConstant(constant, comp.previous.line)
|
|
||||||
if index >= argMax:
|
|
||||||
comp.error("Too many constants in one chunk.")
|
|
||||||
|
|
||||||
|
|
||||||
proc addLocal(comp: Compiler, name: string, delta: int) =
|
|
||||||
if comp.locals.len >= argMax:
|
|
||||||
comp.error("Too many local variables in function.")
|
|
||||||
|
|
||||||
# if delta is 0 or negative, it means that it is already on the stack when addLocal is called
|
|
||||||
# if delta is positive, the first ever value of the local is to the right
|
|
||||||
comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta))
|
|
||||||
|
|
||||||
proc markInitialized(comp: Compiler) =
|
|
||||||
comp.locals[comp.locals.high].depth = comp.scopes.high
|
|
||||||
|
|
||||||
# PARSE RULE/PRECEDENCE MISC
|
|
||||||
proc nop(comp: Compiler) = discard
|
|
||||||
|
|
||||||
var rules: Table[TokenType, ParseRule]
|
|
||||||
template genRule(ttype: TokenType, tprefix: (Compiler) -> void, tinfix: (Compiler) -> void, tprec: Precedence) =
|
|
||||||
if tprec == pcUnary:
|
|
||||||
raise newException(Exception, "pcUnary cannot be used as a rule precedence! Use pcNone for unary-only rules!")
|
|
||||||
elif tprec == pcPrimary:
|
|
||||||
raise newException(Exception, "Invalid rule: pcPrimary cannot be used for binary operators, if this rule is for a primary value, use pcNone!")
|
|
||||||
elif tprec == pcNone and tinfix != nop:
|
|
||||||
raise newException(Exception, "Invalid rule: pcNone only allowed for unary operators and primary values, not for infix ones!")
|
|
||||||
rules[ttype] = ParseRule(name: $ttype, prefix: tprefix, infix: tinfix, prec: tprec)
|
|
||||||
|
|
||||||
proc getRule(opType: TokenType): Option[ParseRule] =
|
|
||||||
if rules.hasKey(opType):
|
|
||||||
some(rules[opType])
|
|
||||||
else:
|
|
||||||
none(ParseRule)
|
|
||||||
|
|
||||||
proc applyRule(rule: ParseRule): Precedence =
|
|
||||||
# returns the rule's precedence
|
|
||||||
rule.prec
|
|
||||||
|
|
||||||
proc increment(prec: Precedence): Precedence =
|
|
||||||
# increases precedence by one
|
|
||||||
if prec == pcPrimary:
|
|
||||||
raise newException(Exception, "Invalid ruletable, pcPrimary precedence increment attempted.")
|
|
||||||
else:
|
|
||||||
Precedence(int(prec) + 1)
|
|
||||||
|
|
||||||
proc `<=`(a, b: Precedence): bool =
|
|
||||||
int(a) <= int(b)
|
|
||||||
|
|
||||||
# JUMP HELPERS
|
|
||||||
|
|
||||||
proc emitJump(comp: Compiler, delta: int, op: OpCode): int =
|
|
||||||
# delta -> 0 if the jump does not pop
|
|
||||||
# delta -> -1 if the jump pops the condition from the stack
|
|
||||||
comp.writeChunk(delta, op)
|
|
||||||
comp.writeChunk(0, 0xffffff.toDU8)
|
|
||||||
comp.chunk.len - argSize
|
|
||||||
|
|
||||||
proc patchJump(comp: Compiler, offset: int) =
|
|
||||||
let jump = (comp.chunk.len - offset - argSize)
|
|
||||||
|
|
||||||
if (jump > argMax):
|
|
||||||
comp.error("Too much code to jump over.")
|
|
||||||
|
|
||||||
let jumpt = jump.toDU8
|
|
||||||
|
|
||||||
comp.chunk.code[offset] = jumpt[0]
|
|
||||||
comp.chunk.code[offset + 1] = jumpt[1]
|
|
||||||
|
|
||||||
proc emitLoop(comp: Compiler, loopstart: int, delta: int, op: OpCode) =
|
|
||||||
comp.writeChunk(delta, op)
|
|
||||||
|
|
||||||
let offset = comp.chunk.len - loopstart + argSize
|
|
||||||
if offset > argMax:
|
|
||||||
comp.error("Loop body too large.")
|
|
||||||
|
|
||||||
comp.writeChunk(0, offset.toDU8)
|
|
||||||
|
|
||||||
# SCOPE HELPERS
|
|
||||||
proc beginScope(comp: Compiler, function: bool = false) =
|
|
||||||
let scope = comp.newScope(function)
|
|
||||||
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"Begin scope called for depth {comp.scopes.len} function? {function}"
|
|
||||||
|
|
||||||
if function:
|
|
||||||
scope.labels.add("result")
|
|
||||||
else:
|
|
||||||
while comp.match(tkLabel):
|
|
||||||
let label = comp.previous.text[1..^1]
|
|
||||||
scope.labels.add(label)
|
|
||||||
|
|
||||||
if function:
|
|
||||||
# if it's a function scope, the frame will move
|
|
||||||
# access to outside locals is also limited to upvalues and closures
|
|
||||||
comp.stackIndex = 0
|
|
||||||
else:
|
|
||||||
# only put the opNil if it's not a function scope, since
|
|
||||||
# function scopes are initialized by the caller
|
|
||||||
comp.writeChunk(1, opNil)
|
|
||||||
|
|
||||||
comp.addLocal("^", delta = 0)
|
|
||||||
# note, that later added scope labels will not be accessible through ^
|
|
||||||
# so it is better not to assign scope labels after beginScope
|
|
||||||
for label in scope.labels:
|
|
||||||
comp.addLocal(&"^{label}", delta = 0)
|
|
||||||
|
|
||||||
proc restore(comp: Compiler, scope: Scope) =
|
|
||||||
let delta = comp.stackIndex - scope.goalStackIndex
|
|
||||||
for i in countup(1, delta):
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
when assertionsCompiler:
|
|
||||||
if not comp.stackIndex == scope.goalStackIndex:
|
|
||||||
comp.error("Assertion failed in restore")
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"Restored scope: delta {delta}"
|
|
||||||
|
|
||||||
proc restoreInFunct(comp: Compiler, scope: Scope) =
|
|
||||||
when debugCompiler:
|
|
||||||
var pops = 0
|
|
||||||
while comp.stackIndex > 0:
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
when debugCompiler:
|
|
||||||
inc pops
|
|
||||||
when assertionsCompiler:
|
|
||||||
if not comp.stackIndex == 0:
|
|
||||||
comp.error("Assertion failed in restoreInFunct")
|
|
||||||
|
|
||||||
comp.stackIndex = scope.goalStackIndex
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"Restored function scope: delta {pops}; new stackindex: {comp.stackIndex}"
|
|
||||||
|
|
||||||
proc jumpToEnd(comp: Compiler, scope: Scope) =
|
|
||||||
let delta = comp.stackIndex - scope.goalStackIndex
|
|
||||||
for i in countup(1, delta):
|
|
||||||
comp.writeChunk(0, opPop)
|
|
||||||
let jmp = comp.emitJump(0, opJump)
|
|
||||||
scope.jumps.add(jmp)
|
|
||||||
|
|
||||||
proc endScope(comp: Compiler) =
|
|
||||||
# remove locals
|
|
||||||
let popped = comp.scopes.pop()
|
|
||||||
let function = popped.function
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"End scope called for depth {comp.scopes.len} function? {function}"
|
|
||||||
if function:
|
|
||||||
comp.restoreInFunct(popped)
|
|
||||||
else:
|
|
||||||
comp.restore(popped)
|
|
||||||
# patch jumps to after the scope (such jumps from breaks emit the pops before jumping)
|
|
||||||
for jump in popped.jumps:
|
|
||||||
comp.patchJump(jump)
|
|
||||||
if function:
|
|
||||||
comp.writeChunk(0, opReturn)
|
|
||||||
|
|
||||||
|
|
||||||
# EXPRESSIONS
|
|
||||||
proc parsePrecedence(comp: Compiler, prec: Precedence) =
|
|
||||||
comp.advance()
|
|
||||||
let rule = comp.previous.tokenType.getRule
|
|
||||||
|
|
||||||
if rule.isSome and rule.get.prefix != nop:
|
|
||||||
comp.canAssign = prec <= pcAssignment
|
|
||||||
|
|
||||||
let rule = rule.get
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"parsePrecedence call, valid prefix op found, rule used: {rule.name}, precedence: {prec}"
|
|
||||||
|
|
||||||
rule.prefix(comp)
|
|
||||||
|
|
||||||
while comp.current.tokenType.getRule.isSome and
|
|
||||||
prec <= comp.current.tokenType.getRule.get.prec:
|
|
||||||
comp.advance()
|
|
||||||
# checked for isSome in the loop
|
|
||||||
# since advance moves current to previous
|
|
||||||
if comp.previous.tokenType.getRule.get.infix == nop:
|
|
||||||
# should never happen, as having a precedence set
|
|
||||||
# means that it is a binary op
|
|
||||||
comp.error("Invalid rule table.")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
let infixRule = comp.previous.tokenType.getRule.get.infix
|
|
||||||
infixRule(comp)
|
|
||||||
else:
|
|
||||||
comp.error("Expect expression.")
|
|
||||||
|
|
||||||
proc expression(comp: Compiler) =
|
|
||||||
## The lowest precedence among the pratt-parsed expressions
|
|
||||||
|
|
||||||
# DO NOT ADD ANYTHING HERE
|
|
||||||
# only use the pratt table for parsing expressions!
|
|
||||||
when assertionsVM:
|
|
||||||
let oldStackIndex = comp.stackIndex
|
|
||||||
comp.parsePrecedence(pcAssignment)
|
|
||||||
when assertionsVM:
|
|
||||||
let diff = comp.stackIndex - oldStackIndex
|
|
||||||
if diff != 1:
|
|
||||||
comp.error(&"Assertion failed: expression increased ({oldStackIndex} -> {comp.stackIndex}) the stack index by {diff} (should be 1).")
|
|
||||||
|
|
||||||
|
|
||||||
proc number(comp: Compiler) =
|
|
||||||
# assume the number is already advanced through
|
|
||||||
let value = comp.previous.text.parseFloat.toKonValue
|
|
||||||
comp.writeConstant(value)
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"Written constant (type: {value.konType}, str repr: {$value}) to chunk"
|
|
||||||
|
|
||||||
tkNumber.genRule(number, nop, pcNone)
|
|
||||||
|
|
||||||
proc expFalse(comp: Compiler) =
|
|
||||||
comp.writeChunk(1, opFalse)
|
|
||||||
|
|
||||||
tkFalse.genRule(expFalse, nop, pcNone)
|
|
||||||
|
|
||||||
proc expTrue(comp: Compiler) =
|
|
||||||
comp.writeChunk(1, opTrue)
|
|
||||||
|
|
||||||
tkTrue.genRule(expTrue, nop, pcNone)
|
|
||||||
|
|
||||||
proc expNil(comp: Compiler) =
|
|
||||||
comp.writeChunk(1, opNil)
|
|
||||||
|
|
||||||
tkNil.genRule(expNil, nop, pcNone)
|
|
||||||
|
|
||||||
proc expString(comp: Compiler) =
|
|
||||||
let value = comp.previous.text[1..^2].toKonValue()
|
|
||||||
comp.writeConstant(value)
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"Written constant (type: {value.konType}, str repr: {$value}) to chunk"
|
|
||||||
|
|
||||||
tkString.genRule(expString, nop, pcNone)
|
|
||||||
|
|
||||||
proc resolveLocal(comp: Compiler, name: string): int =
|
|
||||||
## returns the stack index of the local of the name
|
|
||||||
var i = comp.locals.high
|
|
||||||
while i >= 0:
|
|
||||||
let local = comp.locals[i]
|
|
||||||
if local.name == name:
|
|
||||||
if local.depth == -1:
|
|
||||||
comp.error("Can't read local variable in its own initializer.")
|
|
||||||
return local.index
|
|
||||||
i.dec
|
|
||||||
return -1
|
|
||||||
|
|
||||||
proc variable(comp: Compiler) =
|
|
||||||
# named variable
|
|
||||||
var getOp = opGetGlobal
|
|
||||||
var setOp = opSetGlobal
|
|
||||||
|
|
||||||
let name = comp.previous.text
|
|
||||||
|
|
||||||
# try resolving local, set arg to the index on the stack
|
|
||||||
var arg = comp.resolveLocal(name)
|
|
||||||
|
|
||||||
if arg != -1:
|
|
||||||
# local
|
|
||||||
getOp = opGetLocal
|
|
||||||
setOp = opSetLocal
|
|
||||||
else:
|
|
||||||
# global
|
|
||||||
arg = comp.chunk.addConstant(name.toKonValue)
|
|
||||||
|
|
||||||
if comp.match(tkEqual):
|
|
||||||
# assignment (global/local)
|
|
||||||
if not comp.canAssign:
|
|
||||||
comp.error("Invalid assignment target.")
|
|
||||||
return
|
|
||||||
comp.expression()
|
|
||||||
comp.writeChunk(0, setOp)
|
|
||||||
else:
|
|
||||||
# get (global/local)
|
|
||||||
comp.writeChunk(1, getOp)
|
|
||||||
|
|
||||||
comp.writeChunk(0, arg.toDU8)
|
|
||||||
|
|
||||||
tkIdentifier.genRule(variable, nop, pcNone)
|
|
||||||
|
|
||||||
proc grouping(comp: Compiler) =
|
|
||||||
# assume initial '(' is already consumed
|
|
||||||
comp.expression()
|
|
||||||
comp.consume(tkRightParen, "Expect ')' after expression.")
|
|
||||||
|
|
||||||
|
|
||||||
proc parseCall(comp: Compiler) =
|
|
||||||
# ( consumed
|
|
||||||
|
|
||||||
# create the call env
|
|
||||||
# current stack before opCall:
|
|
||||||
# ... <funct obj> <arg1> <arg2> <arg3>
|
|
||||||
# opCall converts it to this
|
|
||||||
# ... <ret val> <arg1> <arg2> <arg3>
|
|
||||||
|
|
||||||
var argcount = 0
|
|
||||||
# put args on stack
|
|
||||||
while comp.current.tokenType notin {tkRightParen, tkEof}:
|
|
||||||
comp.expression()
|
|
||||||
inc argcount
|
|
||||||
if comp.current.tokenType != tkRightParen:
|
|
||||||
comp.consume(tkComma, "Expected ',' between arguments in function calls.")
|
|
||||||
comp.consume(tkRightParen, "Expected ')' after arguments in function calls.")
|
|
||||||
|
|
||||||
# emit call
|
|
||||||
comp.writeChunk(-argcount, opCall)
|
|
||||||
comp.writeChunk(0, argcount.toDU8)
|
|
||||||
|
|
||||||
|
|
||||||
tkLeftParen.genRule(grouping, parseCall, pcCall)
|
|
||||||
|
|
||||||
proc unary(comp: Compiler) =
|
|
||||||
let opType = comp.previous.tokenType
|
|
||||||
|
|
||||||
comp.parsePrecedence(pcUnary)
|
|
||||||
|
|
||||||
case opType:
|
|
||||||
of tkMinus:
|
|
||||||
comp.writeChunk(0, opNegate)
|
|
||||||
of tkBang:
|
|
||||||
comp.writeChunk(0, opNot)
|
|
||||||
else:
|
|
||||||
discard # unreachable
|
|
||||||
|
|
||||||
tkBang.genRule(unary, nop, pcNone)
|
|
||||||
|
|
||||||
proc binary(comp: Compiler) =
|
|
||||||
let opType = comp.previous.tokenType
|
|
||||||
# safety checked in parsePrecedence
|
|
||||||
let rule = opType.getRule.get
|
|
||||||
comp.parsePrecedence(rule.applyRule.increment)
|
|
||||||
|
|
||||||
case opType:
|
|
||||||
of tkPlus:
|
|
||||||
comp.writeChunk(-1, opAdd)
|
|
||||||
of tkMinus:
|
|
||||||
comp.writeChunk(-1, opSubtract)
|
|
||||||
of tkStar:
|
|
||||||
comp.writeChunk(-1, opMultiply)
|
|
||||||
of tkSlash:
|
|
||||||
comp.writeChunk(-1, opDivide)
|
|
||||||
of tkEqualEqual:
|
|
||||||
comp.writeChunk(-1, opEqual)
|
|
||||||
of tkBangEqual:
|
|
||||||
comp.writeChunk(-1, opEqual)
|
|
||||||
comp.writeChunk(0, opNot)
|
|
||||||
of tkGreater:
|
|
||||||
comp.writeChunk(-1, opGreater)
|
|
||||||
of tkLess:
|
|
||||||
comp.writeChunk(-1, opLess)
|
|
||||||
of tkGreaterEqual:
|
|
||||||
comp.writeChunk(-1, opLess)
|
|
||||||
comp.writeChunk(0, opNot)
|
|
||||||
of tkLessEqual:
|
|
||||||
comp.writeChunk(-1, opGreater)
|
|
||||||
comp.writeChunk(0, opNot)
|
|
||||||
else:
|
|
||||||
return # unreachable
|
|
||||||
|
|
||||||
tkMinus.genRule(unary, binary, pcTerm)
|
|
||||||
tkPlus.genRule(nop, binary, pcTerm)
|
|
||||||
tkSlash.genRule(nop, binary, pcFactor)
|
|
||||||
tkStar.genRule(nop, binary, pcFactor)
|
|
||||||
|
|
||||||
tkEqualEqual.genRule(nop, binary, pcEquality)
|
|
||||||
tkBangEqual.genRule(nop, binary, pcEquality)
|
|
||||||
tkGreater.genRule(nop, binary, pcComparison)
|
|
||||||
tkGreaterEqual.genRule(nop, binary, pcComparison)
|
|
||||||
tkLess.genRule(nop, binary, pcComparison)
|
|
||||||
tkLessEqual.genRule(nop, binary, pcComparison)
|
|
||||||
|
|
||||||
proc ifExpr(comp: Compiler) =
|
|
||||||
# if expressions return the body if condition is truthy,
|
|
||||||
# the else expression otherwise, unless there is no else:
|
|
||||||
# if there is no else, it returns the condition if it is falsey
|
|
||||||
comp.consume(tkLeftParen, "Expect '(' after 'if'.")
|
|
||||||
comp.expression()
|
|
||||||
comp.consume(tkRightParen, "Expect ')' after condition.")
|
|
||||||
|
|
||||||
let thenJump = comp.emitJump(0, opJumpIfFalse)
|
|
||||||
|
|
||||||
# conditional code that can be jumped over must leave the stack in tact!
|
|
||||||
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.expression()
|
|
||||||
# net change to stack: -1 + 1 = 0
|
|
||||||
|
|
||||||
let elseJump = comp.emitJump(0, opJump)
|
|
||||||
comp.patchJump(thenJump)
|
|
||||||
|
|
||||||
if comp.match(tkElse):
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.expression()
|
|
||||||
|
|
||||||
comp.patchJump(elseJump)
|
|
||||||
|
|
||||||
|
|
||||||
tkIf.genRule(ifExpr, nop, pcNone)
|
|
||||||
|
|
||||||
proc andExpr(comp: Compiler) =
|
|
||||||
let endJump = comp.emitJump(0, opJumpIfFalse)
|
|
||||||
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.parsePrecedence(pcAnd)
|
|
||||||
# net effect on stack: -1 + 1 = 0
|
|
||||||
|
|
||||||
comp.patchJump(endJump)
|
|
||||||
|
|
||||||
tkAnd.genRule(nop, andExpr, pcAnd)
|
|
||||||
|
|
||||||
proc orExpr(comp: Compiler) =
|
|
||||||
let elseJump = comp.emitJump(0, opJumpIfFalse)
|
|
||||||
let endJump = comp.emitJump(0, opJump)
|
|
||||||
|
|
||||||
comp.patchJump(elseJump)
|
|
||||||
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.parsePrecedence(pcOr)
|
|
||||||
# net effect on stack: -1 + 1 = 0
|
|
||||||
|
|
||||||
comp.patchJump(endJump)
|
|
||||||
|
|
||||||
tkOr.genRule(nop, orExpr, pcOr)
|
|
||||||
|
|
||||||
proc debugExpr(comp: Compiler) =
|
|
||||||
comp.expression()
|
|
||||||
when debugCompiler:
|
|
||||||
debugEcho &"debug expression, current stackindex: {comp.stackIndex}"
|
|
||||||
comp.writeChunk(0, opPrint)
|
|
||||||
|
|
||||||
tkPrint.genRule(debugExpr, nop, pcNone)
|
|
||||||
|
|
||||||
proc parseWhile(comp: Compiler) =
|
|
||||||
|
|
||||||
comp.writeChunk(1, opNil) # return value
|
|
||||||
let loopStart = comp.chunk.len
|
|
||||||
comp.consume(tkLeftParen, "Expect '(' after 'while'.")
|
|
||||||
# condition
|
|
||||||
comp.expression()
|
|
||||||
comp.consume(tkRightParen, "Expect ')' after condition.")
|
|
||||||
|
|
||||||
let exitJump = comp.emitJump(-1, opJumpIfFalsePop)
|
|
||||||
# this cannot be handled with just opPop, since the net change in the
|
|
||||||
# stack size inside code that is conditional must be 0!
|
|
||||||
|
|
||||||
# body
|
|
||||||
comp.writeChunk(-1, opPop) # pop the old return value
|
|
||||||
comp.expression()
|
|
||||||
# net stack change: 1 + -1 = 0
|
|
||||||
|
|
||||||
comp.emitLoop(loopstart = loopStart, delta = 0, op = opLoop)
|
|
||||||
comp.patchJump(exitJump)
|
|
||||||
|
|
||||||
|
|
||||||
tkWhile.genRule(parseWhile, nop, pcNone)
|
|
||||||
|
|
||||||
proc parseFunct(comp: Compiler) =
|
|
||||||
|
|
||||||
# jump over
|
|
||||||
let jumpOverBody = comp.emitJump(0, opJump)
|
|
||||||
|
|
||||||
comp.consume(tkLeftParen, "Expected '(' after keyword 'funct'.")
|
|
||||||
|
|
||||||
var params: seq[string]
|
|
||||||
# parameters
|
|
||||||
while comp.current.tokenType == tkIdentifier:
|
|
||||||
comp.advance()
|
|
||||||
params.add(comp.previous.text)
|
|
||||||
if comp.current.tokenType == tkRightParen:
|
|
||||||
break
|
|
||||||
comp.consume(tkComma, "Expected ',' to separate items in the parameter list.")
|
|
||||||
|
|
||||||
comp.consume(tkRightParen, "Expected ')' after parameter list.")
|
|
||||||
|
|
||||||
# function body:
|
|
||||||
let functII = comp.chunk.len
|
|
||||||
comp.beginScope(function = true) # this saves the old stackindex, sets it to 0, points ^ and ^function at index 0
|
|
||||||
# assumption:
|
|
||||||
# the caller will create the following stack for the function to run in:
|
|
||||||
# [0] = return value placeholder
|
|
||||||
# [1] = arg #1
|
|
||||||
# [2] = arg #2
|
|
||||||
# [3] = arg #3
|
|
||||||
|
|
||||||
for i in countup(1, params.len):
|
|
||||||
comp.stackIndex = i
|
|
||||||
comp.addLocal(params[i-1], 0)
|
|
||||||
comp.expression()
|
|
||||||
when assertionsCompiler:
|
|
||||||
let shouldbeStackIndex = params.len + 1
|
|
||||||
if shouldbeStackIndex != comp.stackIndex:
|
|
||||||
comp.error(&"Assertion failed: wrong stackindex ({comp.stackIndex}) in function declaration (should be {shouldbeStackIndex}).")
|
|
||||||
comp.endScope()
|
|
||||||
dec comp.stackIndex # the previous end scope did not put anything on the stack, it is jumped over
|
|
||||||
|
|
||||||
# get konvalue functions:
|
|
||||||
let konFunct = newKonFunction(functII, params.len)
|
|
||||||
|
|
||||||
# end of function declaration:
|
|
||||||
comp.patchJump(jumpOverBody)
|
|
||||||
# put the fn object on the stack
|
|
||||||
comp.writeConstant(konFunct)
|
|
||||||
|
|
||||||
tkFunct.genRule(parseFunct, nop, pcNone)
|
|
||||||
|
|
||||||
|
|
||||||
# below are the expressions that can contain statements in some way
|
|
||||||
# the only expressions that can contain a statement are:
|
|
||||||
# the block expression
|
|
||||||
proc statement(comp: Compiler)
|
|
||||||
|
|
||||||
proc parseBlock(comp: Compiler) =
|
|
||||||
## Despite the name, can be used for statements if the arg statement is true
|
|
||||||
## Also can be used for function bodies
|
|
||||||
comp.beginScope()
|
|
||||||
while comp.current.tokenType != tkRightBrace and comp.current.tokenType != tkEof:
|
|
||||||
comp.statement()
|
|
||||||
comp.endScope()
|
|
||||||
|
|
||||||
comp.consume(tkRightBrace, "Expect '}' after block.")
|
|
||||||
|
|
||||||
tkLeftBrace.genRule(parseBlock, nop, pcNone)
|
|
||||||
|
|
||||||
# statements
|
|
||||||
|
|
||||||
proc parseVariable(comp: Compiler, msg: string): int =
|
|
||||||
## Parses variable declarations
|
|
||||||
## During manipulation with variables:
|
|
||||||
## if global:
|
|
||||||
## consume the identifier and return index to work with
|
|
||||||
## if local:
|
|
||||||
## register the name with the vm
|
|
||||||
comp.consume(tkIdentifier, msg)
|
|
||||||
let name = comp.previous.text
|
|
||||||
|
|
||||||
if name[0] in {'^'}:
|
|
||||||
comp.error("Illegal variable name.")
|
|
||||||
|
|
||||||
if comp.scopes.len > 0: # declareVariable
|
|
||||||
# local
|
|
||||||
|
|
||||||
# check if name exists already within scope
|
|
||||||
for i in countdown(comp.locals.high, 0):
|
|
||||||
let local = comp.locals[i]
|
|
||||||
if local.depth != -1 and local.depth < comp.scopes.len:
|
|
||||||
break
|
|
||||||
|
|
||||||
if name == local.name:
|
|
||||||
comp.error("Already a variable with this name in this scope.")
|
|
||||||
break
|
|
||||||
|
|
||||||
comp.addLocal(name, 1)
|
|
||||||
0 # index to the constant is irrelevant if the var is local
|
|
||||||
else:
|
|
||||||
# global
|
|
||||||
comp.chunk.addConstant(name.toKonValue())
|
|
||||||
|
|
||||||
proc defineVariable(comp: Compiler, index: int) =
|
|
||||||
## Generate code that moves the variable on the stack
|
|
||||||
## to a variable at the right place in memory
|
|
||||||
## the right place is defined by the 3 following byte after the op
|
|
||||||
## the thing to move is the item below it
|
|
||||||
|
|
||||||
if comp.scopes.len > 0:
|
|
||||||
# local variable: it's already on the right place
|
|
||||||
# but we need to mark initialized
|
|
||||||
comp.markInitialized()
|
|
||||||
else:
|
|
||||||
comp.writeChunk(-1, opDefineGlobal)
|
|
||||||
comp.writeChunk(0, index.toDU8)
|
|
||||||
|
|
||||||
proc varStatement(comp: Compiler) =
|
|
||||||
let globalIndex = comp.parseVariable("Expect variable name.")
|
|
||||||
|
|
||||||
if comp.match(tkEqual):
|
|
||||||
comp.expression()
|
|
||||||
else:
|
|
||||||
comp.writeChunk(1, opNil)
|
|
||||||
|
|
||||||
comp.defineVariable(globalIndex)
|
|
||||||
|
|
||||||
|
|
||||||
proc breakStatement(comp: Compiler) =
|
|
||||||
if not comp.match(tkLabel):
|
|
||||||
comp.error("Label expected after break.")
|
|
||||||
|
|
||||||
let label = comp.previous.text[1..^1]
|
|
||||||
for i in countdown(comp.scopes.high, 0):
|
|
||||||
let scope = comp.scopes[i]
|
|
||||||
if scope.labels.contains(label):
|
|
||||||
comp.jumpToEnd(scope)
|
|
||||||
break
|
|
||||||
|
|
||||||
comp.consume(tkSemicolon, "Semicolon expected after break statement.")
|
|
||||||
if comp.current.tokenType != tkRightBrace:
|
|
||||||
comp.error("Break statement must be the last element inside the innermost block it is in.")
|
|
||||||
|
|
||||||
proc statement(comp: Compiler) =
|
|
||||||
if comp.match(tkVar):
|
|
||||||
comp.varStatement()
|
|
||||||
comp.consume(tkSemicolon, "Semicolon expected after expression statement.")
|
|
||||||
elif comp.match(tkBreak):
|
|
||||||
comp.breakStatement()
|
|
||||||
else:
|
|
||||||
comp.expression()
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.consume(tkSemicolon, "Semicolon expected after expression statement.")
|
|
||||||
|
|
||||||
if comp.panicMode:
|
|
||||||
comp.synchronize()
|
|
||||||
|
|
||||||
proc compile*(comp: Compiler) =
|
|
||||||
comp.scanner = newScanner(comp.source)
|
|
||||||
comp.writeChunk(0, opNil)
|
|
||||||
# the starting stackIndex is 0, which points to this nil
|
|
||||||
# it is correctly set to delta = 0!!!
|
|
||||||
comp.advance()
|
|
||||||
while comp.current.tokenType != tkEof:
|
|
||||||
comp.statement()
|
|
||||||
comp.writeChunk(-1, opPop)
|
|
||||||
comp.writeChunk(0, opReturn)
|
|
||||||
when debugDumpChunk:
|
|
||||||
if not comp.hadError:
|
|
||||||
comp.chunk.disassembleChunk()
|
|
32
config.nim
32
config.nim
|
@ -1,32 +0,0 @@
|
||||||
type
|
|
||||||
LineEditorChoice = enum
|
|
||||||
leBasic, leRdstdin
|
|
||||||
ReadlineInterruptedException* = object of CatchableError
|
|
||||||
|
|
||||||
# choose debug options here
|
|
||||||
const debugVM* = false
|
|
||||||
const debugScanner* = false
|
|
||||||
const debugCompiler* = false
|
|
||||||
const debugDumpChunk* = false
|
|
||||||
const assertionsVM* = false # sanity checks in the VM, such as the stack being empty at the end
|
|
||||||
const assertionsCompiler* = false # sanity checks in the compiler
|
|
||||||
const profileInstructions* = false # if true, the time spent on every opcode is measured
|
|
||||||
|
|
||||||
# choose a line editor for the repl
|
|
||||||
const lineEditor = leRdstdin
|
|
||||||
|
|
||||||
when lineEditor == leRdstdin:
|
|
||||||
import rdstdin
|
|
||||||
|
|
||||||
proc konLineEditor*: string =
|
|
||||||
proc ctrlc =
|
|
||||||
raise newException(ReadlineInterruptedException, "Ctrl+C/D pressed.")
|
|
||||||
when lineEditor == leBasic:
|
|
||||||
write stdout, "\r-> "
|
|
||||||
result = stdin.readLine()
|
|
||||||
when lineEditor == leRdstdin:
|
|
||||||
var line: string
|
|
||||||
let ok = readLineFromStdin("-> ", line)
|
|
||||||
if not ok:
|
|
||||||
ctrlc()
|
|
||||||
return line
|
|
6
perf.sh
6
perf.sh
|
@ -1,6 +0,0 @@
|
||||||
# change this to the path to nds and to the path to the program profiled
|
|
||||||
sudo perf record -g -F 999 /home/user/Projects/nondescript/bin/nds benchmarks/fib.nds
|
|
||||||
|
|
||||||
sudo perf script -F +pid > test.perf
|
|
||||||
|
|
||||||
# https://profiler.firefox.com/
|
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
template padd*[T](x: ptr T, num: int): ptr T =
|
|
||||||
cast[ptr T](cast[int](x) + num)
|
|
||||||
|
|
||||||
template psub*[T](x: ptr T, num: int): ptr T =
|
|
||||||
cast[ptr T](cast[int](x) - num)
|
|
||||||
|
|
||||||
template pdiff*[T](x: ptr T, y: ptr): int =
|
|
||||||
cast[int](x) - cast[int](y)
|
|
200
scanner.nim
200
scanner.nim
|
@ -1,200 +0,0 @@
|
||||||
import strutils
|
|
||||||
import tables
|
|
||||||
import strformat
|
|
||||||
|
|
||||||
type
|
|
||||||
Scanner* = ref object
|
|
||||||
start: int
|
|
||||||
current: int
|
|
||||||
line: int
|
|
||||||
source: string
|
|
||||||
|
|
||||||
TokenType* = enum
|
|
||||||
tkNone, # the default tokentype, if encountered anywhere, erroring out is the best course of action
|
|
||||||
tkLeftParen, tkRightParen, tkLeftBrace, tkRightBrace, tkComma, tkDot,
|
|
||||||
tkMinus, tkPlus, tkSemicolon, tkSlash, tkStar, tkBang, tkBangEqual,
|
|
||||||
tkGreater, tkGreaterEqual, tkLess, tkLessEqual, tkEqual, tkEqualEqual,
|
|
||||||
tkIdentifier, tkString,
|
|
||||||
tkNumber, tkAnd, tkElse, tkFalse, tkFor, tkFunct, tkGoto, tkIf, tkNil,
|
|
||||||
tkOr, tkPrint, tkLabel, tkBreak, tkTrue, tkVar, tkWhile,
|
|
||||||
tkError, tkEof
|
|
||||||
|
|
||||||
Token* = object
|
|
||||||
tokenType*: TokenType
|
|
||||||
text*: string
|
|
||||||
line*: int
|
|
||||||
|
|
||||||
proc debugPrint*(token: Token) =
|
|
||||||
write stdout, &"Token of type {$token.tokenType} [{token.text}] at line {$token.line}\n"
|
|
||||||
|
|
||||||
proc isAtEnd(scanner: Scanner): bool =
|
|
||||||
scanner.current > scanner.source.high
|
|
||||||
|
|
||||||
proc advance(scanner: Scanner): char =
|
|
||||||
scanner.current.inc
|
|
||||||
scanner.source[scanner.current - 1]
|
|
||||||
|
|
||||||
proc peek(scanner: Scanner): char =
|
|
||||||
if scanner.isAtEnd():
|
|
||||||
'\0'
|
|
||||||
else:
|
|
||||||
scanner.source[scanner.current]
|
|
||||||
|
|
||||||
proc peekNext(scanner: Scanner): char =
|
|
||||||
if scanner.current < scanner.source.high:
|
|
||||||
scanner.source[scanner.current + 1]
|
|
||||||
else:
|
|
||||||
'\0'
|
|
||||||
|
|
||||||
proc match(scanner: Scanner, exp: char): bool =
|
|
||||||
if scanner.peek() == exp:
|
|
||||||
discard scanner.advance()
|
|
||||||
true
|
|
||||||
else:
|
|
||||||
false
|
|
||||||
|
|
||||||
proc newScanner*(source: string): Scanner =
|
|
||||||
Scanner(source: source, line: 0, current: 0)
|
|
||||||
|
|
||||||
proc makeToken(scanner: Scanner, tokenType: TokenType): Token =
|
|
||||||
result.tokenType = tokenType
|
|
||||||
result.text = scanner.source[scanner.start..scanner.current-1]
|
|
||||||
result.line = scanner.line
|
|
||||||
|
|
||||||
proc errorToken(scanner: Scanner, msg: string): Token =
|
|
||||||
result.tokenType = tkError
|
|
||||||
result.text = msg
|
|
||||||
result.line = scanner.line
|
|
||||||
|
|
||||||
proc skipWhitespace(scanner: Scanner) =
|
|
||||||
while true:
|
|
||||||
let c = scanner.peek()
|
|
||||||
case c:
|
|
||||||
of {' ', '\r', '\t'}:
|
|
||||||
discard scanner.advance()
|
|
||||||
of '\n':
|
|
||||||
scanner.line.inc
|
|
||||||
discard scanner.advance()
|
|
||||||
of '/':
|
|
||||||
if scanner.peekNext() == '/':
|
|
||||||
while scanner.peek != '\n' and not scanner.isAtEnd():
|
|
||||||
discard scanner.advance()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
proc scanString(scanner: Scanner): Token =
|
|
||||||
while scanner.peek() != '\"' and not scanner.isAtEnd():
|
|
||||||
if scanner.peek() == '\n':
|
|
||||||
scanner.line.inc
|
|
||||||
discard scanner.advance()
|
|
||||||
|
|
||||||
if scanner.isAtEnd():
|
|
||||||
return scanner.errorToken("Unterminated string.")
|
|
||||||
|
|
||||||
discard scanner.advance()
|
|
||||||
scanner.makeToken(tkString)
|
|
||||||
|
|
||||||
proc scanNumber(scanner: Scanner): Token =
|
|
||||||
while scanner.peek() in Digits:
|
|
||||||
discard scanner.advance()
|
|
||||||
|
|
||||||
if scanner.peek() == '.' and scanner.peekNext() in Digits:
|
|
||||||
discard scanner.advance()
|
|
||||||
while scanner.peek() in Digits:
|
|
||||||
discard scanner.advance()
|
|
||||||
|
|
||||||
return scanner.makeToken(tkNumber)
|
|
||||||
|
|
||||||
const keywords = {
|
|
||||||
"and": tkAnd,
|
|
||||||
"break": tkBreak,
|
|
||||||
"else": tkElse,
|
|
||||||
"false": tkFalse,
|
|
||||||
"for": tkFor,
|
|
||||||
"funct": tkFunct,
|
|
||||||
# here's a language that uses funct... still waiting for the day when a good de-funct joke comes to my mind that I can abuse
|
|
||||||
"goto": tkGoto,
|
|
||||||
"if": tkIf,
|
|
||||||
"nil": tkNil,
|
|
||||||
"or": tkOr,
|
|
||||||
"print": tkPrint,
|
|
||||||
"true": tkTrue,
|
|
||||||
"var": tkVar,
|
|
||||||
"while": tkWhile
|
|
||||||
}.toTable
|
|
||||||
|
|
||||||
proc canStartIdent(chr: char): bool =
|
|
||||||
chr in Letters or chr in {'_', '^'}
|
|
||||||
|
|
||||||
proc canContIdent(chr: char): bool =
|
|
||||||
canStartIdent(chr) or chr in Digits
|
|
||||||
|
|
||||||
proc scanIdentifier(scanner: Scanner): Token =
|
|
||||||
while scanner.peek.canContIdent:
|
|
||||||
discard scanner.advance()
|
|
||||||
|
|
||||||
let text = scanner.source[scanner.start..scanner.current-1]
|
|
||||||
|
|
||||||
if keywords.hasKey(text):
|
|
||||||
return scanner.makeToken(keywords[text])
|
|
||||||
|
|
||||||
return scanner.makeToken(tkIdentifier)
|
|
||||||
|
|
||||||
proc canContLabel(chr: char): bool =
|
|
||||||
chr in Letters or chr == '_'
|
|
||||||
|
|
||||||
proc scanLabel(scanner: Scanner): Token =
|
|
||||||
if not scanner.peek.canContLabel:
|
|
||||||
return scanner.errorToken("Labels must only contain letters and underscores.")
|
|
||||||
|
|
||||||
while scanner.peek.canContLabel:
|
|
||||||
discard scanner.advance()
|
|
||||||
|
|
||||||
return scanner.makeToken(tkLabel)
|
|
||||||
|
|
||||||
proc scanToken*(scanner: Scanner): Token =
|
|
||||||
|
|
||||||
scanner.skipWhitespace()
|
|
||||||
scanner.start = scanner.current
|
|
||||||
|
|
||||||
if scanner.isAtEnd():
|
|
||||||
return scanner.makeToken(tkEof)
|
|
||||||
|
|
||||||
let c = scanner.advance()
|
|
||||||
|
|
||||||
case c:
|
|
||||||
of '(': return scanner.makeToken(tkLeftParen)
|
|
||||||
of ')': return scanner.makeToken(tkRightParen)
|
|
||||||
of '{': return scanner.makeToken(tkLeftBrace)
|
|
||||||
of '}': return scanner.makeToken(tkRightBrace)
|
|
||||||
of ';': return scanner.makeToken(tkSemicolon)
|
|
||||||
of ',': return scanner.makeToken(tkComma)
|
|
||||||
of '.': return scanner.makeToken(tkDot)
|
|
||||||
of '-': return scanner.makeToken(tkMinus)
|
|
||||||
of '+': return scanner.makeToken(tkPlus)
|
|
||||||
of '/': return scanner.makeToken(tkSlash)
|
|
||||||
of '*': return scanner.makeToken(tkStar)
|
|
||||||
of '!':
|
|
||||||
return if scanner.match('='): scanner.makeToken(tkBangEqual) else: scanner.makeToken(tkBang)
|
|
||||||
|
|
||||||
of '=':
|
|
||||||
return if scanner.match('='): scanner.makeToken(tkEqualEqual) else: scanner.makeToken(tkEqual)
|
|
||||||
|
|
||||||
of '<':
|
|
||||||
return if scanner.match('='): scanner.makeToken(tkLessEqual) else: scanner.makeToken(tkLess)
|
|
||||||
|
|
||||||
of '>':
|
|
||||||
return if scanner.match('='): scanner.makeToken(tkGreaterEqual) else: scanner.makeToken(tkGreater)
|
|
||||||
of '\"':
|
|
||||||
return scanner.scanString()
|
|
||||||
of Digits:
|
|
||||||
return scanner.scanNumber()
|
|
||||||
of '@':
|
|
||||||
return scanner.scanLabel()
|
|
||||||
else:
|
|
||||||
if c.canStartIdent():
|
|
||||||
return scanner.scanIdentifier()
|
|
||||||
else:
|
|
||||||
return scanner.errorToken("Unexpected character.")
|
|
142
value.nim
142
value.nim
|
@ -1,142 +0,0 @@
|
||||||
import strformat
|
|
||||||
|
|
||||||
type
|
|
||||||
KonType* = enum
|
|
||||||
ktNil, ktBool, ktFloat, ktString,
|
|
||||||
ktFunct,
|
|
||||||
ktTypeError,
|
|
||||||
|
|
||||||
const errorTypes = {ktTypeError}
|
|
||||||
|
|
||||||
type
|
|
||||||
KonValue* = object
|
|
||||||
case konType*: KonType:
|
|
||||||
of ktNil:
|
|
||||||
discard
|
|
||||||
of ktBool:
|
|
||||||
boolValue*: bool
|
|
||||||
of ktFloat:
|
|
||||||
floatValue*: float64
|
|
||||||
of ktString:
|
|
||||||
stringValue*: string
|
|
||||||
of ktFunct:
|
|
||||||
module*: string
|
|
||||||
entryII*: int # entry instruction index
|
|
||||||
arity*: int # number of arguments
|
|
||||||
of errorTypes:
|
|
||||||
message*: string
|
|
||||||
|
|
||||||
# KON VALUE HELPERS, MUST BE DEFINED FOR EVERY KONVALUE
|
|
||||||
|
|
||||||
proc `$`*(val: KonValue): string =
|
|
||||||
case val.konType:
|
|
||||||
of ktFloat:
|
|
||||||
return $val.floatValue
|
|
||||||
of ktBool:
|
|
||||||
return $val.boolValue
|
|
||||||
of ktNil:
|
|
||||||
return "nil"
|
|
||||||
of ktString:
|
|
||||||
return val.stringValue
|
|
||||||
of ktFunct:
|
|
||||||
return &"Function object: {val.entryII}"
|
|
||||||
of errorTypes:
|
|
||||||
let ename = $val.konType
|
|
||||||
return &"{ename[2..^1]}: {val.message}"
|
|
||||||
|
|
||||||
proc isError*(val: KonValue): bool =
|
|
||||||
val.konType in errorTypes
|
|
||||||
|
|
||||||
proc isFalsey*(val: KonValue): bool =
|
|
||||||
val.konType in {ktNil} or (val.konType == ktBool and not val.boolValue)
|
|
||||||
|
|
||||||
template isTruthy*(val: KonValue): bool =
|
|
||||||
not isFalsey(val)
|
|
||||||
|
|
||||||
proc equal*(val, right: KonValue): bool =
|
|
||||||
if val.konType != right.konType:
|
|
||||||
false
|
|
||||||
else:
|
|
||||||
case val.konType:
|
|
||||||
of ktFloat:
|
|
||||||
val.floatValue == right.floatValue
|
|
||||||
of ktBool:
|
|
||||||
val.boolValue == right.boolValue
|
|
||||||
of ktNil:
|
|
||||||
true
|
|
||||||
of ktString:
|
|
||||||
val.stringValue == right.stringValue
|
|
||||||
of ktFunct:
|
|
||||||
val.entryII == right.entryII
|
|
||||||
# same entry II/module but diff arity is a bug
|
|
||||||
of errorTypes:
|
|
||||||
false # error comparison is undefined
|
|
||||||
|
|
||||||
# NIM VALUE TO KON VALUE WRAPPERS
|
|
||||||
|
|
||||||
proc toKonValue*(val: float): KonValue =
|
|
||||||
KonValue(konType: ktFloat, floatValue: val)
|
|
||||||
|
|
||||||
proc toKonValue*(val: bool): KonValue =
|
|
||||||
KonValue(konType: ktBool, boolValue: val)
|
|
||||||
|
|
||||||
proc toKonValue*(val: string): KonValue =
|
|
||||||
KonValue(konType: ktString, stringValue: val)
|
|
||||||
|
|
||||||
proc newKonFunction*(ii: int, arity: int): KonValue =
|
|
||||||
KonValue(konType: ktFunct, entryII: ii, arity: arity)
|
|
||||||
|
|
||||||
proc toKonValue*: KonValue =
|
|
||||||
KonValue(konType: ktNil)
|
|
||||||
|
|
||||||
proc konTypeError*(msg: string): KonValue =
|
|
||||||
KonValue(konType: ktTypeError, message: msg)
|
|
||||||
|
|
||||||
# OPERATIONS
|
|
||||||
# NOTE: these operations can return ktTypeError with a message if types are invalid
|
|
||||||
|
|
||||||
proc negate*(val: KonValue): KonValue =
|
|
||||||
if (val.konType != ktFloat):
|
|
||||||
return konTypeError("Operand must be a number.")
|
|
||||||
else:
|
|
||||||
return toKonValue(-val.floatValue)
|
|
||||||
|
|
||||||
proc add*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue + right.floatValue)
|
|
||||||
elif val.konType == ktString and right.konType == ktString:
|
|
||||||
return toKonValue(val.stringValue & right.stringValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to add types {val.konType} and {right.konType}.")
|
|
||||||
|
|
||||||
proc subtract*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue - right.floatValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to subtract types {val.konType} and {right.konType}.")
|
|
||||||
|
|
||||||
proc multiply*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue * right.floatValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to multiply types {val.konType} and {right.konType}.")
|
|
||||||
|
|
||||||
|
|
||||||
proc divide*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue / right.floatValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to divide types {val.konType} and {right.konType}.")
|
|
||||||
|
|
||||||
proc `<`*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue < right.floatValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to compare types {val.konType} and {right.konType}.")
|
|
||||||
|
|
||||||
|
|
||||||
proc `>`*(val: KonValue, right: KonValue): KonValue =
|
|
||||||
if val.konType == ktFloat and right.konType == ktFloat:
|
|
||||||
return toKonValue(val.floatValue > right.floatValue)
|
|
||||||
else:
|
|
||||||
return konTypeError(&"Attempt to compare types {val.konType} and {right.konType}.")
|
|
236
vm.nim
236
vm.nim
|
@ -1,236 +0,0 @@
|
||||||
import strformat
|
|
||||||
import tables
|
|
||||||
|
|
||||||
import value
|
|
||||||
import chunk
|
|
||||||
import config
|
|
||||||
import pointerutils
|
|
||||||
when profileInstructions:
|
|
||||||
import times
|
|
||||||
import std/monotimes
|
|
||||||
|
|
||||||
type
|
|
||||||
Frame = object
|
|
||||||
stackBottom: int # the absolute index of where 0 inside the frame is
|
|
||||||
returnIp: ptr uint8
|
|
||||||
|
|
||||||
VM* = ref object
|
|
||||||
chunk: Chunk
|
|
||||||
ip: ptr uint8
|
|
||||||
stack: seq[KonValue]
|
|
||||||
hadError: bool
|
|
||||||
globals: Table[string, KonValue]
|
|
||||||
frames: seq[Frame]
|
|
||||||
|
|
||||||
InterpretResult* = enum
|
|
||||||
irOK, irRuntimeError
|
|
||||||
|
|
||||||
|
|
||||||
proc newVM*(ch: Chunk): VM =
|
|
||||||
result = VM(chunk: ch, stack: newSeqOfCap[KonValue](256), frames: newSeqOfCap[Frame](4))
|
|
||||||
result.ip = ch.code[0].unsafeAddr
|
|
||||||
|
|
||||||
proc push(vm: VM, val: KonValue) =
|
|
||||||
vm.stack.add(val)
|
|
||||||
|
|
||||||
proc pop(vm: VM): KonValue =
|
|
||||||
vm.stack.pop()
|
|
||||||
|
|
||||||
proc peek(vm: VM): KonValue =
|
|
||||||
vm.stack[vm.stack.high]
|
|
||||||
|
|
||||||
proc runtimeError(vm: VM, msg: string) =
|
|
||||||
let ii = vm.ip.pdiff(vm.chunk.code[0].unsafeAddr)
|
|
||||||
let line = vm.chunk.lines[ii]
|
|
||||||
write stderr, &"[line: {line}] {msg}\n"
|
|
||||||
vm.hadError = true
|
|
||||||
|
|
||||||
proc pushSafe(vm: VM, val: KonValue): bool =
|
|
||||||
## returns if the value is not a runtime error
|
|
||||||
## prints the error if it is a runtime error
|
|
||||||
## pushes it to the stack if it is not an error
|
|
||||||
if val.isError():
|
|
||||||
vm.runtimeError($val)
|
|
||||||
return false
|
|
||||||
else:
|
|
||||||
vm.push(val)
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc advance(vm: VM): uint8 =
|
|
||||||
result = vm.ip[]
|
|
||||||
vm.ip = vm.ip.padd(1)
|
|
||||||
|
|
||||||
proc readDU8(vm: VM): int =
|
|
||||||
result = vm.ip.DU8ptrToInt
|
|
||||||
vm.ip = vm.ip.padd(argSize)
|
|
||||||
|
|
||||||
proc readConstant(vm: VM): KonValue =
|
|
||||||
let index = vm.readDU8()
|
|
||||||
vm.chunk.constants[index]
|
|
||||||
|
|
||||||
proc binary(op: OpCode, left: KonValue, right: KonValue): KonValue =
|
|
||||||
case op:
|
|
||||||
of opAdd:
|
|
||||||
return left.add(right)
|
|
||||||
of opSubtract:
|
|
||||||
return left.subtract(right)
|
|
||||||
of opMultiply:
|
|
||||||
return left.multiply(right)
|
|
||||||
of opDivide:
|
|
||||||
return left.divide(right)
|
|
||||||
else:
|
|
||||||
discard #unreachable
|
|
||||||
|
|
||||||
when profileInstructions:
|
|
||||||
# not a perfect profiling approach, doing random stacktrace dumps
|
|
||||||
# x amount of times per second to get the current source line
|
|
||||||
# would be better
|
|
||||||
var durations: array[OpCode, Duration]
|
|
||||||
|
|
||||||
proc run*(vm: VM): InterpretResult =
|
|
||||||
|
|
||||||
template frameBottom: int = vm.frames[vm.frames.high].stackBottom
|
|
||||||
while true:
|
|
||||||
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
|
|
||||||
|
|
||||||
let ins = vm.advance.OpCode
|
|
||||||
|
|
||||||
when debugVM:
|
|
||||||
let opname = ($ins)
|
|
||||||
var msg = &"[{vm.ii:4}] {opname}"
|
|
||||||
msg &= " Stack: [ "
|
|
||||||
for i in 0 .. vm.stack.high:
|
|
||||||
let e = vm.stack[i]
|
|
||||||
if i == frameBottom:
|
|
||||||
msg &= &"<{e}> "
|
|
||||||
else:
|
|
||||||
msg &= &"{e} "
|
|
||||||
msg &= "]"
|
|
||||||
echo msg
|
|
||||||
|
|
||||||
when profileInstructions:
|
|
||||||
let startTime = getMonoTime()
|
|
||||||
|
|
||||||
case ins:
|
|
||||||
of opPop:
|
|
||||||
discard vm.pop()
|
|
||||||
of opConstant:
|
|
||||||
let val: KonValue = vm.readConstant()
|
|
||||||
vm.push(val)
|
|
||||||
of opNegate:
|
|
||||||
let val = vm.pop.negate
|
|
||||||
if not vm.pushSafe(val):
|
|
||||||
break
|
|
||||||
of opAdd, opSubtract, opMultiply, opDivide:
|
|
||||||
let right = vm.pop()
|
|
||||||
let left = vm.pop()
|
|
||||||
if not vm.pushSafe(binary(ins, left, right)):
|
|
||||||
break
|
|
||||||
of opReturn:
|
|
||||||
if vm.frames.len == 0:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
vm.ip = vm.frames.pop().returnIp # remove frame that's over
|
|
||||||
of opTrue:
|
|
||||||
vm.push(toKonValue(true))
|
|
||||||
of opFalse:
|
|
||||||
vm.push(toKonValue(false))
|
|
||||||
of opNil:
|
|
||||||
vm.push(toKonValue())
|
|
||||||
of opNot:
|
|
||||||
vm.push(toKonValue(vm.pop.isFalsey))
|
|
||||||
of opEqual:
|
|
||||||
vm.push(toKonValue(vm.pop.equal(vm.pop())))
|
|
||||||
of opLess:
|
|
||||||
if not vm.pushSafe(vm.pop() > vm.pop()):
|
|
||||||
break
|
|
||||||
of opGreater:
|
|
||||||
if not vm.pushSafe(vm.pop() < vm.pop()):
|
|
||||||
break
|
|
||||||
of opPrint:
|
|
||||||
echo $vm.peek()
|
|
||||||
of opDefineGlobal:
|
|
||||||
let name = vm.readConstant().stringValue
|
|
||||||
if not vm.globals.hasKey(name):
|
|
||||||
vm.globals[name] = vm.pop()
|
|
||||||
else:
|
|
||||||
vm.runtimeError("Attempt to redefine an existing global variable.")
|
|
||||||
break
|
|
||||||
of opGetGlobal:
|
|
||||||
let name = vm.readConstant().stringValue
|
|
||||||
if vm.globals.hasKey(name):
|
|
||||||
vm.push(vm.globals[name])
|
|
||||||
else:
|
|
||||||
vm.runtimeError(&"Undefined global variable {name}.")
|
|
||||||
break
|
|
||||||
of opSetGlobal:
|
|
||||||
let name = vm.readConstant().stringValue
|
|
||||||
if vm.globals.hasKey(name):
|
|
||||||
vm.globals[name] = vm.peek()
|
|
||||||
else:
|
|
||||||
vm.runtimeError(&"Attempt to set undefined global variable {name}.")
|
|
||||||
break
|
|
||||||
of opGetLocal:
|
|
||||||
let slot = vm.readDU8()
|
|
||||||
vm.push(vm.stack[slot + frameBottom])
|
|
||||||
of opSetLocal:
|
|
||||||
let slot = vm.readDU8()
|
|
||||||
vm.stack[slot + frameBottom] = vm.peek()
|
|
||||||
of opJumpIfFalse:
|
|
||||||
let offset = vm.readDU8()
|
|
||||||
if vm.peek.isFalsey:
|
|
||||||
vm.ip = vm.ip.padd(offset)
|
|
||||||
of opJumpIfFalsePop:
|
|
||||||
let offset = vm.readDU8()
|
|
||||||
if vm.pop.isFalsey:
|
|
||||||
vm.ip = vm.ip.padd(offset)
|
|
||||||
of opJump:
|
|
||||||
let offset = vm.readDU8()
|
|
||||||
vm.ip = vm.ip.padd(offset)
|
|
||||||
of opLoop:
|
|
||||||
let offset = vm.readDU8()
|
|
||||||
vm.ip = vm.ip.psub(offset)
|
|
||||||
of opCall:
|
|
||||||
# create the call env
|
|
||||||
# current stack before opCall:
|
|
||||||
# ... <funct obj> <arg1> <arg2> <arg3>
|
|
||||||
# opCall converts it to this
|
|
||||||
# ... <ret val> <arg1> <arg2> <arg3>
|
|
||||||
let argcount = vm.readDU8()
|
|
||||||
let funct = vm.stack[vm.stack.high - argcount]
|
|
||||||
if funct.konType != ktFunct:
|
|
||||||
vm.runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke
|
|
||||||
break
|
|
||||||
if funct.arity != argcount:
|
|
||||||
vm.runtimeError(&"Wrong number of arguments, expected {funct.arity}, got {argcount}.")
|
|
||||||
break
|
|
||||||
# if funct.module ... TODO
|
|
||||||
|
|
||||||
vm.stack[vm.stack.high - argcount] = toKonValue() # replace the function with nil: this is the return value slot
|
|
||||||
|
|
||||||
# create new frame
|
|
||||||
vm.frames.add(Frame(stackBottom: vm.stack.high - argcount, returnIp: vm.ip))
|
|
||||||
|
|
||||||
vm.ip = vm.chunk.code[0].unsafeAddr.padd(funct.entryII) # jump to the entry point
|
|
||||||
|
|
||||||
when profileInstructions:
|
|
||||||
durations[ins] += getMonoTime() - startTime
|
|
||||||
|
|
||||||
when assertionsVM:
|
|
||||||
if not vm.hadError and vm.stack.len > 0:
|
|
||||||
vm.runtimeError(&"VM Assertion failed: stack is of non-zero length {vm.stack.len} after execution has finished.")
|
|
||||||
|
|
||||||
when profileInstructions:
|
|
||||||
for op in OpCode:
|
|
||||||
let dur = durations[op].inMilliseconds
|
|
||||||
echo &"OpCode: {op} total duration {dur} ms"
|
|
||||||
|
|
||||||
if vm.hadError:
|
|
||||||
irRuntimeError
|
|
||||||
else:
|
|
||||||
irOK
|
|
||||||
|
|
||||||
proc interpretChunk*(ch: Chunk): InterpretResult =
|
|
||||||
let vm = newVM(ch)
|
|
||||||
return vm.run()
|
|
||||||
|
|
Loading…
Reference in New Issue