Switch to using Mozilla's standardized AST model for PaperScript parsing though Acorn.js or Esprima.js and their support for ranges.

No more AST mingling but direct code modification means we're finally getting accurate error messages! Sticking to Esprima for now since Acorn still has some issues with ranges: https://github.com/marijnh/acorn/issues/14
This commit is contained in:
Jürg Lehni 2012-11-18 10:06:16 -08:00
parent 67dca29009
commit 34819e6a73
6 changed files with 3981 additions and 77 deletions

View file

@ -42,7 +42,7 @@ case $1 in
;; ;;
compressed) compressed)
eval $COMMAND > temp.js eval $COMMAND > temp.js
../../uglifyjs/bin/uglifyjs temp.js --extra --unsafe --reserved-names "$eval,$sign" > $5 ../../uglifyjs/bin/uglifyjs temp.js --extra --unsafe --reserved-names "_$_,$_" > $5
rm temp.js rm temp.js
;; ;;
esac esac

2
lib/acorn-min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -444,7 +444,8 @@
++tokPos; ++tokPos;
ch = input.charCodeAt(tokPos); ch = input.charCodeAt(tokPos);
} }
(tokComments || (tokComments = [])).push(input.slice(start, tokPos)); if (options.trackComments)
(tokComments || (tokComments = [])).push(input.slice(start, tokPos));
} }
// Called at the start of the parse and after every token. Skips // Called at the start of the parse and after every token. Skips
@ -878,7 +879,8 @@
if (tokCommentsAfter) { if (tokCommentsAfter) {
node.commentsAfter = tokCommentsAfter; node.commentsAfter = tokCommentsAfter;
tokCommentsAfter = null; tokCommentsAfter = null;
} else if (lastFinishedNode && lastFinishedNode.end === lastEnd) { } else if (lastFinishedNode && lastFinishedNode.end === lastEnd &&
lastFinishedNode.commentsAfter) {
node.commentsAfter = lastFinishedNode.commentsAfter; node.commentsAfter = lastFinishedNode.commentsAfter;
lastFinishedNode.commentsAfter = null; lastFinishedNode.commentsAfter = null;
} }
@ -911,9 +913,8 @@
// Test whether a semicolon can be inserted at the current position. // Test whether a semicolon can be inserted at the current position.
function canInsertSemicolon() { function canInsertSemicolon() {
return tokType === _eof || tokType === _braceR || return !options.strictSemicolons &&
!options.strictSemicolons && (tokType === _eof || tokType === _braceR || newline.test(input.slice(lastEnd, tokStart)));
newline.test(input.slice(lastEnd, tokStart));
} }
// Consume a semicolon, or, failing that, see if we are allowed to // Consume a semicolon, or, failing that, see if we are allowed to
@ -1421,6 +1422,7 @@
case _num: case _string: case _regexp: case _num: case _string: case _regexp:
var node = startNode(); var node = startNode();
node.value = tokVal; node.value = tokVal;
node.raw = input.slice(tokStart, tokEnd);
next(); next();
return finishNode(node, "Literal"); return finishNode(node, "Literal");
@ -1585,9 +1587,9 @@
function parseIdent(liberal) { function parseIdent(liberal) {
var node = startNode(); var node = startNode();
node.name = tokType === _name ? tokVal : (liberal && tokType.keyword) || unexpected(); node.name = tokType === _name ? tokVal : (liberal && !options.forbidReserved && tokType.keyword) || unexpected();
next(); next();
return finishNode(node, "Identifier"); return finishNode(node, "Identifier");
} }
})(typeof exports === "undefined" ? (window.acorn = {}) : exports); })(typeof exports === "undefined" ? (window.acorn = {}) : exports);

36
lib/esprima-min.js vendored Normal file

File diff suppressed because one or more lines are too long

3809
lib/esprima.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -18,12 +18,17 @@
* @name PaperScript * @name PaperScript
* @namespace * @namespace
*/ */
/*#*/ if (options.parser == 'acorn') {
/*#*/ include('../../lib/acorn-min.js');
/*#*/ } else {
/*#*/ include('../../lib/esprima-min.js');
/*#*/ }
var PaperScript = this.PaperScript = new function() { var PaperScript = this.PaperScript = new function() {
/*#*/ include('../../lib/parse-js-min.js'); // Operators to overload
// Math Operators var binaryOperators = {
var operators = {
'+': 'add', '+': 'add',
'-': 'subtract', '-': 'subtract',
'*': 'multiply', '*': 'multiply',
@ -33,8 +38,18 @@ var PaperScript = this.PaperScript = new function() {
'!=': 'equals' '!=': 'equals'
}; };
function $eval(left, operator, right) { var unaryOperators = {
var handler = operators[operator]; '-': 'negate',
'+': null
};
// Use very short name for the binary operator (_$_) as well as the
// unary operator ($_), as operations will be replaced with then.
// The underscores stands for the values, and the $ for the operators.
// Binary Operator Handler
function _$_(left, operator, right) {
var handler = binaryOperators[operator];
if (left && left[handler]) { if (left && left[handler]) {
var res = left[handler](right); var res = left[handler](right);
return operator === '!=' ? !res : res; return operator === '!=' ? !res : res;
@ -52,41 +67,21 @@ var PaperScript = this.PaperScript = new function() {
} }
} }
// Sign Operators // Unary Operator Handler
function $_(operator, value) {
var signOperators = { var handler = unaryOperators[operator];
'-': 'negate' if (handler && value && value[handler])
};
function $sign(operator, value) {
var handler = signOperators[operator];
if (value && value[handler]) {
return value[handler](); return value[handler]();
}
switch (operator) { switch (operator) {
case '+': return +value; case '+': return +value;
case '-': return -value; case '-': return -value;
default: default:
throw new Error('Implement Sign Operator: ' + operator); throw new Error('Implement Unary Operator: ' + operator);
} }
} }
// AST Helpers // AST Helpers
function isDynamic(exp) {
var type = exp[0];
return type != 'num' && type != 'string';
}
function handleOperator(operator, left, right) {
// Only replace operators with calls to $operator if the left hand side
// is potentially an object.
if (operators[operator] && isDynamic(left)) {
// Replace with call to $operator(left, operator, right):
return ['call', ['name', '$eval'],
[left, ['string', operator], right]];
}
}
/** /**
* Compiles PaperScript code into JavaScript code. * Compiles PaperScript code into JavaScript code.
@ -97,49 +92,111 @@ var PaperScript = this.PaperScript = new function() {
* @return {String} The compiled PaperScript as JavaScript code. * @return {String} The compiled PaperScript as JavaScript code.
*/ */
function compile(code) { function compile(code) {
// Use parse-js to translate the code into a AST structure which is then // Use Acorn or Esprima to translate the code into an AST structure
// walked and parsed for operators to overload. The resulting AST is // which is then walked and parsed for operators to overload.
// translated back to code and evaluated. // Instead of modifying the AST and converting back to code, we directly
var ast = parse_js.parse(code), // change the source code based on the parser's range information, so we
walker = parse_js.ast_walker(), // can preserve line-numbers in syntax errors and remove the need for
walk = walker.walk; // Escodegen.
ast = walker.with_walkers({ // Tracks code insertions so we can add their differences to the
'binary': function(operator, left, right) { // original offsets.
// Handle simple mathematical operators here: var insertions = [];
return handleOperator(operator, left = walk(left),
right = walk(right))
// Always return a new AST for this node, since we have
// processed left and right int he call above!
|| [this[0], operator, left, right];
},
'assign': function(operator, left, right) { // Converts an original offset to the one in the current state of the
// Handle assignments like +=, -=, etc: // modified code.
// Check if the assignment operator needs to be handled by paper function getOffset(offset) {
// if so, convert the assignment to a simple = and use result of var start = offset;
// of handleOperator on the right hand side. // Add all insertions before this location together to calculate
var res = handleOperator(operator, left = walk(left), // the current offset
right = walk(right)); for (var i = 0, l = insertions.length; i < l; i++) {
return res var insertion = insertions[i];
? [this[0], true, left, res] if (insertion[0] >= offset)
// Always return a new AST for the same reason as in binary break;
: [this[0], operator, left, right]; offset += insertion[1];
}, }
return offset;
}
'unary-prefix': function(operator, exp) { // Returns the node's code as a string, taking insertions into account.
if (signOperators[operator] && isDynamic(exp)) { function getCode(node) {
return ['call', ['name', '$sign'], return code.substring(getOffset(node.range[0]),
[['string', operator], walk(exp)]]; getOffset(node.range[1]));
}
// Replaces the node's code with a new version and keeps insertions
// information up-to-date.
function replaceCode(node, str) {
var start = getOffset(node.range[0]),
end = getOffset(node.range[1]);
var insert = 0;
// Sort insertions by their offset, so getOffest() can do its thing
for (var i = insertions.length - 1; i >= 0; i--) {
if (start > insertions[i][0]) {
insert = i + 1;
break;
} }
} }
}, function() { insertions.splice(insert, 0, [start, str.length - end + start]);
return walk(ast); code = code.substring(0, start) + str + code.substring(end);
}); }
return parse_js.gen_code(ast, { // Recursively walks the AST and replaces the code of certain nodes
beautify: true function walkAst(node) {
}); for (var key in node) {
if (key === 'range')
continue;
var value = node[key];
if (Array.isArray(value)) {
for (var i = 0, l = value.length; i < l; i++)
walkAst(value[i]);
} else if (Base.isObject(value)) {
walkAst(value);
}
}
switch (node.type) {
case 'BinaryExpression':
if (node.operator in binaryOperators
&& node.left.type !== 'Literal') {
var left = getCode(node.left),
right = getCode(node.right);
replaceCode(node, '_$_(' + left + ', "' + node.operator
+ '", ' + right + ')');
}
break;
case 'AssignmentExpression':
if (/^.=$/.test(node.operator)
&& node.left.type !== 'Literal') {
var left = getCode(node.left),
right = getCode(node.right);
replaceCode(node, left + ' = _$_(' + left + ', "'
+ node.operator[0] + '", ' + right + ')');
}
break;
case 'UpdateExpression':
if (!node.prefix) {
var arg = getCode(node.argument);
replaceCode(node, arg + ' = _$_(' + arg + ', "'
+ node.operator[0] + '", 1)');
}
break;
case 'UnaryExpression':
if (node.operator in unaryOperators
&& node.argument.type !== 'Literal') {
var arg = getCode(node.argument);
replaceCode(node, '$_("' + node.operator + '", '
+ arg + ')');
}
break;
}
}
// Now do the parsing magic
/*#*/ if (options.parser == 'acorn') {
walkAst(acorn.parse(code, { ranges: true }));
/*#*/ } else {
walkAst(esprima.parse(code, { range: true }));
/*#*/ }
return code;
} }
/** /**