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 { // 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() } } 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 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 up { 0x41 } static down { 0x42 } static right { 0x43 } static left { 0x44 } } 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. 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() }