diff --git a/lib/ace/mode/lua.js b/lib/ace/mode/lua.js index 47325600..2ef9031a 100644 --- a/lib/ace/mode/lua.js +++ b/lib/ace/mode/lua.js @@ -37,6 +37,7 @@ var Tokenizer = require("../tokenizer").Tokenizer; var LuaHighlightRules = require("./lua_highlight_rules").LuaHighlightRules; var LuaFoldMode = require("./folding/lua").FoldMode; var Range = require("../range").Range; +var WorkerClient = require("../worker/worker_client").WorkerClient; var Mode = function() { this.$tokenizer = new Tokenizer(new LuaHighlightRules().getRules()); @@ -143,6 +144,21 @@ oop.inherits(Mode, TextMode); session.outdentRows(new Range(row, 0, row + 2, 0)); }; + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "ace/mode/lua_worker", "Worker"); + worker.attachToDocument(session.getDocument()); + + worker.on("error", function(e) { + session.setAnnotations([e.data]); + }); + + worker.on("ok", function(e) { + session.clearAnnotations(); + }); + + return worker; + }; + }).call(Mode.prototype); exports.Mode = Mode; diff --git a/lib/ace/mode/lua/luaparse.js b/lib/ace/mode/lua/luaparse.js new file mode 100644 index 00000000..c1c780da --- /dev/null +++ b/lib/ace/mode/lua/luaparse.js @@ -0,0 +1,1680 @@ +define(function(require, exports, module) { +/*global exports:true require:true define:true console:true */ + +(function (root, name, factory) { + 'use strict'; + + if (typeof exports !== 'undefined') { + factory(exports); + } else if (typeof define === 'function' && define.amd) { + define(['exports'], factory); + } else { + factory((root[name] = {})); + } +}(this, 'luaparse', function (exports) { + 'use strict'; + + exports.version = '0.0.1'; + + var input, options, length; + + var defaultOptions = exports.defaultOptions = { + // Explicitly tell the parser when the input ends. + wait: false + // Store comments as an array in the chunk object. + , comments: true + }; + + // The available tokens expressed as enum flags so they can be checked with + // bitwise operations. + + var EOF = 1, StringLiteral = 2, Keyword = 4, Identifier = 8 + , NumericLiteral = 16, Punctuator = 32, BooleanLiteral = 64 + , NilLiteral = 128; + + // As this parser is a bit different from luas own, the error messages + // will be different in some situations. + + var errors = exports.errors = { + unexpected: 'Unexpected %1 \'%2\' near \'%3\'' + , expected: '\'%1\' expected near \'%2\'' + , expectedToken: '%1 expected near \'%2\'' + , unfinishedString: 'unfinished string near \'%1\'' + , malformedNumber: 'malformed number near \'%1\'' + }; + + // ### Abstract Syntax Tree + // + // The default AST structure is inspired by the Mozilla Parser API but can + // easily be customized by overriding these functions. + + var ast = exports.ast = { + labelStatement: function(label) { + return { + type: 'LabelStatement' + , label: label + }; + } + + , breakStatement: function() { + return { + type: 'BreakStatement' + }; + } + + , gotoStatement: function(label) { + return { + type: 'GotoStatement' + , label: label + }; + } + + , returnStatement: function(args) { + return { + type: 'ReturnStatement' + , 'arguments': args + }; + } + + , ifStatement: function(clauses) { + return { + type: 'IfStatement' + , clauses: clauses + }; + } + , elseifClause: function(condition, body) { + return { + type: 'ElseifClause' + , condition: condition + , body: body + }; + } + , elseClause: function(body) { + return { + type: 'ElseClause' + , body: body + }; + } + + , whileStatement: function(condition, body) { + return { + type: 'WhileStatement' + , condition: condition + , body: body + }; + } + + , doStatement: function(body) { + return { + type: 'DoStatement' + , body: body + }; + } + + , repeatStatement: function(condition, body) { + return { + type: 'RepeatStatement' + , condition: condition + , body: body + }; + } + + , localStatement: function(variables, init) { + return { + type: 'LocalStatement' + , variables: variables + , init: init + }; + } + + , assignmentStatement: function(variables, init) { + return { + type: 'AssignmentStatement' + , variables: variables + , init: init + }; + } + + , callStatement: function(expression) { + return { + type: 'CallStatement' + , expression: expression + }; + } + + , functionStatement: function(identifier, parameters, isVararg, isLocal, body) { + return { + type: 'FunctionDeclaration' + , identifier: identifier + , vararg: isVararg + , local: isLocal + , parameters: parameters + , body: body + }; + } + + , forNumericStatement: function(variable, start, end, step, body) { + return { + type: 'ForNumericStatement' + , variable: variable + , start: start + , end: end + , step: step + , body: body + }; + } + + , forGenericStatement: function(variables, iterators, body) { + return { + type: 'ForGenericStatement' + , variables: variables + , iterators: iterators + , body: body + }; + } + + , chunk: function(body) { + return { + type: 'Chunk' + , body: body + }; + } + + , identifier: function(name) { + return { + type: 'Identifier' + , name: name + }; + } + + , literal: function(value, raw) { + return { + type: 'Literal' + , value: value + , raw: raw + }; + } + , varargLiteral: function() { + return { + type: 'VarargLiteral' + }; + } + + , tableKey: function(key, value) { + return { + type: 'TableKey' + , key: key + , value: value + }; + } + , tableKeyString: function(key, value) { + return { + type: 'TableKeyString' + , key: key + , value: value + }; + } + , tableValue: function(value) { + return { + type: 'TableValue' + , value: value + }; + } + + + , tableConstructorExpression: function(fields) { + return { + type: 'TableConstructorExpression' + , fields: fields + }; + } + , binaryExpression: function(operator, left, right) { + var type = ('and' === operator || 'or' === operator) ? + 'LogicalExpression' : + 'BinaryExpression'; + + return { + type: type + , operator: operator + , left: left + , right: right + }; + } + , unaryExpression: function(operator, argument) { + return { + type: 'UnaryExpression' + , operator: operator + , argument: argument + }; + } + , memberExpression: function(base, indexer, identifier) { + return { + type: 'MemberExpression' + , indexer: indexer + , identifier: identifier + , base: base + }; + } + + , indexExpression: function(base, index) { + return { + type: 'IndexExpression' + , base: base + , index: index + }; + } + + , callExpression: function(base, args) { + return { + type: 'CallExpression' + , base: base + , 'arguments': args + }; + } + + , tableCallExpression: function(base, args) { + return { + type: 'TableCallExpression' + , base: base + , 'arguments': args + }; + } + + , stringCallExpression: function(base, argument) { + return { + type: 'StringCallExpression' + , base: base + , argument: argument + }; + } + }; + + // Helpers + // ------- + + var slice = Array.prototype.slice + , toString = Object.prototype.toString; + + // A sprintf implementation using %index (beginning at 1) to input + // arguments in the format string. + // + // Example: + // + // // Unexpected function in token + // sprintf('Unexpected %2 in %1.', 'token', 'function'); + + function sprintf(format) { + var args = slice.call(arguments, 1); + format = format.replace(/%(\d)/g, function (match, index) { + match = ''; // jshint + return '' + args[index - 1] || ''; + }); + return format; + } + + // Returns a new object with the properties from all objectes passed as + // arguments. Last argument takes precedence. + // + // Example: + // + // this.options = extend(options, { output: false }); + + function extend() { + var args = slice.call(arguments) + , dest = {} + , src, prop; + + for (var i = 0, l = args.length; i < l; i++) { + src = args[i]; + for (prop in src) if (src.hasOwnProperty(prop)) { + dest[prop] = src[prop]; + } + } + return dest; + } + + // ### Error functions + + // #### Raise an exception. + // + // Raise an exception by passing a token, a string format and its paramters. + // + // The passed tokens location will automatically be added to the error + // message if it exists, if not it will default to the lexers current + // position. + // + // Example: + // + // // [1:0] expected [ near ( + // raise(token, "expected %1 near %2", '[', token.value); + + function raise(token) { + var message = sprintf.apply(null, slice.call(arguments, 1)) + , error, col; + + if ('undefined' !== typeof token.line) { + col = token.range[0] - token.lineStart; + error = new SyntaxError(sprintf('[%1:%2] %3', token.line, col, message)); + error.line = token.line; + error.index = token.range[0]; + error.column = col; + } else { + col = index - lineStart + 1; + error = new SyntaxError(sprintf('[%1:%2] %3', line, col, message)); + error.index = index; + error.line = line; + error.column = col; + } + throw error; + } + + // #### Raise an unexpected token error. + // + // Example: + // + // // expected near '0' + // raiseUnexpectedToken('', token); + + function raiseUnexpectedToken(type, token) { + raise(token, errors.expectedToken, type, token.value); + } + + // #### Raise a general unexpected error + // + // Usage should pass either a token object or a symbol string which was + // expected. We can also specify a nearby token such as , this will + // default to the currently active token. + // + // Example: + // + // // Unexpected symbol 'end' near '' + // unexpected(token); + // + // If there's no token in the buffer it means we have reached . + + function unexpected(found, near) { + if ('undefined' === typeof near) near = lookahead.value; + if ('undefined' !== typeof found.type) { + var type; + switch (found.type) { + case StringLiteral: type = 'string'; break; + case Keyword: type = 'keyword'; break; + case Identifier: type = 'identifier'; break; + case NumericLiteral: type = 'number'; break; + case Punctuator: type = 'symbol'; break; + case BooleanLiteral: type = 'boolean'; break; + case NilLiteral: + return raise(found, errors.unexpected, 'symbol', 'nil', near); + } + return raise(found, errors.unexpected, type, found.value, near); + } + return raise(found, errors.unexpected, 'symbol', found, near); + } + + // Lexer + // ----- + // + // The lexer, or the tokenizer reads the input string character by character + // and derives a token left-right. To be as efficient as possible the lexer + // prioritizes the common cases such as identifiers. It also works with + // character codes instead of characters as string comparisons was the + // biggest bottleneck of the parser. + // + // If `options.comments` is enabled, all comments encountered will be stored + // in an array which later will be appended to the chunk object. If disabled, + // they will simply be disregarded. + // + // When the lexer has derived a valid token, it will be returned as an object + // containing its value and as well as its position in the input string (this + // is always enabled to provide proper debug messages). + // + // `readToken()` starts lexing and returns the following token in the stream. + + var index + , token + , lookahead + , comments + , tokenStart + , line + , lineStart; + + function readToken() { + skipWhiteSpace(); + + // Skip comments beginning with -- + while (45 === input.charCodeAt(index) && + 45 === input.charCodeAt(index + 1)) { + scanComment(); + skipWhiteSpace(); + } + if (index >= length) return { + type : EOF + , value: '' + , line: line + , lineStart: lineStart + , range: [index, index] + }; + + var char = input.charCodeAt(index) + , next = input.charCodeAt(index + 1); + + // Memorize the range index where the token begins. + tokenStart = index; + if (isIdentifierStart(char)) return scanIdentifierOrKeyword(); + + switch (char) { + case 39: case 34: // '" + return scanStringLiteral(); + + // 0-9 + case 48: case 49: case 50: case 51: case 52: case 53: + case 54: case 55: case 56: case 57: + return scanNumericLiteral(); + + case 46: // . + // If the dot is followed by a digit it's a float. + if (isDecDigit(next)) return scanNumericLiteral(); + if (46 === next) { + if (46 === input.charCodeAt(index + 2)) return scanPunctuator('...'); + return scanPunctuator('..'); + } + return scanPunctuator('.'); + + case 61: // = + if (61 === next) return scanPunctuator('=='); + return scanPunctuator('='); + + case 62: // > + if (61 === next) return scanPunctuator('>='); + return scanPunctuator('>'); + + case 60: // < + if (61 === next) return scanPunctuator('<='); + return scanPunctuator('<'); + + case 126: // ~ + if (61 === next) return scanPunctuator('~='); + return raise({}, errors.expected, '=', '~'); + + case 58: // : + if (58 === next) return scanPunctuator('::'); + return scanPunctuator(':'); + + case 91: // [ + // Check for a multiline string, they begin with [= or [[ + if (91 === next || 61 === next) return scanLongStringLiteral(); + return scanPunctuator('['); + + // \* / ^ % , { } ] ( ) ; # - + + case 42: case 47: case 94: case 37: case 44: case 123: case 125: + case 93: case 40: case 41: case 59: case 35: case 45: case 43: + return scanPunctuator(input.charAt(index)); + } + + return unexpected(input.charAt(index)); + } + + // Whitespace has no semantic meaning in lua so simply skip ahead while + // tracking the encounted newlines. Newlines are also tracked in all + // token functions where multiline values are allowed. + + function skipWhiteSpace() { + while (index < length) { + var char = input.charCodeAt(index); + if (isWhiteSpace(char)) { + index++; + } else if (isLineTerminator(char)) { + line++; + lineStart = ++index; + } else { + break; + } + } + } + + // Identifiers, keywords, booleans and nil all look the same syntax wise. We + // simply go through them one by one and defaulting to an identifier if no + // previous case matched. + + function scanIdentifierOrKeyword() { + var value, type; + + // Slicing the input string is prefered before string concatenation in a + // loop for performance reasons. + while (isIdentifierPart(input.charCodeAt(++index))); + value = input.slice(tokenStart, index); + + // Decide on the token type and possibly cast the value. + if (isKeyword(value)) { + type = Keyword; + } else if ('true' === value || 'false' === value) { + type = BooleanLiteral; + value = ('true' === value); + } else if ('nil' === value) { + type = NilLiteral; + value = null; + } else { + type = Identifier; + } + + return { + type: type + , value: value + , line: line + , lineStart: lineStart + , range: [tokenStart, index] + }; + } + + // Once a punctuator reaches this function it should already have been + // validated so we simply return it as a token. + + function scanPunctuator(value) { + index += value.length; + return { + type: Punctuator + , value: value + , line: line + , lineStart: lineStart + , range: [tokenStart, index] + }; + } + + // Find the string literal by matching the delimiter marks used. + + function scanStringLiteral() { + var delimiter = input.charCodeAt(index++) + , stringStart = index + , string = '' + , char; + + while (index < length) { + char = input.charCodeAt(index++); + if (delimiter === char) break; + if (92 === char) { // \ + string += input.slice(stringStart, index - 1) + readEscapeSequence(); + stringStart = index; + } + // EOF or `\n` terminates a string literal. If we haven't found the + // ending delimiter by now, raise an exception. + else if (index >= length || isLineTerminator(char)) { + string += input.slice(stringStart, index - 1); + raise({}, errors.unfinishedString, string + String.fromCharCode(char)); + } + } + string += input.slice(stringStart, index - 1); + + return { + type: StringLiteral + , value: string + , line: line + , lineStart: lineStart + , range: [tokenStart, index] + }; + } + + // Expect a multiline string literal and return it as a regular string + // literal, if it doesn't validate into a valid multiline string, throw an + // exception. + + function scanLongStringLiteral() { + var string = readLongString(); + // Fail if it's not a multiline literal. + if (false === string) raise(token, errors.expected, '[', token.value); + + return { + type: StringLiteral + , value: string + , line: line + , lineStart: lineStart + , range: [tokenStart, index] + }; + } + + // Numeric literals will be returned as floating-point numbers instead of + // strings. The raw value should be retrieved from slicing the input string + // later on in the process. + // + // If a hexadecimal number is encountered, it will be converted. + + function scanNumericLiteral() { + var char = input.charAt(index) + , next = input.charAt(index + 1); + + var value = ('0' === char && ~'xX'.indexOf(next || null)) ? + readHexLiteral() : readDecLiteral(); + + return { + type: NumericLiteral + , value: value + , line: line + , lineStart: lineStart + , range: [tokenStart, index] + }; + } + + // Lua hexadecimals have an optional fraction part and an optional binary + // exoponent part. These are not included in JavaScript so we will compute + // all three parts separately and then sum them up at the end of the function + // with the following algorithm. + // + // Digit := toDec(digit) + // Fraction := toDec(fraction) / 16 ^ fractionCount + // BinaryExp := 2 ^ binaryExp + // Number := ( Digit + Fraction ) * BinaryExp + + function readHexLiteral() { + var fraction = 0 // defaults to 0 as it gets summed + , binaryExponent = 1 // defaults to 1 as it gets multiplied + , binarySign = 1 // positive + , digit, fractionStart, exponentStart, digitStart; + + digitStart = index += 2; // Skip 0x part + + // A minimum of one hex digit is required. + if (!isHexDigit(input.charCodeAt(index))) + raise({}, errors.malformedNumber, input.slice(tokenStart, index)); + + while (isHexDigit(input.charCodeAt(index))) index++; + // Convert the hexadecimal digit to base 10. + digit = parseInt(input.slice(digitStart, index), 16); + + // Fraction part i optional. + if ('.' === input.charAt(index)) { + fractionStart = ++index; + + while (isHexDigit(input.charCodeAt(index))) index++; + fraction = input.slice(fractionStart, index); + + // Empty fraction parts should default to 0, others should be converted + // 0.x form so we can use summation at the end. + fraction = (fractionStart === index) ? 0 + : parseInt(fraction, 16) / Math.pow(16, index - fractionStart); + } + + // Binary exponents are optional + if (~'pP'.indexOf(input.charAt(index) || null)) { + index++; + + // Sign part is optional and defaults to 1 (positive). + if (~'+-'.indexOf(input.charAt(index) || null)) + binarySign = ('+' === input.charAt(index++)) ? 1 : -1; + + exponentStart = index; + + // The binary exponent sign requires a decimal digit. + if (!isDecDigit(input.charCodeAt(index))) + raise({}, errors.malformedNumber, input.slice(tokenStart, index)); + + while (isDecDigit(input.charCodeAt(index))) index++; + binaryExponent = input.slice(exponentStart, index); + + // Calculate the binary exponent of the number. + binaryExponent = Math.pow(2, binaryExponent * binarySign); + } + + return (digit + fraction) * binaryExponent; + } + + // Decimal numbers are exactly the same in Lua and in JavaScript, because of + // this we check where the token ends and then parse it with native + // functions. + + function readDecLiteral() { + while (isDecDigit(input.charCodeAt(index))) index++; + // Fraction part is optional + if ('.' === input.charAt(index)) { + index++; + // Fraction part defaults to 0 + while (isDecDigit(input.charCodeAt(index))) index++; + } + // Exponent part is optional. + if (~'eE'.indexOf(input.charAt(index) || null)) { + index++; + // Sign part is optional. + if (~'+-'.indexOf(input.charAt(index) || null)) index++; + // An exponent is required to contain at least one decimal digit. + if (!isDecDigit(input.charCodeAt(index))) + raise({}, errors.malformedNumber, input.slice(tokenStart, index)); + + while (isDecDigit(input.charCodeAt(index))) index++; + } + + return parseFloat(input.slice(tokenStart, index)); + } + + + // Translate escape sequences to the actual characters. + + function readEscapeSequence() { + var sequenceStart = index; + switch (input.charAt(index)) { + // Lua allow the following escape sequences. + // We don't escape the bell sequence. + case 'n': index++; return '\n'; + case 'r': index++; return '\r'; + case 't': index++; return '\t'; + case 'v': index++; return '\v'; + case 'b': index++; return '\b'; + case 'f': index++; return '\f'; + // Skips the following span of white-space. + case 'z': index++; skipWhiteSpace(); return ''; + // Byte representation should for now be returned as is. + case 'x': + // \xXX, where XX is a sequence of exactly two hexadecimal digits + if (isHexDigit(input.charCodeAt(index + 1)) && + isHexDigit(input.charCodeAt(index + 2))) { + index += 3; + // Return it as is, without translating the byte. + return '\\' + input.slice(sequenceStart, index); + } + return '\\' + input.charAt(index++); + default: + // \ddd, where ddd is a sequence of up to three decimal digits. + if (isDecDigit(input.charCodeAt(index))) { + while (isDecDigit(input.charCodeAt(++index))); + return '\\' + input.slice(sequenceStart, index); + } + // Simply return the \ as is, it's not escaping any sequence. + return input.charAt(index++); + } + } + + // Comments begin with -- after which it will be decided if they are + // multiline comments or not. + // + // The multiline functionality works the exact same way as with string + // literals so we reuse the functionality. + + function scanComment() { + tokenStart = index; + index += 2; // -- + + var char = input.charAt(index) + , content = '' + , isLong = false + , commentStart = index; + + if ('[' === char) { + content = readLongString(); + // This wasn't a multiline comment after all. + if (false === content) content = char; + else { + isLong = true; + index += 2; // Trailing -- + } + } + // Scan until next line as long as it's not a multiline comment. + if (!isLong) { + while (index < length) { + if (isLineTerminator(input.charCodeAt(index))) break; + index++; + } + content = input.slice(commentStart, index); + } + + if (options.comments) { + comments.push({ + type: 'Comment' + , value: content + , raw: input.slice(tokenStart, index) + }); + } + } + + // Read a multiline string by calculating the depth of `=` characters and + // then appending until an equal depth is found. + + function readLongString() { + var level = 0 + , content = '' + , terminator = false + , char, stringStart; + + index++; // [ + + // Calculate the depth of the comment. + while ('=' === input.charAt(index + level)) level++; + // Exit, this is not a long string afterall. + if ('[' !== input.charAt(index + level)) return false; + + index += level + 1; + + // If the first character is a newline, ignore it and begin on next line. + if (isLineTerminator(input.charCodeAt(index))) { + line++; + lineStart = index++; + } + + stringStart = index; + while (index < length) { + char = input.charAt(index++); + + // We have to keep track of newlines as `skipWhiteSpace()` does not get + // to scan this part. + if (isLineTerminator(char.charCodeAt(0))) { + line++; + lineStart = index; + } + + // Once the delimiter is found, iterate through the depth count and see + // if it matches. + + if (']' === char) { + terminator = true; + for (var i = 0; i < level; i++) { + if ('=' !== input.charAt(index + i)) terminator = false; + } + if (']' !== input.charAt(index + level)) terminator = false; + } + + // We reached the end of the multiline string. Get out now. + if (terminator) break; + + if ('\\' === char) { + content += input.slice(stringStart, index - 1) + readEscapeSequence(); + stringStart = index; + } + } + content += input.slice(stringStart, index - 1); + index += level + 1; + + return content; + } + + // ## Lex functions and helpers. + + // Read the next token. + // + // This is actually done by setting the current token to the lookahead and + // reading in the new lookahead token. + + function next() { + token = lookahead; + lookahead = readToken(); + } + + // Consume a token if its value matches. Once consumed or not, return the + // success of the operation. + + function consume(value) { + if (value === token.value) { + next(); + return true; + } + return false; + } + + // Check if the given expression exists and raise an exception if not. + // + // As expressions can return null due to the design of the parser, we often + // need this strict expression check as well. + + function expectExpression(expression) { + if (null == expression) raiseUnexpectedToken('', token); + else return expression; + } + + // Expect the next token value to match. If not, throw an exception. + + function expect(value) { + if (value === token.value) next(); + else raise(token, errors.expected, value, token.value); + } + + // ### Validation functions + + function isWhiteSpace(char) { + return 9 === char || 32 === char || 0xB === char || 0xC === char; + } + + function isLineTerminator(char) { + return 10 === char || 13 === char; + } + + function isDecDigit(char) { + return char >= 48 && char <= 57; + } + + function isHexDigit(char) { + return (char >= 48 && char <= 57) || (char >= 97 && char <= 102) || (char >= 65 && char <= 70); + } + + // From [Lua 5.2](http://www.lua.org/manual/5.2/manual.html#8.1) onwards + // identifiers cannot use locale-dependet letters. + + function isIdentifierStart(char) { + return (char >= 65 && char <= 90) || (char >= 97 && char <= 122) || 95 === char; + } + + function isIdentifierPart(char) { + return (char >= 65 && char <= 90) || (char >= 97 && char <= 122) || 95 === char || (char >= 48 && char <= 57); + } + + // [3.1 Lexical Conventions](http://www.lua.org/manual/5.2/manual.html#3.1) + // + // `true`, `false` and `nil` will not be considered keywords, but literals. + + function isKeyword(id) { + switch (id.length) { + case 2: + return 'do' === id || 'if' === id || 'in' === id || 'or' === id; + case 3: + return 'and' === id || 'end' === id || 'for' === id || 'not' === id; + case 4: + return 'else' === id || 'goto' === id || 'then' === id; + case 5: + return 'break' === id || 'local' === id || 'until' === id || 'while' === id; + case 6: + return 'elseif' === id || 'repeat' === id || 'return' === id; + case 8: + return 'function' === id; + } + return false; + } + + function isUnary(token) { + if (Punctuator === token.type) return ~'#-'.indexOf(token.value); + if (Keyword === token.type) return 'not' === token.value; + return false; + } + + // @TODO this needs to be rethought. + function isCallExpression(expression) { + switch (expression.type) { + case 'CallExpression': + case 'TableCallExpression': + case 'StringCallExpression': + return true; + } + return false; + } + + // Check if the token syntactically closes a block. + + function isBlockFollow(token) { + if (EOF === token.type) return true; + if (Keyword !== token.type) return false; + switch (token.value) { + case 'else': case 'elseif': + case 'end': case 'until': + return true; + default: + return false; + } + } + + // Parse functions + // --------------- + + // Chunk is the main program object. Syntactically it's the same as a block. + // + // chunk ::= block + + function parseChunk() { + next(); + var body = parseBlock(); + if (EOF !== token.type) unexpected(token); + return ast.chunk(body); + } + + // A block contains a list of statements with an optional return statement + // as its last statement. + // + // block ::= {stat} [retstat] + + function parseBlock(terminator) { + var block = [] + , statement; + + while (!isBlockFollow(token)) { + // Return has to be the last statement in a block. + if ('return' === token.value) { + block.push(parseStatement()); + break; + } + statement = parseStatement(); + // Statements are only added if they are returned, this allows us to + // ignore some statements, such as EmptyStatement. + if (statement) block.push(statement); + } + // Doesn't really need an ast node + return block; + } + + // There are two types of statements, simple and compound. + // + // statement ::= break | goto | do | while | repeat | return + // | if | for | function | local | label | assignment + // | functioncall | ';' + + function parseStatement() { + if (Keyword === token.type) { + switch (token.value) { + case 'local': next(); return parseLocalStatement(); + case 'if': next(); return parseIfStatement(); + case 'return': next(); return parseReturnStatement(); + case 'function': next(); + var name = parseFunctionName(); + return parseFunctionDeclaration(name); + case 'while': next(); return parseWhileStatement(); + case 'for': next(); return parseForStatement(); + case 'repeat': next(); return parseRepeatStatement(); + case 'break': next(); return parseBreakStatement(); + case 'do': next(); return parseDoStatement(); + case 'goto': next(); return parseGotoStatement(); + } + } + + if (Punctuator === token.type) { + if (consume('::')) return parseLabelStatement(); + } + + // When a `;` is encounted, simply eat it without storing it. + if (consume(';')) return; + + return parseAssignmentOrCallStatement(); + } + + // ## Statements + + // label ::= '::' Name '::' + + function parseLabelStatement() { + var label = parseIdentifier(); + expect('::'); + return ast.labelStatement(label); + } + + // break ::= 'break' + + function parseBreakStatement() { + return ast.breakStatement(); + } + + // goto ::= 'goto' Name + + function parseGotoStatement() { + var label = parseIdentifier(); + return ast.gotoStatement(label); + } + + // do ::= 'do' block 'end' + + function parseDoStatement() { + var body = parseBlock(); + expect('end'); + return ast.doStatement(body); + } + + // while ::= 'while' exp 'do' block 'end' + + function parseWhileStatement() { + var condition = parseExpression(); + expect('do'); + var body = parseBlock(); + expect('end'); + return ast.whileStatement(condition, body); + } + + // repeat ::= 'repeat' block 'until' exp + + function parseRepeatStatement() { + var body = parseBlock(); + expect('until'); + var condition = expectExpression(parseExpression()); + return ast.repeatStatement(condition, body); + } + + // retstat ::= 'return' [exp {',' exp}] [';'] + + function parseReturnStatement() { + var expressions = []; + + if ('end' !== token.value) { + var expression = parseExpression(); + if (null != expression) expressions.push(expression); + while (consume(',')) { + expression = expectExpression(parseExpression()); + expressions.push(expression); + } + consume(';'); // grammar tells us ; is optional here. + } + return ast.returnStatement(expressions); + } + + // if ::= 'if' exp 'then' block {elif} ['else' block] 'end' + // elif ::= 'elseif' exp 'then' block + + function parseIfStatement() { + var clauses = [] + , condition + , body; + + do { + condition = parseExpression(); + expect('then'); + body = parseBlock(); + clauses.push(ast.elseifClause(condition, body)); + } while (consume('elseif')); + + if (consume('else')) { + body = parseBlock(); + clauses.push(ast.elseClause(body)); + } + + expect('end'); + return ast.ifStatement(clauses); + } + + // There are two types of for statements, generic and numeric. + // + // for ::= Name '=' exp ',' exp [',' exp] 'do' block 'end' + // for ::= namelist 'in' explist 'do' block 'end' + // namelist ::= Name {',' Name} + // explist ::= exp {',' exp} + + function parseForStatement() { + var variable = parseIdentifier() + , body; + + // If the first expression is followed by a `=` punctuator, this is a + // Numeric For Statement. + if (consume('=')) { + // Start expression + var start = expectExpression(parseExpression()); + expect(','); + // End expression + var end = expectExpression(parseExpression()); + // Optional step expression + var step = consume(',') ? expectExpression(parseExpression()) : null; + + expect('do'); + body = parseBlock(); + expect('end'); + + return ast.forNumericStatement(variable, start, end, step, body); + + // If not, it's a Generic For Statement + } else { + // The namelist can contain one or more identifiers. + var variables = [variable]; + while (consume(',')) variables.push(parseIdentifier()); + expect('in'); + var iterators = []; + + // One or more expressions in the explist. + do { + var expression = expectExpression(parseExpression()); + iterators.push(expression); + } while (consume(',')); + + expect('do'); + body = parseBlock(); + expect('end'); + + return ast.forGenericStatement(variables, iterators, body); + } + } + + // Local statements can either be variable assignments or function + // definitions. If a function definition is found, it will be delegated to + // `parseFunctionDeclaration()` with the isLocal flag. + // + // This AST structure might change into a local assignment with a function + // child. + // + // local ::= 'local' 'function' Name funcdecl + // | 'local' Name {',' Name} ['=' exp {',' exp} + + function parseLocalStatement() { + if (Identifier === token.type) { + var variables = []; + var init = []; + + do { + variables.push(parseIdentifier()); + } while (consume(',')); + + if (consume('=')) { + do { + var expression = expectExpression(parseExpression()); + init.push(expression); + } while (consume(',')); + } + + return ast.localStatement(variables, init); + } + if (consume('function')) { + // MemberExpressions are not allowed in local function statements. + var name = parseIdentifier(); + return parseFunctionDeclaration(name, true); + } else { + raiseUnexpectedToken('', token); + } + } + + // assignment ::= varlist '=' explist + // varlist ::= prefixexp {',' prefixexp} + // explist ::= exp {',' exp} + // + // call ::= callexp + // callexp ::= prefixexp args | prefixexp ':' Name args + + function parseAssignmentOrCallStatement() { + // Keep a reference to the previous token for better error messages in case + // of invalid statement + var previous = token + , expression = parsePrefixExpression(); + + if (null == expression) return unexpected(token); + if (~',='.indexOf(token.value)) { + var variables = [expression] + , init = [] + , exp; + + while (consume(',')) { + exp = expectExpression(parsePrefixExpression()); + variables.push(exp); + } + expect('='); + do { + exp = expectExpression(parseExpression()); + init.push(exp); + } while (consume(',')); + return ast.assignmentStatement(variables, init); + } + if (isCallExpression(expression)) { + return ast.callStatement(expression); + } + // The prefix expression was neither part of an assignment or a + // callstatement, however as it was valid it's been consumed, so raise + // the exception on the previous token to provide a helpful message. + return unexpected(previous); + } + + + + // ### Non-statements + + // Identifier ::= Name + + function parseIdentifier() { + var identifier = token.value; + if (Identifier !== token.type) raiseUnexpectedToken('', token); + next(); + return ast.identifier(identifier); + } + + + // Parse the functions parameters and body block. The name should already + // have been parsed and passed to this declaration function. By separating + // this we allow for anonymous functions in expressions. + // + // For local functions there's a boolean parameter which needs to be set + // when parsing the declaration. + // + // funcdecl ::= '(' [parlist] ')' block 'end' + // parlist ::= Name {',' Name} | [',' '...'] | '...' + + function parseFunctionDeclaration(name, isLocal) { + var isVararg = false; + var parameters = []; + expect('('); + + if (consume('...')) isVararg = true; + else if (Identifier === token.type) { + do { + if (consume('...')) { + isVararg = true; + break; + } + parameters.push(parseIdentifier()); + } while (consume(',')); + } + if (isVararg) expect(')'); + else if (!consume(')')) raiseUnexpectedToken(' or \'...\'', token); + + var body = parseBlock(); + expect('end'); + + isLocal = isLocal || false; + return ast.functionStatement(name, parameters, isVararg, isLocal, body); + } + + // Parse the function name as identifiers and member expressions. + // + // Name {'.' Name} [':' Name] + + function parseFunctionName() { + var base = parseIdentifier(); + + while (consume('.')) { + base = ast.memberExpression(base, '.', parseIdentifier()); + } + + if (consume(':')) { + base = ast.memberExpression(base, ':', parseIdentifier()); + } + + return base; + } + + // tableconstructor ::= '{' [fieldlist] '}' + // fieldlist ::= field {fieldsep field} fieldsep + // field ::= '[' exp ']' '=' exp | Name = 'exp' | exp + // + // fieldsep ::= ',' | ';' + + function parseTableConstructor() { + var fields = [] + , key, value; + + while (true) { + if (Punctuator === token.type && consume('[')) { + key = parseExpression(); + expect(']'); + expect('='); + value = expectExpression(parseExpression()); + fields.push(ast.tableKey(key, value)); + } else if (Identifier === token.type) { + key = parseExpression(); + if (consume('=')) { + value = parseExpression(); + fields.push(ast.tableKeyString(key, value)); + } else { + fields.push(ast.tableValue(key)); + } + } else { + if (null == (value = parseExpression())) break; + fields.push(ast.tableValue(value)); + } + if (~',;'.indexOf(token.value)) { + next(); + continue; + } + if ('}' === token.value) break; + } + expect('}'); + return ast.tableConstructorExpression(fields); + } + + // Expression parser + // ----------------- + // + // Expressions are evaluated and always return a value. + // + // exp ::= (unop exp | primary | prefixexp ) { binop exp } + // + // primary ::= nil | false | true | Number | String | '...' + // | functiondef | tableconstructor + // + // prefixexp ::= (Name | '(' exp ')' ) { '[' exp ']' + // | '.' Name | ':' Name args | args } + // + + function parseExpression() { + var expression = parseSubExpression(0); + return expression; + } + + // Return the precedence priority of the operator. + // + // As unary `-` can't be distinguished from binary `-`, unary precedence + // isn't described in this table but in `parseSubExpression()` itself. + // + // As this function gets hit on every expression it's been optimized due to + // the expensive CompareICStub which took ~8% of the parse time. + + function binaryPrecedence(operator) { + var char = operator.charCodeAt(0) + , length = operator.length; + + if (1 === length) { + switch (char) { + case 94: return 10; // ^ + case 42: case 47: case 37: return 7; // * / % + case 43: case 45: return 6; // + - + case 60: case 62: return 3; // < > + } + } else if (2 === length) { + switch (char) { + case 46: return 5; // .. + case 60: case 62: case 61: case 126: return 3; // <= >= == ~= + case 111: return 1; // or + } + } else if (97 === char && 'and' === operator) return 2; + return 0; + } + + // Implement an operator-precedence parser to handle binary operator + // precedence. + // + // We use this algorithm because it's compact, it's fast and Lua core uses + // the same so we can be sure our expressions are parsed in the same manner + // without excessive amounts of tests. + // + // exp ::= (unop exp | primary | prefixexp ) { binop exp } + + function parseSubExpression(minPrecedence) { + var operator = token.value; + // The left-hand side in binary operations. + var expression; + + // UnaryExpression + if (isUnary(token)) { + next(); + var argument = expectExpression(parseSubExpression(8)); + expression = ast.unaryExpression(operator, argument); + } + if (null == expression) { + // PrimaryExpression + expression = parsePrimaryExpression(); + + // PrefixExpression + if (null == expression) { + expression = parsePrefixExpression(); + } + } + // This is not a valid left hand expression. + if (null == expression) return null; + + var precedence; + while (true) { + operator = token.value; + + precedence = (Punctuator === token.type || Keyword === token.type) ? + binaryPrecedence(operator) : 0; + + if (precedence === 0 || precedence <= minPrecedence) break; + // Right-hand precedence operators + if ('^' === operator || '..' === operator) precedence--; + next(); + var right = expectExpression(parseSubExpression(precedence)); + expression = ast.binaryExpression(operator, expression, right); + } + return expression; + } + + // prefixexp ::= prefix {suffix} + // prefix ::= Name | '(' exp ')' + // suffix ::= '[' exp ']' | '.' Name | ':' Name args | args + // + // args ::= '(' [explist] ')' | tableconstructor | String + + function parsePrefixExpression() { + var base; + + // The prefix + if (Identifier === token.type) { + base = parseIdentifier(); + } else if (consume('(')) { + base = parseExpression(); + expect(')'); + } else { + return null; + } + + // The suffix + var expression, identifier; + while (true) { + expectExpression(base); + if (Punctuator === token.type) { + switch (token.value) { + case '[': + next(); + expression = parseExpression(); + base = ast.indexExpression(base, expression); + expect(']'); + break; + case '.': + next(); + identifier = parseIdentifier(); + base = ast.memberExpression(base, '.', identifier); + break; + case ':': + next(); + identifier = parseIdentifier(); + base = ast.memberExpression(base, ':', identifier); + // Once a : is found, this has to be a callexpression, otherwise + // throw an error. + base = parseCallExpression(base); + break; + case '(': case '{': // args + base = parseCallExpression(base); + break; + default: + return base; + } + } else if (StringLiteral === token.type) { + base = parseCallExpression(base); + } else { + break; + } + } + + return base; + } + + // args ::= '(' [explist] ')' | tableconstructor | String + + function parseCallExpression(base) { + if (Punctuator === token.type) { + switch (token.value) { + case '(': + next(); + + // List of expressions + var expressions = []; + var expression = parseExpression(); + if (null != expression) expressions.push(expression); + while (consume(',')) { + expression = expectExpression(parseExpression()); + expressions.push(expression); + } + + expect(')'); + return ast.callExpression(base, expressions); + + case '{': + next(); + var table = parseTableConstructor(); + return ast.tableCallExpression(base, table); + } + + } else if (StringLiteral === token.type) { + var string = token.value; + next(); + return ast.stringCallExpression(base, string); + } + + raiseUnexpectedToken('function arguments', token); + } + + // primary ::= String | Numeric | nil | true | false + // | functiondef | tableconstructor | '...' + + function parsePrimaryExpression() { + var literals = StringLiteral | NumericLiteral | BooleanLiteral | NilLiteral + , value = token.value; + + if (token.type & literals) { + var raw = input.slice(token.range[0], token.range[1]); + next(); + return ast.literal(value, raw); + } else if (Keyword === token.type && 'function' === token.value) { + next(); + return parseFunctionDeclaration(null); + } else if (Punctuator === token.type) { + // Semantically dotsliteral can only exist within a vararg functions. + if (consume('...')) return ast.varargLiteral(value); + if (consume('{')) return parseTableConstructor(); + } + } + + // Parser + // ------ + + // Export the main parser. + // + // - `wait` Hold parsing until end() is called. Defaults to false + // + // Example: + // + // var parser = require('luaparser'); + // parser.parse('i = 0'); + + exports.parse = parse; + + function parse(_input, _options) { + if ('undefined' === typeof _options && 'object' === typeof _input) { + _options = _input; + _input = undefined; + } + if (!_options) _options = {}; + + input = _input || ''; + options = extend(defaultOptions, _options); + + // Rewind the lexer + index = 0; + line = 1; + lineStart = 0; + length = input.length; + + if (options.comments) comments = []; + if (!options.wait) return end(); + return exports; + } + + // Write to the source code buffer without beginning the parse. + exports.write = write; + + function write(_input) { + input += String(_input); + length = input.length; + return exports; + } + + // Send an EOF and begin parsing. + exports.end = end; + + function end(_input) { + if ('undefined' !== typeof _input) write(_input); + + length = input.length; + // Initialize with a lookahead token. + lookahead = readToken(); + + var chunk = parseChunk(); + if (options.comments) chunk.comments = comments; + return chunk; + } + + // Expose the lex function + exports.lex = readToken; + +})); +/* vim: set sw=2 ts=2 et tw=79 : */ + +}); \ No newline at end of file diff --git a/lib/ace/mode/lua_worker.js b/lib/ace/mode/lua_worker.js new file mode 100644 index 00000000..9c08f614 --- /dev/null +++ b/lib/ace/mode/lua_worker.js @@ -0,0 +1,71 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Distributed under the BSD license: + * + * Copyright (c) 2010, Ajax.org B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Ajax.org B.V. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var Mirror = require("../worker/mirror").Mirror; +var luaparse = require("../mode/lua/luaparse"); + +var Worker = exports.Worker = function(sender) { + Mirror.call(this, sender); + this.setTimeout(500); +}; + +oop.inherits(Worker, Mirror); + +(function() { + + this.onUpdate = function() { + var value = this.doc.getValue(); + + // var t=Date.now() + try { + luaparse.parse(value); + } catch(e) { + if (e instanceof SyntaxError) { + this.sender.emit("error", { + row: e.line - 1, + column: e.column, + text: e.message, + type: "error" + }); + } + // console.log( t-Date.now()) + return; + } + // console.log( t-Date.now()) + this.sender.emit("ok"); + }; + +}).call(Worker.prototype); + +}); diff --git a/tool/update_deps.js b/tool/update_deps.js index b0c30c19..0fc8caa4 100644 --- a/tool/update_deps.js +++ b/tool/update_deps.js @@ -23,6 +23,10 @@ var deps = [{ path: "../../demo/kitchen-sink/require.js", url: "https://raw.github.com/jrburke/requirejs/master/require.js", needsFixup: false +}, { + path: "mode/lua/luaparse.js", + url: "https://raw.github.com/oxyc/luaparse/master/lib/luaparse.js", + needsFixup: true }] var download = function(href, callback) {