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:
parent
9160cb32db
commit
2ab4bdc46d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue