From f7480287f9bcc96945477fc4455a88665b1d878c Mon Sep 17 00:00:00 2001 From: alex <16batesa@gmail.com> Date: Tue, 3 Apr 2018 10:33:57 +0100 Subject: [PATCH 1/3] Add Wren language --- .gitmodules | 3 +++ grammars.yml | 2 ++ lib/linguist/languages.yml | 8 +++++++ samples/Wren/ex.wren | 15 +++++++++++++ vendor/README.md | 1 + vendor/grammars/wren-sublime | 1 + vendor/licenses/grammar/wren-sublime.txt | 28 ++++++++++++++++++++++++ 7 files changed, 58 insertions(+) create mode 100644 samples/Wren/ex.wren create mode 160000 vendor/grammars/wren-sublime create mode 100644 vendor/licenses/grammar/wren-sublime.txt diff --git a/.gitmodules b/.gitmodules index b9e44d4aee..680fa7d8a7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -895,6 +895,9 @@ [submodule "vendor/grammars/wdl-sublime-syntax-highlighter"] path = vendor/grammars/wdl-sublime-syntax-highlighter url = https://github.com/broadinstitute/wdl-sublime-syntax-highlighter +[submodule "vendor/grammars/wren-sublime"] + path = vendor/grammars/wren-sublime + url = https://github.com/munificent/wren-sublime [submodule "vendor/grammars/xc.tmbundle"] path = vendor/grammars/xc.tmbundle url = https://github.com/graymalkin/xc.tmbundle diff --git a/grammars.yml b/grammars.yml index c14001e124..753f4d8d7d 100755 --- a/grammars.yml +++ b/grammars.yml @@ -734,6 +734,8 @@ vendor/grammars/vue-syntax-highlight: - text.html.vue vendor/grammars/wdl-sublime-syntax-highlighter: - source.wdl +vendor/grammars/wren-sublime: +- source.wren vendor/grammars/xc.tmbundle: - source.xc vendor/grammars/xml.tmbundle: diff --git a/lib/linguist/languages.yml b/lib/linguist/languages.yml index f19558681f..b14447bd57 100755 --- a/lib/linguist/languages.yml +++ b/lib/linguist/languages.yml @@ -5325,6 +5325,14 @@ wisp: - ".wisp" tm_scope: source.clojure language_id: 420 +Wren: + type: programming + color: "#3399CC" + extensions: + - ".wren" + ace_mode: text + tm_scope: source.wren + language_id: 432 xBase: type: programming color: "#403a40" diff --git a/samples/Wren/ex.wren b/samples/Wren/ex.wren new file mode 100644 index 0000000000..35a5773d3f --- /dev/null +++ b/samples/Wren/ex.wren @@ -0,0 +1,15 @@ +System.print("Hello, world!") + +class Wren { + flyTo(city) { + System.print("Flying to %(city)") + } +} + +// I am a comment + +var adjectives = Fiber.new { + ["small", "clean", "fast"].each {|word| Fiber.yield(word) } +} + +while (!adjectives.isDone) System.print(adjectives.call()) diff --git a/vendor/README.md b/vendor/README.md index 6165cbeb4c..e3879d3883 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -395,6 +395,7 @@ This is a list of grammars that Linguist selects to provide syntax highlighting - **WebIDL:** [andik/IDL-Syntax](https://github.com/andik/IDL-Syntax) - **wisp:** [atom/language-clojure](https://github.com/atom/language-clojure) - **World of Warcraft Addon Data:** [nebularg/language-toc-wow](https://github.com/nebularg/language-toc-wow) +- **Wren:** [munificent/wren-sublime](https://github.com/munificent/wren-sublime) - **X10:** [x10-lang/x10-highlighting](https://github.com/x10-lang/x10-highlighting) - **xBase:** [hernad/atom-language-harbour](https://github.com/hernad/atom-language-harbour) - **XC:** [graymalkin/xc.tmbundle](https://github.com/graymalkin/xc.tmbundle) diff --git a/vendor/grammars/wren-sublime b/vendor/grammars/wren-sublime new file mode 160000 index 0000000000..94cc343b26 --- /dev/null +++ b/vendor/grammars/wren-sublime @@ -0,0 +1 @@ +Subproject commit 94cc343b26ffba3c850313bee3cdfc42e00f5e1f diff --git a/vendor/licenses/grammar/wren-sublime.txt b/vendor/licenses/grammar/wren-sublime.txt new file mode 100644 index 0000000000..aa5010e4c2 --- /dev/null +++ b/vendor/licenses/grammar/wren-sublime.txt @@ -0,0 +1,28 @@ +--- +type: grammar +name: wren-sublime +license: other +--- +The Wren Sublime Package uses the MIT License: + +Copyright (c) 2013-2016 Robert Nystrom + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. (As clarification, there is no +requirement that the copyright notice and permission be included in binary +distributions of the Software.) + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From b6a34ca3806c5831030b46b3207573f424eb8c89 Mon Sep 17 00:00:00 2001 From: alex <16batesa@gmail.com> Date: Sat, 7 Apr 2018 11:41:25 +0100 Subject: [PATCH 2/3] Fix Wren color and position in languages.yml --- lib/linguist/languages.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/linguist/languages.yml b/lib/linguist/languages.yml index b14447bd57..50cdf1066c 100755 --- a/lib/linguist/languages.yml +++ b/lib/linguist/languages.yml @@ -4941,6 +4941,14 @@ World of Warcraft Addon Data: tm_scope: source.toc ace_mode: text language_id: 396 +Wren: + type: programming + color: "#52667a" + extensions: + - ".wren" + ace_mode: text + tm_scope: source.wren + language_id: 432 X10: type: programming aliases: @@ -5325,14 +5333,6 @@ wisp: - ".wisp" tm_scope: source.clojure language_id: 420 -Wren: - type: programming - color: "#3399CC" - extensions: - - ".wren" - ace_mode: text - tm_scope: source.wren - language_id: 432 xBase: type: programming color: "#403a40" From 302bf23b33199d5bb46f94642a2c2e5dfbdeebd3 Mon Sep 17 00:00:00 2001 From: alex <16batesa@gmail.com> Date: Sat, 7 Apr 2018 11:50:15 +0100 Subject: [PATCH 3/3] Use real-world Wren samples --- samples/Wren/ast.wren | 521 +++++++++++++++++++++++++++ samples/Wren/ex.wren | 15 - samples/Wren/parser.wren | 735 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1256 insertions(+), 15 deletions(-) create mode 100644 samples/Wren/ast.wren delete mode 100644 samples/Wren/ex.wren create mode 100644 samples/Wren/parser.wren diff --git a/samples/Wren/ast.wren b/samples/Wren/ast.wren new file mode 100644 index 0000000000..fb6115bb2d --- /dev/null +++ b/samples/Wren/ast.wren @@ -0,0 +1,521 @@ +// https://github.com/munificent/wrenalyzer/blob/master/ast.wren +// MIT License + +class Node {} + +class Expr is Node {} + +class Stmt is Node {} + +class Module is Node { + construct new(statements) { + _statements = statements + } + + statements { _statements } + + accept(visitor) { visitor.visitModule(this) } + + toString { "Module(%(_statements))" } +} + +class MapEntry { + construct new(key, value) { + _key = key + _value = value + } + + key { _key } + value { _value } + + toString { "%(_key): %(_value)" } +} + +class Method { + construct new(foreignKeyword, staticKeyword, constructKeyword, name, body) { + _foreignKeyword = foreignKeyword + _staticKeyword = staticKeyword + _constructKeyword = constructKeyword + _name = name + _body = body + } + + foreignKeyword { _foreignKeyword } + staticKeyword { _staticKeyword } + constructKeyword { _constructKeyword } + name { _name } + body { _body } + + accept(visitor) { visitor.visitMethod(this) } + + toString { + return "Method(%(_staticKeyword) %(_constructKeyword) %(_name) %(_body))" + } +} + +/// A block argument or method body. +class Body { + construct new(parameters, expression, statements) { + _parameters = parameters + _expression = expression + _statements = statements + } + + parameters { _parameters } + expression { _expression } + statements { _statements } + + accept(visitor) { visitor.visitBody(this) } + + toString { + return "Body(%(_parameters) %(_expression) %(_statements))" + } +} + +class ListExpr is Expr { + construct new(leftBracket, elements, rightBracket) { + _leftBracket = leftBracket + _elements = elements + _rightBracket = rightBracket + } + + leftBracket { _leftBracket } + elements { _elements } + rightBracket { _rightBracket } + + accept(visitor) { visitor.visitListExpr(this) } + + toString { + return "List(%(_leftBracket) %(_elements) %(_rightBracket))" + } +} + +class ThisExpr is Expr { + construct new(keyword) { + _keyword = keyword + } + + keyword { _keyword } + + accept(visitor) { visitor.visitThisExpr(this) } + + toString { + return "This(%(_keyword))" + } +} + +class NullExpr is Expr { + construct new(value) { + _value = value + } + + value { _value } + + accept(visitor) { visitor.visitNullExpr(this) } + + toString { + return "Null(%(_value))" + } +} + +class StaticFieldExpr is Expr { + construct new(name) { + _name = name + } + + name { _name } + + accept(visitor) { visitor.visitStaticFieldExpr(this) } + + toString { + return "StaticField(%(_name))" + } +} + +class FieldExpr is Expr { + construct new(name) { + _name = name + } + + name { _name } + + accept(visitor) { visitor.visitFieldExpr(this) } + + toString { + return "Field(%(_name))" + } +} + +class CallExpr is Expr { + construct new(receiver, name, arguments, blockArgument) { + _receiver = receiver + _name = name + _arguments = arguments + _blockArgument = blockArgument + } + + receiver { _receiver } + name { _name } + arguments { _arguments } + blockArgument { _blockArgument } + + accept(visitor) { visitor.visitCallExpr(this) } + + toString { + return "Call(%(_receiver) %(_name) %(_arguments) %(_blockArgument))" + } +} + +class PrefixExpr is Expr { + construct new(operator, right) { + _operator = operator + _right = right + } + + operator { _operator } + right { _right } + + accept(visitor) { visitor.visitPrefixExpr(this) } + + toString { + return "Prefix(%(_operator) %(_right))" + } +} + +class GroupingExpr is Expr { + construct new(leftParen, expression, rightParen) { + _leftParen = leftParen + _expression = expression + _rightParen = rightParen + } + + leftParen { _leftParen } + expression { _expression } + rightParen { _rightParen } + + accept(visitor) { visitor.visitGroupingExpr(this) } + + toString { + return "Grouping(%(_leftParen) %(_expression) %(_rightParen))" + } +} + +class AssignmentExpr is Expr { + construct new(target, equal, value) { + _target = target + _equal = equal + _value = value + } + + target { _target } + equal { _equal } + value { _value } + + accept(visitor) { visitor.visitAssignmentExpr(this) } + + toString { + return "Assignment(%(_target) %(_equal) %(_value))" + } +} + +class InfixExpr is Expr { + construct new(left, operator, right) { + _left = left + _operator = operator + _right = right + } + + left { _left } + operator { _operator } + right { _right } + + accept(visitor) { visitor.visitInfixExpr(this) } + + toString { + return "Infix(%(_left) %(_operator) %(_right))" + } +} + +class MapExpr is Expr { + construct new(leftBrace, entries, rightBrace) { + _leftBrace = leftBrace + _entries = entries + _rightBrace = rightBrace + } + + leftBrace { _leftBrace } + entries { _entries } + rightBrace { _rightBrace } + + accept(visitor) { visitor.visitMapExpr(this) } + + toString { + return "Map(%(_leftBrace) %(_entries) %(_rightBrace))" + } +} + +class ConditionalExpr is Expr { + construct new(condition, question, thenBranch, colon, elseBranch) { + _condition = condition + _question = question + _thenBranch = thenBranch + _colon = colon + _elseBranch = elseBranch + } + + condition { _condition } + question { _question } + thenBranch { _thenBranch } + colon { _colon } + elseBranch { _elseBranch } + + accept(visitor) { visitor.visitConditionalExpr(this) } + + toString { + return "Conditional(%(_condition) %(_question) %(_thenBranch) %(_colon) %(_elseBranch))" + } +} + +class NumExpr is Expr { + construct new(value) { + _value = value + } + + value { _value } + + accept(visitor) { visitor.visitNumExpr(this) } + + toString { + return "Num(%(_value))" + } +} + +class SuperExpr is Expr { + construct new(name, arguments, blockArgument) { + _name = name + _arguments = arguments + _blockArgument = blockArgument + } + + name { _name } + arguments { _arguments } + blockArgument { _blockArgument } + + accept(visitor) { visitor.visitSuperExpr(this) } + + toString { + return "Super(%(_name) %(_arguments) %(_blockArgument))" + } +} + +class StringExpr is Expr { + construct new(value) { + _value = value + } + + value { _value } + + accept(visitor) { visitor.visitStringExpr(this) } + + toString { + return "String(%(_value))" + } +} + +class SubscriptExpr is Expr { + construct new(receiver, leftBracket, arguments, rightBracket) { + _receiver = receiver + _leftBracket = leftBracket + _arguments = arguments + _rightBracket = rightBracket + } + + receiver { _receiver } + leftBracket { _leftBracket } + arguments { _arguments } + rightBracket { _rightBracket } + + accept(visitor) { visitor.visitSubscriptExpr(this) } + + toString { + return "Subscript(%(_receiver) %(_leftBracket) %(_arguments) %(_rightBracket))" + } +} + +class BoolExpr is Expr { + construct new(value) { + _value = value + } + + value { _value } + + accept(visitor) { visitor.visitBoolExpr(this) } + + toString { + return "Bool(%(_value))" + } +} + +class InterpolationExpr is Expr { + construct new(strings, expressions) { + _strings = strings + _expressions = expressions + } + + strings { _strings } + expressions { _expressions } + + accept(visitor) { visitor.visitInterpolationExpr(this) } + + toString { + return "Interpolation(%(_strings) %(_expressions))" + } +} + +class ForStmt is Stmt { + construct new(variable, iterator, body) { + _variable = variable + _iterator = iterator + _body = body + } + + variable { _variable } + iterator { _iterator } + body { _body } + + accept(visitor) { visitor.visitForStmt(this) } + + toString { + return "For(%(_variable) %(_iterator) %(_body))" + } +} + +class ReturnStmt is Stmt { + construct new(keyword, value) { + _keyword = keyword + _value = value + } + + keyword { _keyword } + value { _value } + + accept(visitor) { visitor.visitReturnStmt(this) } + + toString { + return "Return(%(_keyword) %(_value))" + } +} + +class BlockStmt is Stmt { + construct new(statements) { + _statements = statements + } + + statements { _statements } + + accept(visitor) { visitor.visitBlockStmt(this) } + + toString { + return "Block(%(_statements))" + } +} + +class VarStmt is Stmt { + construct new(name, initializer) { + _name = name + _initializer = initializer + } + + name { _name } + initializer { _initializer } + + accept(visitor) { visitor.visitVarStmt(this) } + + toString { + return "Var(%(_name) %(_initializer))" + } +} + +class ImportStmt is Stmt { + construct new(path, variables) { + _path = path + _variables = variables + } + + path { _path } + variables { _variables } + + accept(visitor) { visitor.visitImportStmt(this) } + + toString { + return "Import(%(_path) %(_variables))" + } +} + +class IfStmt is Stmt { + construct new(condition, thenBranch, elseBranch) { + _condition = condition + _thenBranch = thenBranch + _elseBranch = elseBranch + } + + condition { _condition } + thenBranch { _thenBranch } + elseBranch { _elseBranch } + + accept(visitor) { visitor.visitIfStmt(this) } + + toString { + return "If(%(_condition) %(_thenBranch) %(_elseBranch))" + } +} + +class BreakStmt is Stmt { + construct new(keyword) { + _keyword = keyword + } + + keyword { _keyword } + + accept(visitor) { visitor.visitBreakStmt(this) } + + toString { + return "Break(%(_keyword))" + } +} + +class WhileStmt is Stmt { + construct new(condition, body) { + _condition = condition + _body = body + } + + condition { _condition } + body { _body } + + accept(visitor) { visitor.visitWhileStmt(this) } + + toString { + return "While(%(_condition) %(_body))" + } +} + +class ClassStmt is Stmt { + construct new(foreignKeyword, name, superclass, methods) { + _foreignKeyword = foreignKeyword + _name = name + _superclass = superclass + _methods = methods + } + + foreignKeyword { _foreignKeyword } + name { _name } + superclass { _superclass } + methods { _methods } + + accept(visitor) { visitor.visitClassStmt(this) } + + toString { + return "Class(%(_foreignKeyword) %(_name) %(_superclass) %(_methods))" + } +} diff --git a/samples/Wren/ex.wren b/samples/Wren/ex.wren deleted file mode 100644 index 35a5773d3f..0000000000 --- a/samples/Wren/ex.wren +++ /dev/null @@ -1,15 +0,0 @@ -System.print("Hello, world!") - -class Wren { - flyTo(city) { - System.print("Flying to %(city)") - } -} - -// I am a comment - -var adjectives = Fiber.new { - ["small", "clean", "fast"].each {|word| Fiber.yield(word) } -} - -while (!adjectives.isDone) System.print(adjectives.call()) diff --git a/samples/Wren/parser.wren b/samples/Wren/parser.wren new file mode 100644 index 0000000000..c1b2bad3fb --- /dev/null +++ b/samples/Wren/parser.wren @@ -0,0 +1,735 @@ +// https://github.com/munificent/wrenalyzer/blob/master/parser.wren +// MIT License + +import "ast" for + AssignmentExpr, + BlockStmt, + Body, + BoolExpr, + BreakStmt, + CallExpr, + ClassStmt, + ConditionalExpr, + FieldExpr, + ForStmt, + GroupingExpr, + IfStmt, + ImportStmt, + InfixExpr, + InterpolationExpr, + ListExpr, + MapEntry, + MapExpr, + Method, + Module, + NullExpr, + NumExpr, + PrefixExpr, + ReturnStmt, + StaticFieldExpr, + StringExpr, + SubscriptExpr, + SuperExpr, + ThisExpr, + VarStmt, + WhileStmt +import "lexer" for Lexer +import "token" for Token + +var EQUALITY_OPERATORS = [ + Token.equalEqual, + Token.bangEqual +] + +var COMPARISON_OPERATORS = [ + Token.less, + Token.lessEqual, + Token.greater, + Token.greaterEqual +] + +var BITWISE_SHIFT_OPERATORS = [ + Token.lessLess, + Token.greaterGreater +] + +var RANGE_OPERATORS = [ + Token.dotDot, + Token.dotDotDot +] + +var TERM_OPERATORS = [ + Token.plus, + Token.minus +] + +var FACTOR_OPERATORS = [ + Token.star, + Token.slash, + Token.percent +] + +var PREFIX_OPERATORS = [ + Token.minus, + Token.bang, + Token.tilde +] + +var INFIX_OPERATORS = [ + Token.pipePipe, + Token.ampAmp, + Token.equalEqual, + Token.bangEqual, + Token.isKeyword, + Token.less, + Token.lessEqual, + Token.greater, + Token.greaterEqual, + Token.pipe, + Token.caret, + Token.amp, + Token.lessLess, + Token.greaterGreater, + Token.dotDot, + Token.dotDotDot, + Token.plus, + Token.minus, + Token.star, + Token.slash, + Token.percent +] + +class Parser { + construct new(lexer, reporter) { + _lexer = lexer + _reporter = reporter + _current = _lexer.readToken() + } + + parseModule() { + ignoreLine() + + var statements = [] + while (peek() != Token.eof) { + statements.add(definition()) + if (!matchLine()) break + } + + consume(Token.eof, "Expect end of input.") + return Module.new(statements) + } + + definition() { + if (match(Token.classKeyword)) { + return finishClass(null) + } + + if (match(Token.foreignKeyword)) { + var foreignKeyword = _previous + consume(Token.classKeyword, "Expect 'class' after 'foreign'.") + return finishClass(foreignKeyword) + } + + if (match(Token.importKeyword)) { + var path = consume(Token.string, "Expect import path.") + var variables + + // Parse the variable list, if there is one. + if (match(Token.forKeyword)) { + ignoreLine() + + variables = [] + while (true) { + variables.add(consume(Token.name, "Expect imported variable name.")) + if (!match(Token.comma)) break + ignoreLine() + } + } + + return ImportStmt.new(path, variables) + } + + if (match(Token.varKeyword)) { + var name = consume(Token.name, "Expect variable name.") + var initializer + if (match(Token.equal)) { + initializer = expression() + } + + return VarStmt.new(name, initializer) + } + + return statement() + } + + // Parses the rest of a class definition after the "class" token. + finishClass(foreignKeyword) { + var name = consume(Token.name, "Expect class name.") + + var superclass + if (match(Token.isKeyword)) { + // TODO: This is different from the VM (which is wrong). Need to make + // sure we don't parse the class body as a block argument. + superclass = consume(Token.name, "Expect name of superclass.") + } + + var methods = [] + consume(Token.leftBrace, "Expect '{' after class name.") + ignoreLine() + + while (!match(Token.rightBrace) && peek() != Token.eof) { + methods.add(method()) + + // Don't require a newline after the last definition. + if (match(Token.rightBrace)) break + + consumeLine("Expect newline after definition in class.") + } + + return ClassStmt.new(foreignKeyword, name, superclass, methods) + } + + method() { + // Note: This parses more permissively than the grammar actually is. For + // example, it will allow "static construct *()". We'll report errors on + // invalid forms later. + var foreignKeyword + if (match(Token.foreignKeyword)) { + foreignKeyword = _previous + } + + var staticKeyword + if (match(Token.staticKeyword)) { + staticKeyword = _previous + } + + var constructKeyword + if (match(Token.constructKeyword)) { + constructKeyword = _previous + } + + // TODO: Error on invalid combinations of above keywords. + + var name + var parameters + + var allowParameters = false + + if (match(Token.leftBracket)) { + // Subscript operator. + parameters = parameterList() + consume(Token.rightBracket, "Expect ']' after parameters.") + allowParameters = false + } else if (matchAny(INFIX_OPERATORS)) { + _previous + allowParameters = true + } else if (matchAny([Token.bang, Token.tilde])) { + allowParameters = false + } else { + consume(Token.name, "Expect method name.") + allowParameters = true + } + name = _previous + + if (match(Token.leftParen)) { + // Parse the parameter list even if not allowed to give better errors + // and have fewer cascaded errors. + if (!allowParameters) { + error("A parameter list is not allowed for this method.") + } + + ignoreLine() + if (!match(Token.rightParen)) { + parameters = parameterList() + ignoreLine() + consume(Token.rightParen, "Expect ')' after parameters.") + } + } + // TODO: Setters. + + var body + if (foreignKeyword == null) { + consume(Token.leftBrace, "Expect '{' before method body.") + body = finishBody(parameters) + } + + return Method.new(foreignKeyword, staticKeyword, constructKeyword, name, body) + } + + statement() { + // Break statement. + if (match(Token.breakKeyword)) { + return BreakStmt.new(_previous) + } + + // If statement. + if (match(Token.ifKeyword)) { + consume(Token.leftParen, "Expect '(' after 'if'.") + ignoreLine() + var condition = expression() + consume(Token.rightParen, "Expect ')' after if condition.") + var thenBranch = statement() + var elseBranch + if (match(Token.elseKeyword)) { + elseBranch = statement() + } + return IfStmt.new(condition, thenBranch, elseBranch) + } + + // For statement. + if (match(Token.forKeyword)) { + consume(Token.leftParen, "Expect '(' after 'for'.") + var variable = consume(Token.name, "Expect for loop variable name.") + consume(Token.inKeyword, "Expect 'in' after loop variable.") + ignoreLine() + var iterator = expression() + consume(Token.rightParen, "Expect ')' after loop expression.") + var body = statement() + return ForStmt.new(variable, iterator, body) + } + + // While statement. + if (match(Token.whileKeyword)) { + consume(Token.leftParen, "Expect '(' after 'while'.") + ignoreLine() + var condition = expression() + consume(Token.rightParen, "Expect ')' after while condition.") + var body = statement() + return WhileStmt.new(condition, body) + } + + // Return statement. + if (match(Token.returnKeyword)) { + var keyword = _previous + var value + if (peek() != Token.line) { + value = expression() + } + + return ReturnStmt.new(keyword, value) + } + + // Block statement. + if (match(Token.leftBrace)) { + var statements = [] + ignoreLine() + + if (!match(Token.rightBrace)) { + while (peek() != Token.eof) { + statements.add(definition()) + consumeLine("Expect newline after statement.") + + if (match(Token.rightBrace)) break + } + } + + return BlockStmt.new(statements) + } + + // Expression statement. + return expression() + } + + // Parses the rest of a method or block argument body. + finishBody(parameters) { + // An empty block. + if (match(Token.rightBrace)) return Body.new(parameters, null, []) + + // If there's no line after the "{", it's a single-expression body. + if (!matchLine()) { + var expr = expression() + ignoreLine() + consume(Token.rightBrace, "Expect '}' at end of block.") + return Body.new(parameters, expr, null) + } + + // Empty blocks (with just a newline inside) do nothing. + if (match(Token.rightBrace)) return Body.new(parameters, null, []) + + var statements = [] + while (peek() != Token.eof) { + statements.add(definition()) + consumeLine("Expect newline after statement.") + + if (match(Token.rightBrace)) break + } + + return Body.new(parameters, null, statements) + } + + expression() { assignment() } + + // assignment: conditional ( "=" assignment )? + assignment() { + // TODO: This allows invalid LHS like "1 + 2 = 3". Decide if we want to + // handle that here or later in the pipeline. + var expr = conditional() + if (!match(Token.equal)) return expr + + var equal = _previous + var value = assignment() + return AssignmentExpr.new(expr, equal, value) + } + + // conditional: logicalOr ( "?" conditional ":" assignment )? + conditional() { + var expr = logicalOr() + if (!match(Token.question)) return expr + + var question = _previous + var thenBranch = conditional() + var colon = consume(Token.colon, + "Expect ':' after then branch of conditional operator.") + var elseBranch = assignment() + return ConditionalExpr.new(expr, question, thenBranch, colon, elseBranch) + } + + // logicalOr: logicalAnd ( "||" logicalAnd )* + logicalOr() { parseInfix([Token.pipePipe]) { logicalAnd() } } + + // logicalAnd: equality ( "&&" equality )* + logicalAnd() { parseInfix([Token.ampAmp]) { equality() } } + + // equality: typeTest ( equalityOperator typeTest )* + // equalityOperator: "==" | "!=" + equality() { parseInfix(EQUALITY_OPERATORS) { typeTest() } } + + // typeTest: comparison ( "is" comparison )* + typeTest() { parseInfix([Token.isKeyword]) { comparison() } } + + // comparison: bitwiseOr ( comparisonOperator bitwiseOr )* + // comparisonOperator: "<" | ">" | "<=" | ">=" + comparison() { parseInfix(COMPARISON_OPERATORS) { bitwiseOr() } } + + // bitwiseOr: bitwiseXor ( "|" bitwiseXor )* + bitwiseOr() { parseInfix([Token.pipe]) { bitwiseXor() } } + + // bitwiseXor: bitwiseAnd ( "^" bitwiseAnd )* + bitwiseXor() { parseInfix([Token.caret]) { bitwiseAnd() } } + + // bitwiseAnd: bitwiseShift ( "&" bitwiseShift )* + bitwiseAnd() { parseInfix([Token.amp]) { bitwiseShift() } } + + // bitwiseShift: range ( bitwiseShiftOperator range )* + // bitwiseShiftOperator: "<<" | ">>" + bitwiseShift() { parseInfix(BITWISE_SHIFT_OPERATORS) { range() } } + + // range: term ( rangeOperator term )* + // rangeOperator: ".." | ".." + range() { parseInfix(RANGE_OPERATORS) { term() } } + + // term: factor ( termOperator factor )* + // termOperator: "+" | "-" + term() { parseInfix(TERM_OPERATORS) { factor() } } + + // factor: prefix ( factorOperator prefix )* + // factorOperator: "*" | "/" | "%" + factor() { parseInfix(FACTOR_OPERATORS) { prefix() } } + + // prefix: ("-" | "!" | "~")* call + prefix() { + if (matchAny(PREFIX_OPERATORS)) { + return PrefixExpr.new(_previous, prefix()) + } + + return call() + } + + // call: primary ( subscript | "." methodCall )* + // subscript: "[" argumentList "]" + call() { + var expr = primary() + + while (true) { + if (match(Token.leftBracket)) { + var leftBracket = _previous + var arguments = argumentList() + var rightBracket = consume(Token.rightBracket, + "Expect ']' after subscript arguments.") + expr = SubscriptExpr.new(expr, leftBracket, arguments, rightBracket) + } else if (match(Token.dot)) { + var name = consume(Token.name, "Expect method name after '.'.") + expr = methodCall(expr, name) + } else { + break + } + } + + return expr + } + + // Parses the argument list for a method call and creates a call expression + // for it. + // + // methodCall: ( "(" argumentList? ")" )? blockArgument? + // blockArgument: "{" ( "|" parameterList "|" )? body "}" + // parameterList: Name ( "," Name )* + // body: + // | "\n" ( definition "\n" )* + // | expression + methodCall(receiver, name) { + var arguments = finishCall() + return CallExpr.new(receiver, name, arguments[0], arguments[1]) + } + + // Parses the argument list for a method call. Returns a list containing the + // argument list (if any) and block argument (if any). If either is missing, + // the list element at that position is `null`. + finishCall() { + var arguments + if (match(Token.leftParen)) { + // Allow an empty argument list. Note that we treat this differently than + // a getter (no argument list). The former will have a `null` argument + // list and the latter will have an empty one. + if (match(Token.rightParen)) { + arguments = [] + } else { + arguments = argumentList() + consume(Token.rightParen, "Expect ')' after arguments.") + } + } + + var blockArgument + if (match(Token.leftBrace)) { + var parameters + if (match(Token.pipe)) { + parameters = parameterList() + consume(Token.pipe, "Expect '|' after block parameters.") + } + + blockArgument = finishBody(parameters) + } + + return [arguments, blockArgument] + } + + // argumentList: expression ( "," expression )* + argumentList() { + var arguments = [] + + ignoreLine() + while (true) { + arguments.add(expression()) + if (!match(Token.comma)) break + ignoreLine() + } + + return arguments + } + + // parameterList: name ( "," name )* + parameterList() { + var parameters = [] + + while (true) { + parameters.add(consume(Token.name, "Expect parameter name.")) + if (!match(Token.comma)) break + ignoreLine() + } + + return parameters + } + + // primary: + // | grouping + // | listLiteral + // | mapLiteral + // | "true" | "false" | "null" | "this" + // | Field | StaticField | Number + primary() { + if (match(Token.leftParen)) return grouping() + if (match(Token.leftBracket)) return listLiteral() + if (match(Token.leftBrace)) return mapLiteral() + if (match(Token.name)) return methodCall(null, _previous) + if (match(Token.superKeyword)) return superCall() + + if (match(Token.falseKeyword)) return BoolExpr.new(_previous) + if (match(Token.trueKeyword)) return BoolExpr.new(_previous) + if (match(Token.nullKeyword)) return NullExpr.new(_previous) + if (match(Token.thisKeyword)) return ThisExpr.new(_previous) + + // TODO: Error if not inside class. + if (match(Token.field)) return FieldExpr.new(_previous) + if (match(Token.staticField)) return StaticFieldExpr.new(_previous) + + if (match(Token.number)) return NumExpr.new(_previous) + if (match(Token.string)) return StringExpr.new(_previous) + + if (peek() == Token.interpolation) return stringInterpolation() + // TODO: Token.super. + + error("Expected expression.") + // Make a fake node so that we don't have to worry about null later. + // TODO: Should this be an error node? + return NullExpr.new(_previous) + } + + // Finishes parsing a parenthesized expression. + // + // grouping: "(" expressions ")" + grouping() { + var leftParen = _previous + var expr = expression() + var rightParen = consume(Token.rightParen, "Expect ')' after expression.") + return GroupingExpr.new(leftParen, expr, rightParen) + } + + // Finishes parsing a list literal. + // + // listLiteral: "[" ( expression ("," expression)* ","? )? "]" + listLiteral() { + var leftBracket = _previous + var elements = [] + + ignoreLine() + + while (peek() != Token.rightBracket) { + elements.add(expression()) + + ignoreLine() + if (!match(Token.comma)) break + ignoreLine() + } + + var rightBracket = consume(Token.rightBracket, + "Expect ']' after list elements.") + return ListExpr.new(leftBracket, elements, rightBracket) + } + + // Finishes parsing a map literal. + // + // mapLiteral: "[" ( mapEntry ("," mapEntry)* ","? )? "}" + // mapEntry: expression ":" expression + mapLiteral() { + var leftBrace = _previous + var entries = [] + + ignoreLine() + + while (peek() != Token.rightBrace) { + var key = expression() + consume(Token.colon, "Expect ':' after map key.") + + var value = expression() + entries.add(MapEntry.new(key, value)) + + ignoreLine() + if (!match(Token.comma)) break + ignoreLine() + } + + var rightBrace = consume(Token.rightBrace, "Expect '}' after map entries.") + return MapExpr.new(leftBrace, entries, rightBrace) + } + + superCall() { + var name + if (match(Token.dot)) { + // It's a named super call. + name = consume(Token.name, "Expect method name after 'super.'.") + } + + var arguments = finishCall() + return SuperExpr.new(name, arguments[0], arguments[1]) + } + + // stringInterpolation: (interpolation expression )? string + stringInterpolation() { + var strings = [] + var expressions = [] + + while (match(Token.interpolation)) { + strings.add(_previous) + expressions.add(expression()) + } + + // This error should never be reported. It's the lexer's job to ensure we + // generate the right token sequence. + strings.add(consume(Token.string, "Expect end of string interpolation.")) + + return InterpolationExpr.new(strings, expressions) + } + + // Utility methods. + + // Parses a left-associative series of infix operator expressions using any + // of [tokenTypes] as operators and calling [parseOperand] to parse the left + // and right operands. + parseInfix(tokenTypes, parseOperand) { + var expr = parseOperand.call() + while (matchAny(tokenTypes)) { + var operator = _previous + ignoreLine() + var right = parseOperand.call() + expr = InfixExpr.new(expr, operator, right) + } + + return expr + } + + // If the next token has [type], consumes and returns it. Otherwise, returns + // `null`. + match(type) { + if (peek() != type) return null + return consume() + } + + // Consumes and returns the next token if its type is contained in the list + // [types]. + matchAny(types) { + for (type in types) { + var result = match(type) + if (result) return result + } + + return null + } + + // Consumes zero or more newlines. Returns `true` if at least one was matched. + matchLine() { + if (!match(Token.line)) return false + while (match(Token.line)) { + // Do nothing. + } + + return true + } + + // Same as [matchLine()], but makes it clear that the intent is to discard + // newlines appearing where this is called. + ignoreLine() { matchLine() } + + // Consumes one or more newlines. + consumeLine(error) { + consume(Token.line, error) + ignoreLine() + } + + // Reads and consumes the next token. + consume() { + peek() + _previous = _current + _current = null + return _previous + } + + // Reads the next token if it is of [type]. Otherwise, discards it and + // reports an error with [message] + consume(type, message) { + var token = consume() + if (token.type != type) error(message) + + return token + } + + // Returns the type of the next token. + peek() { + if (_current == null) _current = _lexer.readToken() + return _current.type + } + + /// Reports an error on the most recent token. + error(message) { + _reporter.error(message, [_current != null ? _current : _previous]) + } +}