Added multiple inline exception support for except handlers and updated grammar accordingly. Added scopeDepth field to parser to keep track of local scopes and emit some errors about declarations. Added some more docs to bytecode instructions, fixed some issues with local scopes in the compiler and made dynamic declarations legal in functions. Added a flag for disabling compiler optimizations

This commit is contained in:
Nocturn9x 2021-11-20 13:17:16 +01:00
parent 9160cb32db
commit 2ab4bdc46d
6 changed files with 122 additions and 61 deletions

View File

@ -98,7 +98,7 @@ awaitStmt → "await" expression ";"; // Pauses the execution of the cal
// to the given name. The finally clause, if present, is executed regardless of whether the try block raises an exception, meaning it even overrides return,
// break and continue statements and it must be below all except clauses. The else clause, if present, is executed when the try block doesn't raise an exception.
// It must be the last statement of the block. A bare except clause without an exception name acts as a catch-all and must be placed below any other except clauses
tryStmt → "try" statement (("except" IDENTIFIER? ("as" IDENTIFIER)? statement)+ "finally" statement | "finally" statement)? ("else" statement)?;
tryStmt → "try" statement (("except" expression? statement)+ "finally" statement | "finally" statement)? ("else" statement)?;
blockStmt → "{" declaration* "}"; // Blocks create a new scope that lasts until they're closed
ifStmt → "if" "(" expression ")" statement ("else" statement)?; // If statements are conditional jumps
whileStmt → "while" "(" expression ")" statement; // While loops run until their condition is truthy

View File

@ -66,9 +66,10 @@ type
staticNames: seq[StaticName]
scopeDepth: int
currentFunction: FunDecl
enableOptimizations*: bool
proc initCompiler*(): Compiler =
proc initCompiler*(enableOptimizations: bool = true): Compiler =
## Initializes a new Compiler object
new(result)
result.ast = @[]
@ -78,6 +79,7 @@ proc initCompiler*(): Compiler =
result.staticNames = @[]
result.scopeDepth = 0
result.currentFunction = nil
result.enableOptimizations = enableOptimizations
@ -377,7 +379,9 @@ proc declareName(self: Compiler, node: ASTNode) =
isPrivate: node.isPrivate, owner: node.owner))
self.emitConstant(node.value)
else:
# Statically resolved variable here
# Statically resolved variable here. Only creates a new StaticName entry
# so that self.identifier (and, by extension, self.getStaticIndex) emit the
# proper stack offset
if self.staticNames.high() > 16777215:
# If someone ever hits this limit in real-world scenarios, I swear I'll
# slap myself 100 times with a sign saying "I'm dumb". Mark my words
@ -480,8 +484,8 @@ proc beginScope(self: Compiler) =
proc endScope(self: Compiler) =
## Ends the current local scope
if self.scopeDepth <= 0:
self.error("cannot call endScope with scopeDepth <= 0 (This is an internal error and most likely a bug)")
if self.scopeDepth < 0:
self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)")
for ident in reversed(self.staticNames):
if ident.depth > self.scopeDepth:
# All variables with a scope depth larger than the current one
@ -609,11 +613,8 @@ proc compile*(self: Compiler, ast: seq[ASTNode], file: string): Chunk =
self.current = 0
while not self.done():
self.declaration(self.step())
while self.staticNames.len() > 0:
# Gets rid of statically resolved locals
self.emitByte(Pop)
discard self.staticNames.pop()
if self.ast.len() > 0:
# *Technically* an empty program is a valid program
self.endScope()
self.emitByte(OpCode.Return) # Exits the VM's main loop when used at the global scope
result = self.chunk

View File

@ -80,17 +80,18 @@ type
BinarySlice, # Perform slicing on supported objects (like "hello"[0:2], which yields "he"). The result is pushed onto the stack
BinarySubscript, # Subscript operator, like "hello"[0] (which pushes 'h' onto the stack)
# Binary comparison operators
GreaterThan,
LessThan,
EqualTo,
GreaterOrEqual,
LessOrEqual,
GreaterThan, # Pushes the result of a > b onto the stack
LessThan, # Pushes the result of a < b onto the stack
EqualTo, # Pushes the result of a == b onto the stack
NotEqualTo, # Pushes the result of a != b onto the stack (optimization for not (a == b))
GreaterOrEqual, # Pushes the result of a >= b onto the stack
LessOrEqual, # Pushes the result of a <= b onto the stack
# Logical operators
LogicalNot,
LogicalAnd,
LogicalOr,
# Binary in-place operators. Same as their non in-place counterparts
# except they operate on already existing names.
# except they operate on already existing names
InPlaceAdd,
InPlaceSubtract,
InPlaceDivide,
@ -111,35 +112,52 @@ type
Nan,
Inf,
# Basic stack operations
Pop,
Pop,
Push,
PopN, # Pops N elements off the stack (optimization for exiting scopes and returning from functions)
# Name resolution/handling
LoadAttribute,
DeclareName,
LoadName,
LoadNameFast, # Compile-time optimization for statically resolved global variables
UpdateName,
UpdateNameFast,
DeleteName,
DeleteNameFast,
LoadAttribute,
DeclareName, # Declares a global dynamically bound name in the current scope
LoadName, # Loads a dynamically bound variable
LoadNameFast, # Loads a statically bound variable
UpdateName, # Updates a dynamically bound variable's value
UpdateNameFast, # Updates a statically bound variable's value
DeleteName, # Unbinds a dynamically bound variable's name from the current scope
DeleteNameFast, # Unbinds a statically bound variable's name from the current scope
# Looping and jumping
JumpIfFalse, # Jumps to an absolute index in the bytecode if the value at the top of the stack is falsey
Jump, # Relative unconditional jump in the bytecode. This is how instructions like break and continue are implemented
JumpIfFalse, # Jumps to an absolute index in the bytecode if the value at the top of the stack is falsey
JumpIfFalsePop, # Like JumpIfFalse, but it also pops off the stack (regardless of truthyness). Optimization for if statements
JumpForwards, # Relative, unconditional, positive jump in the bytecode
JumpBackwards, # Relative, unconditional, negative jump into the bytecode
## Long variants of jumps (they use a 24-bit operand instead of a 16-bit one)
LongJumpIfFalse,
LongJumpIfFalsePop,
LongJumpForwards,
LongJumpBackwards,
# Functions
Call,
Return
# Misc
# Exception handling
Raise,
ReRaise, # Re-raises active exception
BeginTry,
FinishTry,
# Generators
Yield,
# Coroutines
Await,
# Collection literals
BuildList,
BuildDict,
BuildSet,
BuildTuple
# We group instructions by their operation/operand types for easier handling when debugging
# Simple instructions encompass:
# - Instructions that push onto/pop off the stack unconditionally (True, False, PopN, Pop, etc.)
# - Unary and binary operators
const simpleInstructions* = {Return, BinaryAdd, BinaryMultiply,
BinaryDivide, BinarySubtract,
BinaryMod, BinaryPow, Nil,
@ -147,16 +165,26 @@ const simpleInstructions* = {Return, BinaryAdd, BinaryMultiply,
BinaryShiftLeft, BinaryShiftRight,
BinaryXor, LogicalNot, EqualTo,
GreaterThan, LessThan, LoadAttribute,
BinarySlice, Pop, UnaryNegate,
BinarySlice, Pop, PopN, UnaryNegate,
BinaryIs, BinaryAs, GreaterOrEqual,
LessOrEqual, BinaryOr, BinaryAnd,
UnaryNot, InPlaceAdd, InPlaceDivide,
InPlaceFloorDiv, InPlaceMod, InPlaceMultiply,
InPlaceSubtract, BinaryFloorDiv, BinaryOf, Raise,
ReRaise, BeginTry, FinishTry, Yield, Await}
# Constant instructions are instructions that operate on the bytecode constant table
const constantInstructions* = {LoadConstant, DeclareName, LoadName, UpdateName, DeleteName}
# Stack instructions operate on the stack at arbitrary offsets
const stackInstructions* = {Call, UpdateNameFast, DeleteNameFast, LoadNameFast}
const jumpInstructions* = {JumpIfFalse, Jump}
# Jump instructions jump at relative or absolute bytecode offsets
const jumpInstructions* = {JumpIfFalse, JumpIfFalsePop, JumpForwards, JumpBackwards,
LongJumpIfFalse, LongJumpIfFalsePop, LongJumpForwards,
LongJumpBackwards}
# Collection instructions push a built-in collection type onto the stack
const collectionInstructions* = {BuildList, BuildDict, BuildSet, BuildTuple}
@ -171,7 +199,7 @@ proc `$`*(self: Chunk): string = &"""Chunk(consts=[{self.consts.join(", ")}], co
proc write*(self: Chunk, newByte: uint8, line: int) =
## Adds the given instruction at the provided line number
## to the given chunk object
assert line > 0
assert line > 0, "line must be greater than zero"
if self.lines.high() >= 1 and self.lines[^2] == line:
self.lines[^1] += 1
else:
@ -218,7 +246,7 @@ proc getLine*(self: Chunk, idx: int): int =
proc findOrAddConstant(self: Chunk, constant: ASTNode): int =
## Small optimization function that reuses the same constant
## if it's already been written before (only if self.reuseConstants
## if it's already been written before (only if self.reuseConsts
## equals true)
if self.reuseConsts:
for i, c in self.consts:
@ -230,7 +258,7 @@ proc findOrAddConstant(self: Chunk, constant: ASTNode): int =
var c = LiteralExpr(c)
var constant = LiteralExpr(constant)
if c.literal.lexeme == constant.literal.lexeme:
# This woldn't work for stuff like 2e3 and 2000.0, but those
# This wouldn't work for stuff like 2e3 and 2000.0, but those
# forms are collapsed in the compiler before being written
# to the constants table
return i
@ -248,7 +276,7 @@ proc findOrAddConstant(self: Chunk, constant: ASTNode): int =
proc addConstant*(self: Chunk, constant: ASTNode): array[3, uint8] =
## Writes a constant to a chunk. Returns its index casted to a 3-byte
## sequence (array). Constant indexes are reused if a constant is used
## more than once!
## more than once and self.reuseConsts equals true
if self.consts.len() == 16777215:
# The constant index is a 24 bit unsigned integer, so that's as far
# as we can index into the constant table (the same applies

View File

@ -65,9 +65,12 @@ type
# yield expression(s). This attribute
# is nil when the parser is at the top-level
# code and is what allows the parser to detect
# errors like return outside functions before
# errors like return outside functions and attempts
# to declare public names inside them before
# compilation even begins
currentFunction: ASTNode
# Stores the current scope depth (0 = global, > 0 local)
scopeDepth: int
proc initParser*(): Parser =
@ -78,6 +81,7 @@ proc initParser*(): Parser =
result.tokens = @[]
result.currentFunction = nil
result.currentLoop = None
result.scopeDepth = 0
# Handy templates to make our life easier, thanks nim!
@ -529,12 +533,14 @@ proc blockStmt(self: Parser): ASTNode =
## Parses block statements. A block
## statement simply opens a new local
## scope
inc(self.scopeDepth)
let tok = self.peek(-1)
var code: seq[ASTNode] = @[]
while not self.check(RightBrace) and not self.done():
code.add(self.declaration())
self.expect(RightBrace, "unterminated block statement")
result = newBlockStmt(code, tok)
dec(self.scopeDepth)
proc breakStmt(self: Parser): ASTNode =
@ -674,15 +680,25 @@ proc tryStmt(self: Parser): ASTNode =
var excName: ASTNode
var handlerBody: ASTNode
while self.match(Except):
if self.check(Identifier):
excName = self.expression()
if excName.kind == identExpr:
discard
elif excName.kind == binaryExpr and BinaryExpr(excName).operator.kind == As:
asName = BinaryExpr(excName).b
if BinaryExpr(excName).a.kind != identExpr:
self.error("expecting alias name after 'except ... as'")
excName = BinaryExpr(excName).a
excName = self.expression()
if excName.kind == identExpr:
continue
elif excName.kind == binaryExpr and BinaryExpr(excName).operator.kind == As:
asName = BinaryExpr(excName).b
if BinaryExpr(excName).b.kind != identExpr:
self.error("expecting alias name after 'except ... as'")
excName = BinaryExpr(excName).a
# Note how we don't use elif here: when the if above sets excName to As'
# first operand, that might be a tuple, which we unpack below
if excName.kind == tupleExpr:
# This allows to do except (a, b, c) as SomeError {...}
# TODO: Consider adding the ability to make exc a sequence
# instead of adding the same body with different exception
# types each time
handlerBody = self.statement()
for element in TupleExpr(excName).members:
handlers.add((body: handlerBody, exc: element, name: asName))
continue
else:
excName = nil
handlerBody = self.statement()
@ -776,10 +792,10 @@ proc varDecl(self: Parser, isStatic: bool = true, isPrivate: bool = true): ASTNo
keyword = "constant"
else:
keyword = "variable"
if not isStatic and self.currentFunction != nil:
self.error("dynamic declarations are illegal inside functions")
if not isPrivate and self.currentFunction != nil:
self.error("public declarations are illegal inside functions")
self.error("cannot bind public names inside functions")
elif not isPrivate and self.scopeDepth > 0:
self.error("cannot bind public names in local scopes")
self.expect(Identifier, &"expecting {keyword} name after '{varKind.lexeme}'")
var name = newIdentExpr(self.peek(-1))
if self.match(Equal):
@ -999,5 +1015,7 @@ proc parse*(self: Parser, tokens: seq[Token], file: string): seq[ASTNode] =
self.file = file
self.current = 0
self.currentLoop = None
self.currentFunction = nil
self.scopeDepth = 0
while not self.done():
result.add(self.declaration())

View File

@ -90,23 +90,28 @@ proc extend[T](s: var seq[T], a: openarray[T]) =
s.add(e)
proc writeHeaders(self: Serializer, stream: var seq[byte], file: string) =
## Writes the JAPL bytecode headers in-place into a byte stream
stream.extend(self.toBytes(BYTECODE_MARKER))
stream.add(byte(JAPL_VERSION.major))
stream.add(byte(JAPL_VERSION.minor))
stream.add(byte(JAPL_VERSION.patch))
stream.add(byte(len(JAPL_BRANCH)))
stream.extend(self.toBytes(JAPL_BRANCH))
if len(JAPL_COMMIT_HASH) != 40:
self.error("the commit hash must be exactly 40 characters long")
stream.extend(self.toBytes(JAPL_COMMIT_HASH))
stream.extend(self.toBytes(getTime().toUnixFloat().int()))
stream.extend(self.toBytes(computeSHA256(file)))
proc dumpBytes*(self: Serializer, chunk: Chunk, file, filename: string): seq[byte] =
## Dumps the given bytecode and file to a sequence of bytes and returns it.
## The file argument must be the actual file's content and is needed to compute its SHA256 hash.
self.file = file
self.filename = filename
self.chunk = chunk
result.extend(self.toBytes(BYTECODE_MARKER))
result.add(byte(JAPL_VERSION.major))
result.add(byte(JAPL_VERSION.minor))
result.add(byte(JAPL_VERSION.patch))
result.add(byte(len(JAPL_BRANCH)))
result.extend(self.toBytes(JAPL_BRANCH))
if len(JAPL_COMMIT_HASH) != 40:
self.error("the commit hash must be exactly 40 characters long")
result.extend(self.toBytes(JAPL_COMMIT_HASH))
result.extend(self.toBytes(getTime().toUnixFloat().int()))
result.extend(self.toBytes(computeSHA256(file)))
self.writeHeaders(result, self.file)
for constant in chunk.consts:
case constant.kind:
of intExpr, floatExpr:
@ -114,9 +119,9 @@ proc dumpBytes*(self: Serializer, chunk: Chunk, file, filename: string): seq[byt
result.add(byte(len(constant.token.lexeme)))
result.extend(self.toBytes(constant.token.lexeme))
of strExpr:
result.add(0x2)
var strip: int = 2
var offset: int = 1
result.add(0x2)
case constant.token.lexeme[0]:
of 'f':
strip = 3

View File

@ -46,7 +46,6 @@ proc printInstruction(instruction: OpCode, newline: bool = false) =
nl()
proc simpleInstruction(instruction: OpCode, offset: int): int =
printInstruction(instruction)
nl()
@ -54,6 +53,7 @@ proc simpleInstruction(instruction: OpCode, offset: int): int =
proc stackInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
## Debugs instructions that operate on a single value on the stack
var slot = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple()
printInstruction(instruction)
stdout.write(&", points to stack index ")
@ -64,7 +64,7 @@ proc stackInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
proc constantInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
# Rebuild the index
## Debugs instructions that operate on a constant
var constant = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple()
printInstruction(instruction)
stdout.write(&", points to constant at position ")
@ -84,7 +84,15 @@ proc constantInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
proc jumpInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
var jump = [chunk.code[offset + 1], chunk.code[offset + 2]].fromDouble()
## Debugs jumps
var jump: int
case instruction:
of JumpIfFalse, JumpIfFalsePop, JumpForwards, JumpBackwards:
jump = [chunk.code[offset + 1], chunk.code[offset + 2]].fromDouble().int()
of LongJumpIfFalse, LongJumpIfFalsePop, LongJumpForwards, LongJumpBackwards:
jump = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple().int()
else:
discard # Unreachable
printInstruction(instruction)
printDebug(&"Jump size: {jump}")
nl()
@ -92,6 +100,7 @@ proc jumpInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
proc collectionInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
## Debugs instructions that push collection types on the stack
var elemCount = int([chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple())
printInstruction(instruction, true)
case instruction: