Hack Club is running a programming language jam from June 08 - 29, where you'll get to hack on and ship a programming language in the span of three weeks and possibly win a chance to get a signed copy of Crafting Interpreters. Sign up here before June 07!
High schooler? Get stickers!
Ok. We have a bunch of nice tokens now.
Our lexer produces a bunch of tokens, but we still can't do much with them quite yet. The next step to solving this problem is to write a parser, which will turn our currently rather one-dimensional list of tokens into what's known as an abstract syntax tree.
Orpheus drew a diagram for us last chapter on how lexers turn a blob of text that's meant to be our program into a list of tokens.
Parsers take these tokens and in turn output a tree of sorts that describes how tokens apply to one another. Here's an example of such a transformation:
prepare rows as 50
We took four tokens and smooshed them into one node. The four tokens described the syntax of our program but the node describes what the syntax actually does - creates a variable with a value of 50!
We're going to write that parser now. It's actually, surprisingly, similar to how we wrote our lexer, except that we're working with a tree now, which means we'll need some form of recursion. We'll get to that. For now, let's start with the usual! Orpheus opens a file called parser.js
and throws this in there:
import { EaselError } from './stdlib.js'
import { TOKENS } from './lexer.js'
export class Parser {
constructor(tokens) {
this.tokens = tokens
this.ast = []
this.current = 0
}
error(token, msg) {
throw new EaselError(
`Syntax error on ${token.line}:${token.column}: ${msg}`
)
}
}
Based on the diagram, the most intuitive next step is to loop through every token that we get. Let's do that in our parse()
method:
export class Parser {
// ...
parse() {
while (this.peekType() !== TOKENS.EOF) continue // TODO
return this.ast
}
}
This is pretty similar to our scanTokens()
method in our lexer, but last time, we dealt with each character at a time. Tokens have more information than that - being able to see the next token's type would also be pretty useful - so we have two helper peek()
methods this time instead of one:
export class Parser {
// ...
peek() {
if (this.current >= this.tokens.length) return null
return this.tokens[this.current]
}
peekType() {
if (this.current >= this.tokens.length) return null
return this.tokens[this.current].type
}
parse() {
// ...
}
}
Cool! Now, what exactly do we do with these tokens?
With our lexer, we looped through every character and checked if it could be a token or belong to a set of characters that would belong to a token. Here, we're going to do something different - we're going to define a grammar for what our program could look like and work our way up until we can describe an entire chunk of code.
First up: what do these have in common?
Orpheus has an answer! They're all super basic. They're what we'll call literals - we can't really break them down further, or they'd just be unintelligible chunks that we can't use, and they also happen to be the most basic values we can reference.
To give an exact definition we'll reference later when writing our parser, literals in Easel can be: strings, numbers, or booleans.
Ok, what's the next obvious step from here? I'm thinking like arrays, or data types that build off literals by collecting them and storing them together. In Easel, it's quite simple - I can think of two data types and those are arrays and structs (or maps or whatever you want to call them. Orpheus decided to stick to structs while editing this, although that might not semantically be the best.)
I bet you already have ideas forming in your head on how to parse these. Arrays? LeftBracket → parse stuff in between → RightBracket.
But let's not get too far ahead of ourselves yet. What is this stuff "in between"?
Well, I imagine we might have an array that looks like [2 + 3, 5, 7]
. That first item in the array is not a literal anymore. It's a binary expression!
So things are kind of coming together now. Literals can form expressions. We don't just have to form binary expressions - we can also form unary ones. Bet those are more rare, but surely you've seen them before: the NOT operator !true
, for example.
Ok. So we know literals can form expressions. What can expressions form? It turns out almost everything else we can think of, especially for a language like Easel. This is because everything else is a statement - a set of expressions. Let's see:
prepare phoneVolume as 50
is assigning a literal to a variable.brush Phone has { phoneVolume }
is a statement.Our job now is to write a parser that can understand this hierarchy. The parser will start by looking for statements, which are made of expressions, which are made of literals. In other words: basic recursive descent!
Ok! Sturp talking! Let's get to smacking the keyboard instead. By then, it should be pretty obvious that we'll need another utility method we're desperately missing: the good ole' eat
method for eating tokens up:
export class Parser {
// ...
eat(type) {
if (this.peekType() === type) return this.tokens[this.current++]
this.error(
this.peek(),
`Expected ${type} but got ${this.peekType().toString()}`
)
}
// ...
}
Now let's begin our descent... into the underworld writing of a parser! Let's start with this basic structure:
export class Parser {
simple() {}
expr() {
const left = this.simple()
// Let's deal with the right side later
return left
}
stmt() {
const next = this.peek()
switch (next.type) {
// Can you guess what's coming here later?
default: {
return this.expr()
}
}
}
}
Ok, understandable enough! Although this isn't going to do anything right now. Let's fix that by updating simple()
:
simple() {
const token = this.eat(this.peekType())
switch (token.type) {
case TOKENS.String:
case TOKENS.Number:
case TOKENS.Boolean: {
return new Ast.Literal(token.content)
}
}
this.error(token, "Expected expression but got " + token)
}
Notice how we throw an error if we don't get anything. Since this is the last method we reach when we're looking at a token, there must be a syntax error if we can't get a proper node.
Something we didn't consider ahead of time was how we would define how these nodes look like. We can do this really easily. Let's create a new file called ast.js
, and put our first kind of node in there:
export class Literal {
constructor(value) {
this.type = 'Literal'
this.value = value
}
}
export default {
Literal
}
Make sure to import this in parser.js
!
import Ast from './ast.js'
Perfect. Back to simple()
now - we're going to add arrays now:
simple() {
let token = this.eat(this.peekType())
switch (token.type) {
// ...
case TOKENS.LeftBracket: {
let items = []
if (this.peekType() !== TOKENS.RightBracket) items = this.exprList()
this.eat(TOKENS.RightBracket)
return new Ast.Array(items)
}
}
this.error(token, "Expected expression but got " + token)
}
Our predictions were correct. We gobble up the left bracket, create a new array, and if we haven't already reached the right bracket (i.e. empty array), we grab the list of expressions in between.
Now, we make use of an extra helper called exprList()
to do this.
exprList() {
let exprs = []
exprs.push(this.expr())
while (this.peekType() === TOKENS.Comma) {
this.eat(TOKENS.Comma)
exprs.push(this.expr())
}
return exprs
}
We'll also need to add a new kind of node:
export class Array {
constructor(value) {
this.type = 'Array'
this.value = value
}
}
export default {
Array,
// ...
}
One last thing we'll add for now to simple()
is:
simple() {
// ...
switch (token.type) {
// ...
case TOKENS.Identifier: {
return new Ast.Var(token.value)
}
}
}
This might confuse you a bit - didn't we say variables were statements? Yes, but this is not that - this is referring to when we reference a variable, not direct assignment - e.g., the variable reference in ink(x)
.
This does make use of a new kind of node though, so let's add that to ast.js
:
export class Var {
constructor(name, value) {
this.type = 'Var'
this.name = name
this.value = value
}
}
export default {
// ...,
Var
}
Cool. That takes care of simple()
for now. Let's move up the hierarchy and tweak expr()
! Right now, we've only grabbed the left side of what should be a binary operation. Let's grab the operator and the right side after that:
expr() {
// expr has two sides
const left = this.simple()
if (isOp(this.peekType())) {
const op = this.eat(this.peekType()).value
const right = this.expr()
return new Ast.Binary(left, op, right)
}
return left
}
Looks like we need an extra isOp
function. Orpheus and I are going to put this at the top of parser.js
:
const isOp = type =>
[
TOKENS.Or,
TOKENS.And,
TOKENS.Equiv,
TOKENS.NotEquiv,
TOKENS.Gt,
TOKENS.Gte,
TOKENS.Lt,
TOKENS.Lte,
TOKENS.Plus,
TOKENS.Minus,
TOKENS.Asterisk,
TOKENS.Slash
].includes(type)
Our binary expression node is pretty simple:
export class Binary {
constructor(left, operator, right) {
this.type = 'Binary'
this.left = left
this.operator = operator
this.right = right
}
}
export default {
// ...,
Binary
}
Ok, I think we've got enough here where we can test the parser out and see what happens. Let's remove that continue
we had earlier in parse()
:
parse() {
while (this.peekType() !== TOKENS.EOF) this.ast.push(this.stmt())
return this.ast
}
We should, of course, start using our parser in easel.js
:
import { Parser } from './parser.js'
// ...
if (location) {
const program = await readFile(location)
const lexer = new Lexer(program)
try {
lexer.scanTokens()
} catch (err) {
console.log(err)
process.exit(1)
} finally {
if (debug) await writeFile('tokens.json', JSON.stringify(lexer.tokens, null, 2))
}
const parser = new Parser(lexer.tokens)
try {
parser.parse()
} catch (err) {
console.log(err)
} finally {
if (debug) await writeFile('ast.json', JSON.stringify(parser.ast, null, 2))
}
}
// ...
Cool! Let's test this out with a test program in test.easel
:
[4 + 21]
2006 / 4012 + 2
Try it out with node easel.js test.easel --dbg
! It's not really a complete program because we haven't taken care of statements yet, but I digress. You'll get something like this ast.json
:
[
{
type: 'Literal',
value: [
{
type: 'Binary',
left: { type: 'Literal', value: 4 },
operator: '+',
right: { type: 'Literal', value: 21 }
}
]
},
{
type: 'Binary',
left: { type: 'Literal', value: 2006 },
operator: '/',
right: {
type: 'Binary',
left: { type: 'Literal', value: 4012 },
operator: '+',
right: { type: 'Literal', value: 2 }
}
}
]
This looks right, but there's just one minor problem... if we run that second node, this is what we get:
Oopsies. We forgot about PEMDAS. We should fix that. Never fear! Orpheus has put on her wizard hat and come up with a rather ingenious way to tackle this.
First things first, all operators should have a precedence hierarchy. If I see an addition operator and a multiplication operator in the same expression, then I should combine the operators next to the multiplication operator first before moving on to the addition operator. But if we've already parsed the addition operator, we can do a little switcheroo. Orpheus wants to show you what she means by this. Read the following in what you might imagine to be her voice.
First things first, at the top of parser.js
!
const opOrder = {
'<': 0,
'<=': 0,
'>': 0,
'>=': 0,
'!=': 0,
'==': 0,
'&&': 0,
'||': 0,
'+': 1,
'-': 1,
'*': 2,
'/': 2
}
Next, I am going to add a little if-statement that checks if right
is also a binary expression node, and compare the operators if so:
expr() {
// expr has two sides
let left = this.simple()
if (isOp(this.peekType())) {
const op = this.eat(this.peekType()).value
let right = this.expr()
if (right instanceof Ast.Binary && opOrder[op] > opOrder[right.operator])
// Quick reordering based on precedence
return new Ast.Binary(
new Ast.Binary(left, op, right.left),
right.operator,
right.right
)
return new Ast.Binary(left, op, right)
}
return left
}
Ok. Back to me. I'll take the mic back. Thanks. [grabs mic]
And that fixes all our problems! Well, not all of our problems, of course. Right now it might also be a good time to deal with parentheses:
(15 + 15) / 3
If you run this currently, it'll throw a syntax error, because we haven't dealt with parentheses in this scenario! In this scenario, the parentheses represent an expression inside an expression that we should evaluate first. To take care of this precedence, we can place this in simple()
to take care of this:
case TOKENS.LeftParen: {
const expr = this.expr()
this.eat(TOKENS.RightParen)
return expr
}
An expression inside an expression, essentially. This works because now expr()
will call simple()
and evaluate (15 + 15)
on its own, before peeking at /
. Now if we run our parser on this, we'll get the proper-looking AST and not a syntax error:
Ok, awesome cool. What's next? Statements! Let's take care of that now. Remember this earlier?
stmt() {
const next = this.peek()
switch (next.type) {
// Can you guess what's coming here later?
default: {
return this.expr()
}
}
}
Now we'll replace that comment with some actual code. What exactly is going there? Well, we're going to parse anything that could lead to a block of expressions or be used to store expressions.
Perhaps the best example to run through first is parsing functions, so let's take a look at that! The structure of a function is as follows: it starts with the sketch
keyword, then an identifier that's the name of the function. If it takes in parameters, we'll know because the needs
keyword will come afterwards, followed by a list of identifiers that represent the parameters. Of course, we have the braces and the body that follow afterwards.
We'll have a nested function inside stmt()
:
stmt() {
const funcStmt = () => {
this.eatKeyword('sketch')
const name = this.eat(TOKENS.Identifier).value
let params = []
if (this.peekKeyword('needs')) {
// Parameters
this.eatKeyword('needs')
this.eat(TOKENS.LeftParen)
params = this.identifierList()
this.eat(TOKENS.RightParen)
}
this.eat(TOKENS.LeftBrace)
let body = []
while (this.peekType() !== TOKENS.RightBrace) body.push(this.stmt())
this.eat(TOKENS.RightBrace)
return new Ast.Func(name, params, body)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'sketch': {
return funcStmt()
}
//...
}
}
// ...
}
}
We need two extra utilities that wrap peek()
and eat()
: peekKeyword()
and eatKeyword()
. eatKeyword()
will throw an error if the next token isn't a keyword or if it doesn't match up to the keyword provided:
peekKeyword(keyword) {
if (this.peekType() !== TOKENS.Keyword || this.peek().value !== keyword)
return null
return this.peek()
}
eatKeyword(keyword) {
if (this.peekType() !== TOKENS.Keyword)
this.error(
this.peek(),
`Expected ${TOKENS.Keyword} but got ${this.peekType()}`
)
else if (this.peek().value !== keyword)
this.error(
this.peek(),
`Expected keyword ${keyword} but got keyword ${this.peek().value}`
)
return this.eat(TOKENS.Keyword)
}
We also need an extra helper method, similar to exprList()
, called identifierList()
:
identifierList() {
let identifiers = []
identifiers.push(this.eat(TOKENS.Identifier).value)
while (this.peekType() === TOKENS.Comma) {
this.eat(TOKENS.Comma)
identifiers.push(this.eat(TOKENS.Identifier).value)
}
return identifiers
}
Instead of returning a list of expressions by calling expr()
, identifierList()
just returns a list of identifiers by gobbling them up.
Of course, we also need Ast.Func
in ast.js
:
export class Func {
constructor(name, params, body) {
this.type = 'Func'
this.name = name
this.params = params
this.body = body
}
}
export {
Func,
// ...
}
With functions come return statements. Let's work on those next. They're really simple, just eat the return keyword and return an expression:
stmt() {
const returnStmt = () => {
this.eatKeyword('finished')
return new Ast.Return(this.expr())
}
// ...
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'finished':
return returnStmt()
// ...
}
}
// ...
}
}
Of course, Ast.Return
:
export class Return {
constructor(value) {
this.type = 'Return'
this.value = value
}
}
export default {
// ...,
Return
}
Let's deal with the loops next. For loops will gobble up the loop
keyword, grab the identifier we'll be using to keep track of the current iteration, the range, and of course, the body:
const forStmt = () => {
this.eatKeyword('loop')
const id = this.eat(TOKENS.Identifier).value
this.eatKeyword('through')
this.eat(TOKENS.LeftParen)
const range = this.exprList()
if (range.length !== 2)
this.error(
range[range.length - 1],
'Expected (start, end) range but received more arguments than expected'
)
this.eat(TOKENS.RightParen)
this.eat(TOKENS.LeftBrace)
let body = []
while (this.peekType() !== TOKENS.RightBrace) body.push(this.stmt())
this.eat(TOKENS.RightBrace)
return new Ast.For(id, range, body)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'loop': {
return forStmt()
}
// ...
}
}
// ...
}
In ast.js
:
export class For {
constructor(id, range, body) {
this.type = 'For'
this.id = id
this.range = range
this.body = body
}
}
export default {
// ...,
For
}
Lovely. While loops are simpler, all we have to do is grab the condition and the body afterwards:
const whileStmt = () => {
this.eatKeyword('while')
this.eat(TOKENS.LeftParen)
const condition = this.expr()
this.eat(TOKENS.RightParen)
this.eat(TOKENS.LeftBrace)
let body = []
while (this.peekType() !== TOKENS.RightBrace) body.push(this.stmt())
this.eat(TOKENS.RightBrace)
return new Ast.While(condition, body)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'while': {
return whileStmt()
}
// ...
}
}
// ...
}
In ast.js
:
export class While {
constructor(condition, body) {
this.type = 'While'
this.condition = condition
this.body = body
}
}
export default {
// ...,
While
}
Conditionals are also pretty intuitive. We'll do this recursively - we'll start by eating up the if
keyword, parsing the condition, and the body. If elif
or else
pops up afterwards, we'll also deal with those by calling conditionalStmt
again:
const conditionalStmt = keyword => {
this.eatKeyword(keyword)
let condition = new Ast.Literal(true)
if (keyword !== 'else') {
this.eat(TOKENS.LeftParen)
condition = this.expr()
this.eat(TOKENS.RightParen)
}
this.eat(TOKENS.LeftBrace)
let body = []
while (this.peekType() !== TOKENS.RightBrace) body.push(this.stmt())
this.eat(TOKENS.RightBrace)
let otherwise = []
while (this.peekKeyword('elif') || this.peekKeyword('else'))
otherwise.push(conditionalStmt(this.peek().value))
return new Ast.Conditional(condition, body, otherwise)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'if': {
return conditionalStmt('if')
}
// ...
}
}
// ...
}
In ast.js
:
export class Conditional {
constructor(condition, body, otherwise) {
this.type = 'Conditional'
this.condition = condition
this.body = body
this.otherwise = otherwise
}
}
export default {
// ...,
Conditional
}
We should be able to assign values to a variable, but we should also be able to assign values to a property, like prepare cell.live as false
. We can do this in the same function by simply checking that first identifier and checking if a Period token comes afterwards:
const assignStmt = () => {
this.eatKeyword('prepare')
const name = this.eat(TOKENS.Identifier).value
if (this.peekType() === TOKENS.Period) {
// Setter
this.eat(TOKENS.Period)
const property = this.eat(TOKENS.Identifier).value
this.eatKeyword('as')
const value = this.expr()
return new Ast.Set(name, property, value)
}
this.eatKeyword('as')
const value = this.expr()
return new Ast.Var(name, value)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'prepare': {
return assignStmt()
}
// ...
}
}
// ...
}
Ast.Var
exists, but Ast.Set
doesn't, so let's add that in ast.js
:
export class Set {
constructor(caller, property, value) {
this.type = 'Set'
this.caller = caller
this.property = property
this.value = value
}
}
export default {
// ...,
Set
}
Last but not least, structs! We need to have a statement for defining structs. Defining a struct looks like brush Cell has { x, y, live }
, so we'll gobble up the brush
keyword, get the name of the struct, gobble up the has
keyword, the braces, and then get the members of the struct (a list of identifiers):
const structStmt = () => {
this.eatKeyword('brush')
const name = this.eat(TOKENS.Identifier).value
this.eatKeyword('has')
this.eat(TOKENS.LeftBrace)
const members = this.identifierList()
this.eat(TOKENS.RightBrace)
return new Ast.Struct(name, members)
}
const next = this.peek()
switch (next.type) {
case TOKENS.Keyword: {
switch (next.value) {
case 'brush':
return structStmt()
// ...
}
}
// ...
}
In ast.js
:
export class Struct {
constructor(name, members) {
this.type = 'Struct'
this.name = name
this.members = members
}
}
export default {
// ...,
Struct
}
We haven't accounted for actual struct copies with values yet, so let's take care of that!
simple() {
let token = this.eat(this.peekType())
switch (token.type) {
case TOKENS.Keyword: {
if (token.value === 'prep') {
// Instance is essentially a struct copy
const id = this.eat(TOKENS.Identifier).value
this.eat(TOKENS.LeftParen)
let members = {}
while (this.peekType() !== TOKENS.RightParen) {
const member = this.eat(TOKENS.Identifier).value
this.eat(TOKENS.Colon)
members[member] = this.expr()
if (this.peekType() === TOKENS.Comma) this.eat(TOKENS.Comma)
}
this.eat(TOKENS.RightParen)
return new Ast.Instance(id, members)
}
break
// ...
}
}
this.error(token, 'Expected expression but got ' + token)
}
Let's add Instance
to ast.js
:
export class Instance {
constructor(name, members) {
this.type = 'Instance'
this.name = name
this.members = members
}
}
export default {
// ...,
Instance
}
Bring, bling! Bring, bring!
I think that's Orpheus' phone ringing. She's going to answer it.
Who is it, Orpheus?
Wait, our mysterious sender? Nah, beware of those scam calls. We've talked about this before!
What do you mean, he's telling me to stop drinking so much hot chocolate? [sputters] How does he even know about that? Why is he calling us? Wait, he's a he? Ok, so we know he's some random dude harassing us.
Just reminding us about... calls? Calls! Oh, right! We almost forgot about those! Our programming language would be pretty useful without those.
We're missing an extra step between binary expressions and simple literals, and that happens to be function calls and getters. I like to think of this like a chain, like orpheus.picksUp(phone).numbers[0]
. It's an intermediate step between expr()
and simple()
because it can be one-half of an expression but evaluates down to a simple value.
call() {
let expr = this.simple()
while (true) {
if (this.peekType() === TOKENS.LeftParen) {
this.eat(TOKENS.LeftParen)
let args = []
if (this.peekType() !== TOKENS.RightParen) args = this.exprList()
this.eat(TOKENS.RightParen)
expr = new Ast.Call(expr, args)
} else if (this.peekType() === TOKENS.LeftBracket) {
// This is also a getter, but we accept an expression rather than just an identifier
this.eat(TOKENS.LeftBracket)
const property = this.expr()
this.eat(TOKENS.RightBracket)
expr = new Ast.Get(expr, property, true)
} else if (this.peekType() === TOKENS.Period) {
this.eat(TOKENS.Period)
const property = this.eat(TOKENS.Identifier).value
expr = new Ast.Get(expr, property)
} else break
}
return expr
}
expr() {
const left = this.call()
// ...
}
Once again, we have the cascading value. We call simple()
first, and while we can keep chaining, we keep chaining and wrapping around that initial simple()
with one of two kinds of nodes: Ast.Call
, which represents a function call, or Ast.Get
, which represents a property getter. Let's add them to ast.js
now:
export class Call {
constructor(caller, args) {
this.type = 'Call'
this.caller = caller
this.args = args
}
}
export class Get {
constructor(caller, property, isExpr = false) {
this.type = 'Get'
this.caller = caller
this.property = property
this.isExpr = isExpr
}
}
export default {
// ...,
Call,
Get
}
Getting random calls from a stranger is kind of fun. You're right about that, Orpheus.
[Actually, I have a funny story. One time, one of my friends got his phone number changed, and some other bloke with the same name got his phone number. (What are the coincidences?) Anyways, a friend tried to text him but ended up texting this other person with the same name who played along for a solid two weeks. That's my funny story of the day.]
There's just one last token we haven't accounted for. I'll give you a moment to think about it. Which token could we possibly be forgetting about?
The NOT operator! That's right - our only unary operator. We haven't dealt with unary operators yet. Let's take care of that now. It makes the most sense to cascade from expr()
(binary expressions) to unary expressions to call()
, so let's first add a unary()
method:
unary() {
// unary has one side
if (this.peekType() === TOKENS.Not) {
const op = this.eat(this.peekType()).value
return new Ast.Unary(op, this.unary())
}
return this.call()
}
In ast.js
:
export class Unary {
constructor(operator, apply) {
this.type = 'Unary'
this.operator = operator
this.apply = apply
}
}
export default {
// ...,
Unary
}
Instead of calling call()
in expr()
now, we'll replace it with unary()
, which will cascade down to call()
:
expr() {
const left = this.unary()
// ...
}
Let's test out everything we have so far. Try running node easel.js test.easel --dbg
, and you'll get a new debug file containing the AST in ast.json
. Mine looks something like this:
[
{
"type": "Var",
"name": "rows",
"value": { "type": "Literal", "value": 50 }
},
{
"type": "Var",
"name": "cols",
"value": { "type": "Literal", "value": 20 }
},
{ "type": "Struct", "name": "Cell", "members": ["x", "y", "live"] },
{
"type": "Func",
"name": "seed",
"params": [],
"body": [
{
"type": "Var",
"name": "cells",
"value": { "type": "Array", "value": [] }
},
{
"type": "For",
"id": "x",
"range": [
{ "type": "Literal", "value": 0 },
{ "type": "Var", "name": "rows" }
],
"body": [
{
"type": "For",
"id": "y",
"range": [
{ "type": "Literal", "value": 0 },
{ "type": "Var", "name": "cols" }
],
"body": [
{
"type": "Var",
"name": "live",
"value": { "type": "Literal", "value": false }
},
{
"type": "Var",
"name": "chance",
"value": {
"type": "Call",
"caller": { "type": "Var", "name": "random" },
"args": [
{ "type": "Literal", "value": 0 },
{ "type": "Literal", "value": 100 }
]
}
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": { "type": "Var", "name": "chance" },
"operator": "<",
"right": { "type": "Literal", "value": 20 }
},
"body": [
{
"type": "Var",
"name": "live",
"value": { "type": "Literal", "value": true }
}
],
"otherwise": []
},
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Instance",
"name": "Cell",
"members": {
"x": { "type": "Var", "name": "x" },
"y": { "type": "Var", "name": "y" },
"live": { "type": "Var", "name": "live" }
}
}
]
}
]
}
]
},
{ "type": "Return", "value": { "type": "Var", "name": "cells" } }
]
},
{
"type": "Var",
"name": "cells",
"value": {
"type": "Call",
"caller": { "type": "Var", "name": "seed" },
"args": []
}
},
{
"type": "Func",
"name": "getNeighbors",
"params": ["cells", "index"],
"body": [
{
"type": "Var",
"name": "neighbors",
"value": { "type": "Array", "value": [] }
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"operator": ">",
"right": { "type": "Literal", "value": 0 }
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": { "type": "Var", "name": "rows" }
},
"operator": ">",
"right": { "type": "Literal", "value": 0 }
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": { "type": "Var", "name": "rows" }
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "+",
"right": { "type": "Literal", "value": 1 }
}
},
"operator": ">",
"right": { "type": "Literal", "value": 0 }
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "+",
"right": { "type": "Literal", "value": 1 }
}
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": ">",
"right": { "type": "Literal", "value": 0 }
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "<",
"right": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "length",
"isExpr": false
},
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": { "type": "Literal", "value": 1 }
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"operator": "<",
"right": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "length",
"isExpr": false
},
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": { "type": "Var", "name": "rows" }
},
"operator": "<",
"right": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "length",
"isExpr": false
},
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": { "type": "Literal", "value": 1 }
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "+",
"right": { "type": "Literal", "value": 1 }
}
},
"operator": "<",
"right": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "length",
"isExpr": false
},
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": {
"type": "Binary",
"left": { "type": "Var", "name": "index" },
"operator": "+",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "rows" },
"operator": "-",
"right": { "type": "Literal", "value": 1 }
}
},
"isExpr": true
}
]
}
],
"otherwise": []
},
{
"type": "Var",
"name": "alive",
"value": { "type": "Array", "value": [] }
},
{
"type": "For",
"id": "i",
"range": [
{ "type": "Literal", "value": 0 },
{
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "length",
"isExpr": false
}
],
"body": [
{
"type": "Conditional",
"condition": {
"type": "Get",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": { "type": "Var", "name": "i" },
"isExpr": true
},
"property": "live",
"isExpr": false
},
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "alive" },
"property": "add",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": { "type": "Var", "name": "i" },
"isExpr": true
}
]
}
],
"otherwise": []
}
]
},
{ "type": "Return", "value": { "type": "Var", "name": "alive" } }
]
},
{
"type": "Func",
"name": "painting",
"params": [],
"body": [
{
"type": "For",
"id": "i",
"range": [
{ "type": "Literal", "value": 0 },
{
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": "length",
"isExpr": false
}
],
"body": [
{
"type": "Var",
"name": "cell",
"value": {
"type": "Get",
"caller": { "type": "Var", "name": "cells" },
"property": { "type": "Var", "name": "i" },
"isExpr": true
}
},
{
"type": "Var",
"name": "neighbors",
"value": {
"type": "Call",
"caller": { "type": "Var", "name": "getNeighbors" },
"args": [
{ "type": "Var", "name": "cells" },
{ "type": "Var", "name": "i" }
]
}
},
{
"type": "Conditional",
"condition": {
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "live",
"isExpr": false
},
"body": [
{
"type": "Conditional",
"condition": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "length",
"isExpr": false
},
"operator": "<",
"right": {
"type": "Binary",
"left": { "type": "Literal", "value": 2 },
"operator": "||",
"right": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "neighbors" },
"property": "length",
"isExpr": false
},
"operator": ">",
"right": { "type": "Literal", "value": 3 }
}
}
},
"body": [
{
"type": "Set",
"caller": "cell",
"property": "live",
"value": { "type": "Literal", "value": false }
}
],
"otherwise": [
{
"type": "Conditional",
"condition": {
"type": "Unary",
"operator": "!",
"apply": {
"type": "Binary",
"left": {
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "live",
"isExpr": false
},
"operator": "&&",
"right": {
"type": "Binary",
"left": { "type": "Var", "name": "neighbors" },
"operator": "==",
"right": { "type": "Literal", "value": 3 }
}
}
},
"body": [
{
"type": "Set",
"caller": "cell",
"property": "live",
"value": { "type": "Literal", "value": true }
}
],
"otherwise": []
}
]
}
],
"otherwise": []
},
{
"type": "Conditional",
"condition": {
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "live",
"isExpr": false
},
"body": [
{
"type": "Var",
"name": "color",
"value": {
"type": "Instance",
"name": "Color",
"members": {
"r": { "type": "Literal", "value": 0 },
"g": { "type": "Literal", "value": 255 },
"b": { "type": "Literal", "value": 0 }
}
}
},
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "Canvas" },
"property": "fill",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "x",
"isExpr": false
},
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "y",
"isExpr": false
}
]
},
{
"type": "Call",
"caller": { "type": "Var", "name": "ink" },
"args": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "Canvas" },
"property": "get",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "x",
"isExpr": false
},
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "y",
"isExpr": false
}
]
}
]
}
],
"otherwise": [
{
"type": "Conditional",
"condition": { "type": "Literal", "value": true },
"body": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "Canvas" },
"property": "erase",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "x",
"isExpr": false
},
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "y",
"isExpr": false
}
]
},
{
"type": "Call",
"caller": { "type": "Var", "name": "ink" },
"args": [
{
"type": "Call",
"caller": {
"type": "Get",
"caller": { "type": "Var", "name": "Canvas" },
"property": "get",
"isExpr": false
},
"args": [
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "x",
"isExpr": false
},
{
"type": "Get",
"caller": { "type": "Var", "name": "cell" },
"property": "y",
"isExpr": false
}
]
}
]
}
],
"otherwise": []
}
]
}
]
}
]
},
{
"type": "Call",
"caller": { "type": "Var", "name": "painting" },
"args": []
}
]
Awesome! We've got a parser working. Clap yourself on the back - that wasn't so hard, was it?
That was definitely longer than writing a lexer, for sure, but rev up - we're going to see our programming language in action in the next part and get Conway's Game of Life running (eventually)!
The complete code here is at parser.js.