955 lines
25 KiB
Plaintext
955 lines
25 KiB
Plaintext
import "meta" for Meta
|
|
import "io" for Stdin, Stdout
|
|
import "os" for Platform
|
|
|
|
/// Abstract base class for the REPL. Manages the input line and history, but
|
|
/// does not render.
|
|
class Repl {
|
|
construct new() {
|
|
_cursor = 0
|
|
_line = ""
|
|
|
|
_history = []
|
|
_historyIndex = 0
|
|
}
|
|
|
|
cursor { _cursor }
|
|
cursor=(value) { _cursor = value }
|
|
line { _line }
|
|
line=(value) { _line = value }
|
|
|
|
run() {
|
|
Stdin.isRaw = true
|
|
refreshLine(false)
|
|
|
|
while (true) {
|
|
var byte = Stdin.readByte()
|
|
if (handleChar(byte)) break
|
|
refreshLine(true)
|
|
}
|
|
}
|
|
|
|
handleChar(byte) {
|
|
if (byte == Chars.ctrlC) {
|
|
System.print()
|
|
return true
|
|
} else if (byte == Chars.ctrlD) {
|
|
// If the line is empty, Ctrl_D exits.
|
|
if (_line.isEmpty) {
|
|
System.print()
|
|
return true
|
|
}
|
|
|
|
// Otherwise, it deletes the character after the cursor.
|
|
deleteRight()
|
|
} else if (byte == Chars.tab) {
|
|
var completion = getCompletion()
|
|
if (completion != null) {
|
|
_line = _line + completion
|
|
_cursor = _line.count
|
|
}
|
|
} else if (byte == Chars.ctrlU) {
|
|
// Clear the line.
|
|
_line = ""
|
|
_cursor = 0
|
|
} else if (byte == Chars.ctrlN) {
|
|
nextHistory()
|
|
} else if (byte == Chars.ctrlP) {
|
|
previousHistory()
|
|
} else if (byte == Chars.escape) {
|
|
var escapeType = Stdin.readByte()
|
|
var value = Stdin.readByte()
|
|
if (escapeType == Chars.leftBracket) {
|
|
// ESC [ sequence.
|
|
handleEscapeBracket(value)
|
|
} else {
|
|
// TODO: Handle ESC 0 sequences.
|
|
}
|
|
} else if (byte == Chars.carriageReturn) {
|
|
executeInput()
|
|
} else if (byte == Chars.delete) {
|
|
deleteLeft()
|
|
} else if (byte >= Chars.space && byte <= Chars.tilde) {
|
|
insertChar(byte)
|
|
} else if (byte == Chars.ctrlW) { // Handle Ctrl+w
|
|
// Delete trailing spaces
|
|
while (_cursor != 0 && _line[_cursor - 1] == " ") {
|
|
deleteLeft()
|
|
}
|
|
// Delete until the next space
|
|
while (_cursor != 0 && _line[_cursor - 1] != " ") {
|
|
deleteLeft()
|
|
}
|
|
} else {
|
|
// TODO: Other shortcuts?
|
|
System.print("Unhandled key-code [dec]: %(byte)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Inserts the character with [byte] value at the current cursor position.
|
|
insertChar(byte) {
|
|
var char = String.fromCodePoint(byte)
|
|
_line = _line[0..._cursor] + char + _line[_cursor..-1]
|
|
_cursor = _cursor + 1
|
|
}
|
|
|
|
/// Deletes the character before the cursor, if any.
|
|
deleteLeft() {
|
|
if (_cursor == 0) return
|
|
|
|
// Delete the character before the cursor.
|
|
_line = _line[0...(_cursor - 1)] + _line[_cursor..-1]
|
|
_cursor = _cursor - 1
|
|
}
|
|
|
|
/// Deletes the character after the cursor, if any.
|
|
deleteRight() {
|
|
if (_cursor == _line.count) return
|
|
|
|
// Delete the character after the cursor.
|
|
_line = _line[0..._cursor] + _line[(_cursor + 1)..-1]
|
|
}
|
|
|
|
handleEscapeBracket(byte) {
|
|
if (byte == EscapeBracket.up) {
|
|
previousHistory()
|
|
} else if (byte == EscapeBracket.down) {
|
|
nextHistory()
|
|
} else if (byte == EscapeBracket.delete) {
|
|
deleteRight()
|
|
// Consume extra 126 character generated by delete
|
|
Stdin.readByte()
|
|
} else if (byte == EscapeBracket.end) {
|
|
_cursor = _line.count
|
|
} else if (byte == EscapeBracket.home) {
|
|
_cursor = 0
|
|
}
|
|
}
|
|
|
|
previousHistory() {
|
|
if (_historyIndex == 0) return
|
|
|
|
_historyIndex = _historyIndex - 1
|
|
_line = _history[_historyIndex]
|
|
_cursor = _line.count
|
|
}
|
|
|
|
nextHistory() {
|
|
if (_historyIndex >= _history.count) return
|
|
|
|
_historyIndex = _historyIndex + 1
|
|
if (_historyIndex < _history.count) {
|
|
_line = _history[_historyIndex]
|
|
_cursor = _line.count
|
|
} else {
|
|
_line = ""
|
|
_cursor = 0
|
|
}
|
|
}
|
|
|
|
executeInput() {
|
|
// Remove the completion hint.
|
|
refreshLine(false)
|
|
|
|
// Add it to the history (if the line is interesting).
|
|
if (_line != "" && (_history.isEmpty || _history[-1] != _line)) {
|
|
_history.add(_line)
|
|
_historyIndex = _history.count
|
|
}
|
|
|
|
// Reset the current line.
|
|
var input = _line
|
|
_line = ""
|
|
_cursor = 0
|
|
|
|
System.print()
|
|
|
|
// Guess if it looks like a statement or expression. If it looks like an
|
|
// expression, we try to print the result.
|
|
var token = lexFirst(input)
|
|
|
|
// No code, so do nothing.
|
|
if (token == null) return
|
|
|
|
var isStatement =
|
|
token.type == Token.breakKeyword ||
|
|
token.type == Token.classKeyword ||
|
|
token.type == Token.forKeyword ||
|
|
token.type == Token.foreignKeyword ||
|
|
token.type == Token.ifKeyword ||
|
|
token.type == Token.importKeyword ||
|
|
token.type == Token.returnKeyword ||
|
|
token.type == Token.varKeyword ||
|
|
token.type == Token.whileKeyword
|
|
|
|
var closure
|
|
if (isStatement) {
|
|
closure = Meta.compile(input)
|
|
} else {
|
|
closure = Meta.compileExpression(input)
|
|
}
|
|
|
|
// Stop if there was a compile error.
|
|
if (closure == null) return
|
|
|
|
var fiber = Fiber.new(closure)
|
|
|
|
var result = fiber.try()
|
|
if (fiber.error != null) {
|
|
// TODO: Include callstack.
|
|
showRuntimeError("Runtime error: %(fiber.error)")
|
|
return
|
|
}
|
|
|
|
if (!isStatement) {
|
|
showResult(result)
|
|
}
|
|
}
|
|
|
|
lex(line, includeWhitespace) {
|
|
var lexer = Lexer.new(line)
|
|
var tokens = []
|
|
while (true) {
|
|
var token = lexer.readToken()
|
|
if (token.type == Token.eof) break
|
|
|
|
if (includeWhitespace ||
|
|
(token.type != Token.comment && token.type != Token.whitespace)) {
|
|
tokens.add(token)
|
|
}
|
|
}
|
|
|
|
return tokens
|
|
}
|
|
|
|
lexFirst(line) {
|
|
var lexer = Lexer.new(line)
|
|
while (true) {
|
|
var token = lexer.readToken()
|
|
if (token.type == Token.eof) return null
|
|
|
|
if (token.type != Token.comment && token.type != Token.whitespace) {
|
|
return token
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets the best possible auto-completion for the current line, or null if
|
|
/// there is none. The completion is the remaining string to append to the
|
|
/// line, not the entire completed line.
|
|
getCompletion() {
|
|
if (_line.isEmpty) return null
|
|
|
|
// Only complete if the cursor is at the end.
|
|
if (_cursor != _line.count) return null
|
|
|
|
for (name in Meta.getModuleVariables("repl")) {
|
|
// TODO: Also allow completion if the line ends with an identifier but
|
|
// has other stuff before it.
|
|
if (name.startsWith(_line)) {
|
|
return name[_line.count..-1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A reduced functionality REPL that doesn't use ANSI escape sequences.
|
|
class SimpleRepl is Repl {
|
|
construct new() {
|
|
super()
|
|
_erase = ""
|
|
}
|
|
|
|
refreshLine(showCompletion) {
|
|
// A carriage return just moves the cursor to the beginning of the line.
|
|
// We have to erase it manually. Since we can't use ANSI escapes, and we
|
|
// don't know how wide the terminal is, erase the longest line we've seen
|
|
// so far.
|
|
if (line.count > _erase.count) _erase = " " * line.count
|
|
System.write("\r %(_erase)")
|
|
|
|
// Show the prompt at the beginning of the line.
|
|
System.write("\r> ")
|
|
|
|
// Write the line.
|
|
System.write(line)
|
|
Stdout.flush()
|
|
}
|
|
|
|
showResult(value) {
|
|
// TODO: Syntax color based on type? It might be nice to distinguish
|
|
// between string results versus stringified results. Otherwise, the
|
|
// user can't tell the difference between `true` and "true".
|
|
System.print(value)
|
|
}
|
|
|
|
showRuntimeError(message) {
|
|
System.print(message)
|
|
}
|
|
}
|
|
|
|
class AnsiRepl is Repl {
|
|
construct new() {
|
|
super()
|
|
}
|
|
|
|
handleChar(byte) {
|
|
if (byte == Chars.ctrlA) {
|
|
cursor = 0
|
|
} else if (byte == Chars.ctrlB) {
|
|
cursorLeft()
|
|
} else if (byte == Chars.ctrlE) {
|
|
cursor = line.count
|
|
} else if (byte == Chars.ctrlF) {
|
|
cursorRight()
|
|
} else if (byte == Chars.ctrlK) {
|
|
// Delete everything after the cursor.
|
|
line = line[0...cursor]
|
|
} else if (byte == Chars.ctrlL) {
|
|
// Clear the screen.
|
|
System.write("\x1b[2J")
|
|
// Move cursor to top left.
|
|
System.write("\x1b[H")
|
|
} else {
|
|
// TODO: Ctrl-T to swap chars.
|
|
// TODO: ESC H and F to move to beginning and end of line. (Both ESC
|
|
// [ and ESC 0 sequences?)
|
|
// TODO: Ctrl-W delete previous word.
|
|
return super.handleChar(byte)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
handleEscapeBracket(byte) {
|
|
if (byte == EscapeBracket.left) {
|
|
cursorLeft()
|
|
} else if (byte == EscapeBracket.right) {
|
|
cursorRight()
|
|
}
|
|
|
|
super.handleEscapeBracket(byte)
|
|
}
|
|
|
|
/// Move the cursor left one character.
|
|
cursorLeft() {
|
|
if (cursor > 0) cursor = cursor - 1
|
|
}
|
|
|
|
/// Move the cursor right one character.
|
|
cursorRight() {
|
|
// TODO: Take into account multi-byte characters?
|
|
if (cursor < line.count) cursor = cursor + 1
|
|
}
|
|
|
|
refreshLine(showCompletion) {
|
|
// Erase the whole line.
|
|
System.write("\x1b[2K")
|
|
|
|
// Show the prompt at the beginning of the line.
|
|
System.write(Color.gray)
|
|
System.write("\r> ")
|
|
System.write(Color.none)
|
|
|
|
// Syntax highlight the line.
|
|
for (token in lex(line, true)) {
|
|
if (token.type == Token.eof) break
|
|
|
|
System.write(TOKEN_COLORS[token.type])
|
|
System.write(token.text)
|
|
System.write(Color.none)
|
|
}
|
|
|
|
if (showCompletion) {
|
|
var completion = getCompletion()
|
|
if (completion != null) {
|
|
System.write("%(Color.gray)%(completion)%(Color.none)")
|
|
}
|
|
}
|
|
|
|
// Position the cursor.
|
|
System.write("\r\x1b[%(2 + cursor)C")
|
|
Stdout.flush()
|
|
}
|
|
|
|
showResult(value) {
|
|
// TODO: Syntax color based on type? It might be nice to distinguish
|
|
// between string results versus stringified results. Otherwise, the
|
|
// user can't tell the difference between `true` and "true".
|
|
System.print("%(Color.brightWhite)%(value)%(Color.none)")
|
|
}
|
|
|
|
showRuntimeError(message) {
|
|
System.print("%(Color.red)%(message)%(Color.none)")
|
|
// TODO: Print entire stack.
|
|
}
|
|
}
|
|
|
|
/// ANSI color escape sequences.
|
|
class Color {
|
|
static none { "\x1b[0m" }
|
|
static black { "\x1b[30m" }
|
|
static red { "\x1b[31m" }
|
|
static green { "\x1b[32m" }
|
|
static yellow { "\x1b[33m" }
|
|
static blue { "\x1b[34m" }
|
|
static magenta { "\x1b[35m" }
|
|
static cyan { "\x1b[36m" }
|
|
static white { "\x1b[37m" }
|
|
|
|
static gray { "\x1b[30;1m" }
|
|
static pink { "\x1b[31;1m" }
|
|
static brightWhite { "\x1b[37;1m" }
|
|
}
|
|
|
|
/// Utilities for working with characters.
|
|
class Chars {
|
|
static ctrlA { 0x01 }
|
|
static ctrlB { 0x02 }
|
|
static ctrlC { 0x03 }
|
|
static ctrlD { 0x04 }
|
|
static ctrlE { 0x05 }
|
|
static ctrlF { 0x06 }
|
|
static tab { 0x09 }
|
|
static lineFeed { 0x0a }
|
|
static ctrlK { 0x0b }
|
|
static ctrlL { 0x0c }
|
|
static carriageReturn { 0x0d }
|
|
static ctrlN { 0x0e }
|
|
static ctrlP { 0x10 }
|
|
static ctrlU { 0x15 }
|
|
static ctrlW { 0x17 }
|
|
static escape { 0x1b }
|
|
static space { 0x20 }
|
|
static bang { 0x21 }
|
|
static quote { 0x22 }
|
|
static percent { 0x25 }
|
|
static amp { 0x26 }
|
|
static leftParen { 0x28 }
|
|
static rightParen { 0x29 }
|
|
static star { 0x2a }
|
|
static plus { 0x2b }
|
|
static comma { 0x2c }
|
|
static minus { 0x2d }
|
|
static dot { 0x2e }
|
|
static slash { 0x2f }
|
|
|
|
static zero { 0x30 }
|
|
static nine { 0x39 }
|
|
|
|
static colon { 0x3a }
|
|
static less { 0x3c }
|
|
static equal { 0x3d }
|
|
static greater { 0x3e }
|
|
static question { 0x3f }
|
|
|
|
static upperA { 0x41 }
|
|
static upperF { 0x46 }
|
|
static upperZ { 0x5a }
|
|
|
|
static leftBracket { 0x5b }
|
|
static backslash { 0x5c }
|
|
static rightBracket { 0x5d }
|
|
static caret { 0x5e }
|
|
static underscore { 0x5f }
|
|
|
|
static lowerA { 0x61 }
|
|
static lowerF { 0x66 }
|
|
static lowerX { 0x78 }
|
|
static lowerZ { 0x7a }
|
|
|
|
static leftBrace { 0x7b }
|
|
static pipe { 0x7c }
|
|
static rightBrace { 0x7d }
|
|
static tilde { 0x7e }
|
|
static delete { 0x7f }
|
|
|
|
static isAlpha(c) {
|
|
return c >= lowerA && c <= lowerZ ||
|
|
c >= upperA && c <= upperZ ||
|
|
c == underscore
|
|
}
|
|
|
|
static isDigit(c) { c >= zero && c <= nine }
|
|
|
|
static isAlphaNumeric(c) { isAlpha(c) || isDigit(c) }
|
|
|
|
static isHexDigit(c) {
|
|
return c >= zero && c <= nine ||
|
|
c >= lowerA && c <= lowerF ||
|
|
c >= upperA && c <= upperF
|
|
}
|
|
|
|
static isLowerAlpha(c) { c >= lowerA && c <= lowerZ }
|
|
|
|
static isWhitespace(c) { c == space || c == tab || c == carriageReturn }
|
|
}
|
|
|
|
class EscapeBracket {
|
|
static delete { 0x33 }
|
|
static up { 0x41 }
|
|
static down { 0x42 }
|
|
static right { 0x43 }
|
|
static left { 0x44 }
|
|
static end { 0x46 }
|
|
static home { 0x48 }
|
|
}
|
|
|
|
class Token {
|
|
// Punctuators.
|
|
static leftParen { "leftParen" }
|
|
static rightParen { "rightParen" }
|
|
static leftBracket { "leftBracket" }
|
|
static rightBracket { "rightBracket" }
|
|
static leftBrace { "leftBrace" }
|
|
static rightBrace { "rightBrace" }
|
|
static colon { "colon" }
|
|
static dot { "dot" }
|
|
static dotDot { "dotDot" }
|
|
static dotDotDot { "dotDotDot" }
|
|
static comma { "comma" }
|
|
static star { "star" }
|
|
static slash { "slash" }
|
|
static percent { "percent" }
|
|
static plus { "plus" }
|
|
static minus { "minus" }
|
|
static pipe { "pipe" }
|
|
static pipePipe { "pipePipe" }
|
|
static caret { "caret" }
|
|
static amp { "amp" }
|
|
static ampAmp { "ampAmp" }
|
|
static question { "question" }
|
|
static bang { "bang" }
|
|
static tilde { "tilde" }
|
|
static equal { "equal" }
|
|
static less { "less" }
|
|
static lessEqual { "lessEqual" }
|
|
static lessLess { "lessLess" }
|
|
static greater { "greater" }
|
|
static greaterEqual { "greaterEqual" }
|
|
static greaterGreater { "greaterGreater" }
|
|
static equalEqual { "equalEqual" }
|
|
static bangEqual { "bangEqual" }
|
|
|
|
// Keywords.
|
|
static breakKeyword { "break" }
|
|
static classKeyword { "class" }
|
|
static constructKeyword { "construct" }
|
|
static elseKeyword { "else" }
|
|
static falseKeyword { "false" }
|
|
static forKeyword { "for" }
|
|
static foreignKeyword { "foreign" }
|
|
static ifKeyword { "if" }
|
|
static importKeyword { "import" }
|
|
static inKeyword { "in" }
|
|
static isKeyword { "is" }
|
|
static nullKeyword { "null" }
|
|
static returnKeyword { "return" }
|
|
static staticKeyword { "static" }
|
|
static superKeyword { "super" }
|
|
static thisKeyword { "this" }
|
|
static trueKeyword { "true" }
|
|
static varKeyword { "var" }
|
|
static whileKeyword { "while" }
|
|
|
|
static field { "field" }
|
|
static name { "name" }
|
|
static number { "number" }
|
|
static string { "string" }
|
|
static interpolation { "interpolation" }
|
|
static comment { "comment" }
|
|
static whitespace { "whitespace" }
|
|
static line { "line" }
|
|
static error { "error" }
|
|
static eof { "eof" }
|
|
|
|
construct new(source, type, start, length) {
|
|
_source = source
|
|
_type = type
|
|
_start = start
|
|
_length = length
|
|
}
|
|
|
|
type { _type }
|
|
text { _source[_start...(_start + _length)] }
|
|
|
|
start { _start }
|
|
length { _length }
|
|
|
|
toString { text }
|
|
}
|
|
|
|
var KEYWORDS = {
|
|
"break": Token.breakKeyword,
|
|
"class": Token.classKeyword,
|
|
"construct": Token.constructKeyword,
|
|
"else": Token.elseKeyword,
|
|
"false": Token.falseKeyword,
|
|
"for": Token.forKeyword,
|
|
"foreign": Token.foreignKeyword,
|
|
"if": Token.ifKeyword,
|
|
"import": Token.importKeyword,
|
|
"in": Token.inKeyword,
|
|
"is": Token.isKeyword,
|
|
"null": Token.nullKeyword,
|
|
"return": Token.returnKeyword,
|
|
"static": Token.staticKeyword,
|
|
"super": Token.superKeyword,
|
|
"this": Token.thisKeyword,
|
|
"true": Token.trueKeyword,
|
|
"var": Token.varKeyword,
|
|
"while": Token.whileKeyword
|
|
}
|
|
|
|
var TOKEN_COLORS = {
|
|
Token.leftParen: Color.gray,
|
|
Token.rightParen: Color.gray,
|
|
Token.leftBracket: Color.gray,
|
|
Token.rightBracket: Color.gray,
|
|
Token.leftBrace: Color.gray,
|
|
Token.rightBrace: Color.gray,
|
|
Token.colon: Color.gray,
|
|
Token.dot: Color.gray,
|
|
Token.dotDot: Color.none,
|
|
Token.dotDotDot: Color.none,
|
|
Token.comma: Color.gray,
|
|
Token.star: Color.none,
|
|
Token.slash: Color.none,
|
|
Token.percent: Color.none,
|
|
Token.plus: Color.none,
|
|
Token.minus: Color.none,
|
|
Token.pipe: Color.none,
|
|
Token.pipePipe: Color.none,
|
|
Token.caret: Color.none,
|
|
Token.amp: Color.none,
|
|
Token.ampAmp: Color.none,
|
|
Token.question: Color.none,
|
|
Token.bang: Color.none,
|
|
Token.tilde: Color.none,
|
|
Token.equal: Color.none,
|
|
Token.less: Color.none,
|
|
Token.lessEqual: Color.none,
|
|
Token.lessLess: Color.none,
|
|
Token.greater: Color.none,
|
|
Token.greaterEqual: Color.none,
|
|
Token.greaterGreater: Color.none,
|
|
Token.equalEqual: Color.none,
|
|
Token.bangEqual: Color.none,
|
|
|
|
// Keywords.
|
|
Token.breakKeyword: Color.cyan,
|
|
Token.classKeyword: Color.cyan,
|
|
Token.constructKeyword: Color.cyan,
|
|
Token.elseKeyword: Color.cyan,
|
|
Token.falseKeyword: Color.cyan,
|
|
Token.forKeyword: Color.cyan,
|
|
Token.foreignKeyword: Color.cyan,
|
|
Token.ifKeyword: Color.cyan,
|
|
Token.importKeyword: Color.cyan,
|
|
Token.inKeyword: Color.cyan,
|
|
Token.isKeyword: Color.cyan,
|
|
Token.nullKeyword: Color.cyan,
|
|
Token.returnKeyword: Color.cyan,
|
|
Token.staticKeyword: Color.cyan,
|
|
Token.superKeyword: Color.cyan,
|
|
Token.thisKeyword: Color.cyan,
|
|
Token.trueKeyword: Color.cyan,
|
|
Token.varKeyword: Color.cyan,
|
|
Token.whileKeyword: Color.cyan,
|
|
|
|
Token.field: Color.none,
|
|
Token.name: Color.none,
|
|
Token.number: Color.magenta,
|
|
Token.string: Color.yellow,
|
|
Token.interpolation: Color.yellow,
|
|
Token.comment: Color.gray,
|
|
Token.whitespace: Color.none,
|
|
Token.line: Color.none,
|
|
Token.error: Color.red,
|
|
Token.eof: Color.none,
|
|
}
|
|
|
|
// Data table for tokens that are tokenized using maximal munch.
|
|
//
|
|
// The key is the character that starts the token or tokens. After that is a
|
|
// list of token types and characters. As long as the next character is matched,
|
|
// the type will update to the type after that character.
|
|
var PUNCTUATORS = {
|
|
Chars.leftParen: [Token.leftParen],
|
|
Chars.rightParen: [Token.rightParen],
|
|
Chars.leftBracket: [Token.leftBracket],
|
|
Chars.rightBracket: [Token.rightBracket],
|
|
Chars.leftBrace: [Token.leftBrace],
|
|
Chars.rightBrace: [Token.rightBrace],
|
|
Chars.colon: [Token.colon],
|
|
Chars.comma: [Token.comma],
|
|
Chars.star: [Token.star],
|
|
Chars.percent: [Token.percent],
|
|
Chars.plus: [Token.plus],
|
|
Chars.minus: [Token.minus],
|
|
Chars.tilde: [Token.tilde],
|
|
Chars.caret: [Token.caret],
|
|
Chars.question: [Token.question],
|
|
Chars.lineFeed: [Token.line],
|
|
|
|
Chars.pipe: [Token.pipe, Chars.pipe, Token.pipePipe],
|
|
Chars.amp: [Token.amp, Chars.amp, Token.ampAmp],
|
|
Chars.bang: [Token.bang, Chars.equal, Token.bangEqual],
|
|
Chars.equal: [Token.equal, Chars.equal, Token.equalEqual],
|
|
|
|
Chars.dot: [Token.dot, Chars.dot, Token.dotDot, Chars.dot, Token.dotDotDot]
|
|
}
|
|
|
|
/// Tokenizes a string of input. This lexer differs from most in that it
|
|
/// silently ignores errors from incomplete input, like a string literal with
|
|
/// no closing quote. That's because this is intended to be run on a line of
|
|
/// input while the user is still typing it.
|
|
class Lexer {
|
|
construct new(source) {
|
|
_source = source
|
|
|
|
// Due to the magic of UTF-8, we can safely treat Wren source as a series
|
|
// of bytes, since the only code points that are meaningful to Wren fit in
|
|
// ASCII. The only place where non-ASCII code points can occur is inside
|
|
// string literals and comments and the lexer safely treats those as opaque
|
|
// bytes.
|
|
_bytes = source.bytes
|
|
|
|
_start = 0
|
|
_current = 0
|
|
|
|
// The stack of ongoing interpolated strings. Each element in the list is
|
|
// a single level of interpolation nesting. The value of the element is the
|
|
// number of unbalanced "(" still remaining to be closed.
|
|
_interpolations = []
|
|
}
|
|
|
|
readToken() {
|
|
if (_current >= _bytes.count) return makeToken(Token.eof)
|
|
|
|
_start = _current
|
|
var c = _bytes[_current]
|
|
advance()
|
|
|
|
if (!_interpolations.isEmpty) {
|
|
if (c == Chars.leftParen) {
|
|
_interpolations[-1] = _interpolations[-1] + 1
|
|
} else if (c == Chars.rightParen) {
|
|
_interpolations[-1] = _interpolations[-1] - 1
|
|
|
|
// The last ")" in an interpolated expression ends the expression and
|
|
// resumes the string.
|
|
if (_interpolations[-1] == 0) {
|
|
// This is the final ")", so the interpolation expression has ended.
|
|
// This ")" now begins the next section of the template string.
|
|
_interpolations.removeAt(-1)
|
|
return readString()
|
|
}
|
|
}
|
|
}
|
|
|
|
if (PUNCTUATORS.containsKey(c)) {
|
|
var punctuator = PUNCTUATORS[c]
|
|
var type = punctuator[0]
|
|
var i = 1
|
|
while (i < punctuator.count) {
|
|
if (!match(punctuator[i])) break
|
|
type = punctuator[i + 1]
|
|
i = i + 2
|
|
}
|
|
|
|
return makeToken(type)
|
|
}
|
|
|
|
// Handle "<", "<<", and "<=".
|
|
if (c == Chars.less) {
|
|
if (match(Chars.less)) return makeToken(Token.lessLess)
|
|
if (match(Chars.equal)) return makeToken(Token.lessEqual)
|
|
return makeToken(Token.less)
|
|
}
|
|
|
|
// Handle ">", ">>", and ">=".
|
|
if (c == Chars.greater) {
|
|
if (match(Chars.greater)) return makeToken(Token.greaterGreater)
|
|
if (match(Chars.equal)) return makeToken(Token.greaterEqual)
|
|
return makeToken(Token.greater)
|
|
}
|
|
|
|
// Handle "/", "//", and "/*".
|
|
if (c == Chars.slash) {
|
|
if (match(Chars.slash)) return readLineComment()
|
|
if (match(Chars.star)) return readBlockComment()
|
|
return makeToken(Token.slash)
|
|
}
|
|
|
|
if (c == Chars.underscore) return readField()
|
|
if (c == Chars.quote) return readString()
|
|
|
|
if (c == Chars.zero && peek() == Chars.lowerX) return readHexNumber()
|
|
if (Chars.isWhitespace(c)) return readWhitespace()
|
|
if (Chars.isDigit(c)) return readNumber()
|
|
if (Chars.isAlpha(c)) return readName()
|
|
|
|
return makeToken(Token.error)
|
|
}
|
|
|
|
// Reads a line comment until the end of the line is reached.
|
|
readLineComment() {
|
|
// A line comment stops at the newline since newlines are significant.
|
|
while (peek() != Chars.lineFeed && !isAtEnd) {
|
|
advance()
|
|
}
|
|
|
|
return makeToken(Token.comment)
|
|
}
|
|
|
|
readBlockComment() {
|
|
// Block comments can nest.
|
|
var nesting = 1
|
|
while (nesting > 0) {
|
|
// TODO: Report error.
|
|
if (isAtEnd) break
|
|
|
|
if (peek() == Chars.slash && peek(1) == Chars.star) {
|
|
advance()
|
|
advance()
|
|
nesting = nesting + 1
|
|
} else if (peek() == Chars.star && peek(1) == Chars.slash) {
|
|
advance()
|
|
advance()
|
|
nesting = nesting - 1
|
|
if (nesting == 0) break
|
|
} else {
|
|
advance()
|
|
}
|
|
}
|
|
|
|
return makeToken(Token.comment)
|
|
}
|
|
|
|
// Reads a static or instance field.
|
|
readField() {
|
|
var type = Token.field
|
|
|
|
// Read the rest of the name.
|
|
while (match {|c| Chars.isAlphaNumeric(c) }) {}
|
|
|
|
return makeToken(type)
|
|
}
|
|
|
|
// Reads a string literal.
|
|
readString() {
|
|
var type = Token.string
|
|
|
|
while (!isAtEnd) {
|
|
var c = _bytes[_current]
|
|
advance()
|
|
|
|
if (c == Chars.backslash) {
|
|
// TODO: Process specific escapes and validate them.
|
|
if (!isAtEnd) advance()
|
|
} else if (c == Chars.percent) {
|
|
// Consume the '('.
|
|
if (!isAtEnd) advance()
|
|
// TODO: Handle missing '('.
|
|
_interpolations.add(1)
|
|
type = Token.interpolation
|
|
break
|
|
} else if (c == Chars.quote) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return makeToken(type)
|
|
}
|
|
|
|
// Reads a number literal.
|
|
readHexNumber() {
|
|
// Skip past the `x`.
|
|
advance()
|
|
|
|
// Read the rest of the number.
|
|
while (match {|c| Chars.isHexDigit(c) }) {}
|
|
return makeToken(Token.number)
|
|
}
|
|
|
|
// Reads a series of whitespace characters.
|
|
readWhitespace() {
|
|
// Read the rest of the whitespace.
|
|
while (match {|c| Chars.isWhitespace(c) }) {}
|
|
|
|
return makeToken(Token.whitespace)
|
|
}
|
|
|
|
// Reads a number literal.
|
|
readNumber() {
|
|
// Read the rest of the number.
|
|
while (match {|c| Chars.isDigit(c) }) {}
|
|
|
|
// TODO: Floating point, scientific.
|
|
return makeToken(Token.number)
|
|
}
|
|
|
|
// Reads an identifier or keyword token.
|
|
readName() {
|
|
// Read the rest of the name.
|
|
while (match {|c| Chars.isAlphaNumeric(c) }) {}
|
|
|
|
var text = _source[_start..._current]
|
|
var type = Token.name
|
|
if (KEYWORDS.containsKey(text)) {
|
|
type = KEYWORDS[text]
|
|
}
|
|
|
|
return Token.new(_source, type, _start, _current - _start)
|
|
}
|
|
|
|
// Returns `true` if we have scanned all characters.
|
|
isAtEnd { _current >= _bytes.count }
|
|
|
|
// Advances past the current character.
|
|
advance() {
|
|
_current = _current + 1
|
|
}
|
|
|
|
// Returns the byte value of the current character.
|
|
peek() { peek(0) }
|
|
|
|
// Returns the byte value of the character [n] bytes past the current
|
|
// character.
|
|
peek(n) {
|
|
if (_current + n >= _bytes.count) return -1
|
|
return _bytes[_current + n]
|
|
}
|
|
|
|
// Consumes the current character if it matches [condition], which can be a
|
|
// numeric code point value or a function that takes a code point and returns
|
|
// `true` if the code point matches.
|
|
match(condition) {
|
|
if (isAtEnd) return false
|
|
|
|
var c = _bytes[_current]
|
|
if (condition is Fn) {
|
|
if (!condition.call(c)) return false
|
|
} else if (c != condition) {
|
|
return false
|
|
}
|
|
|
|
advance()
|
|
return true
|
|
}
|
|
|
|
// Creates a token of [type] from the current character range.
|
|
makeToken(type) { Token.new(_source, type, _start, _current - _start) }
|
|
}
|
|
|
|
// Fire up the REPL. We use ANSI when talking to a POSIX TTY.
|
|
if (Platform.isPosix && Stdin.isTerminal) {
|
|
AnsiRepl.new().run()
|
|
} else {
|
|
// ANSI escape sequences probably aren't supported, so degrade.
|
|
SimpleRepl.new().run()
|
|
}
|