Skip to content

Commit

Permalink
expression parser (#206)
Browse files Browse the repository at this point in the history
* feat(expression-parser): delegate errors to Reporter for more descriptive errors

* feat(debug): create AST expression serializer

* test(expression-parser): use serializer instead of unparser for assertions

* test(expression-parser): initial rework for parser tests to cover all edge cases

* fix(expression-parser): correctly parse "unsafe" integers

* fix(expression-parser): disable broken "raw" values on template for now

* fix(expression-parser): correctly parse member expressions when preceded by a unary operator

* fix(expression-parser): allow template strings to have member expressions

* fix(expression-parser): allow AccessThis to be the tag for a template

* fix(expression-parser): use IsAssign instead of Conditional precedence for nested expressions

* fix(expression-parser): fix binary sibling operator precedence

* test(jit): re-enable some tests

* chore(test): reorganize tests and add clarifying comments

* test(expression-parser): add assign/vc/bb tests

fix(expression-parser): ensure AccessScope is assignable

* test(expression-parser): add all unicode tests back in

* refactor(expression-parser): extract enums and util function out into common.ts

* perf(expression-parser): reuse one ParserState object

* chore(jit): export common

* fix(expression-parser): properly detect EOF for unterminated quote instead of hanging

* fix(expression-parser): throw on invalid dot terminal in AccessThis

fix(expression-parser): throw on unterminated template instead of hanging

* fix(expression-parser): correctly parse number with trailing dot

* feat(expression-parser): improve error reporting

* chore(all): remove .only

* test(expression-parser): verify some more errors

* test(expression-parser): wrap up the error code tests

* chore(jit): move stuff around

* test(expression-parser): add BindingTypes to simple test combinations

* test(expression-parser): add invalidForOf

* test(unparser): let the unparser ride along with the parser tests

* chore(jit): fix typo
  • Loading branch information
fkleuver authored and EisenbergEffect committed Oct 5, 2018
1 parent 582c77c commit a0c9b0e
Show file tree
Hide file tree
Showing 8 changed files with 1,818 additions and 1,381 deletions.
173 changes: 164 additions & 9 deletions packages/debug/src/binding/unparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class Unparser implements AST.IVisitor<void> {
this.text += '(';
expr.object.accept(this);
this.text += `.${expr.name}`;
this.writeArgs(<AST.IExpression[]>expr.args);
this.writeArgs(expr.args);
this.text += ')';
}

Expand All @@ -138,7 +138,7 @@ export class Unparser implements AST.IVisitor<void> {
this.text += '$parent.';
}
this.text += expr.name;
this.writeArgs(<AST.IExpression[]>expr.args);
this.writeArgs(expr.args);
this.text += ')';
}

Expand Down Expand Up @@ -226,19 +226,19 @@ export class Unparser implements AST.IVisitor<void> {
}
}

public visitArrayBindingPattern(expr: AST.ArrayBindingPattern): void { }
public visitArrayBindingPattern(expr: AST.ArrayBindingPattern): string { throw new Error('visitArrayBindingPattern'); }

public visitObjectBindingPattern(expr: AST.ObjectBindingPattern): void { }
public visitObjectBindingPattern(expr: AST.ObjectBindingPattern): string { throw new Error('visitObjectBindingPattern'); }

public visitBindingIdentifier(expr: AST.BindingIdentifier): void { }
public visitBindingIdentifier(expr: AST.BindingIdentifier): string { throw new Error('visitBindingIdentifier'); }

public visitHtmlLiteral(expr: AST.HtmlLiteral): void { }
public visitHtmlLiteral(expr: AST.HtmlLiteral): string { throw new Error('visitHtmlLiteral'); }

public visitForOfStatement(expr: AST.ForOfStatement): void { }
public visitForOfStatement(expr: AST.ForOfStatement): string { throw new Error('visitForOfStatement'); }

public visitInterpolation(expr: AST.Interpolation): void { }
public visitInterpolation(expr: AST.Interpolation): string { throw new Error('visitInterpolation'); }

private writeArgs(args: AST.IExpression[]): void {
private writeArgs(args: ReadonlyArray<AST.IExpression>): void {
this.text += '(';
for (let i = 0, length = args.length; i < length; ++i) {
if (i !== 0) {
Expand All @@ -249,3 +249,158 @@ export class Unparser implements AST.IVisitor<void> {
this.text += ')';
}
}

/*@internal*/
export class Serializer implements AST.IVisitor<string> {
public static serialize(expr: AST.IExpression): string {
const visitor = new Serializer();
if (expr === null || expr === undefined || typeof expr.accept !== 'function') {
return `${expr}`;
}
return expr.accept(visitor);
}

public visitAccessMember(expr: AST.AccessMember): string {
return `{"type":"AccessMember","name":${expr.name},"object":${expr.object.accept(this)}}`;
}

public visitAccessKeyed(expr: AST.AccessKeyed): string {
return `{"type":"AccessKeyed","object":${expr.object.accept(this)},"key":${expr.key.accept(this)}}`;
}

public visitAccessThis(expr: AST.AccessThis): string {
return `{"type":"AccessThis","ancestor":${expr.ancestor}}`;
}

public visitAccessScope(expr: AST.AccessScope): string {
return `{"type":"AccessScope","name":"${expr.name}","ancestor":${expr.ancestor}}`;
}

public visitArrayLiteral(expr: AST.ArrayLiteral): string {
return `{"type":"ArrayLiteral","elements":${this.serializeExpressions(expr.elements)}}`;
}

public visitObjectLiteral(expr: AST.ObjectLiteral): string {
return `{"type":"ObjectLiteral","keys":${serializePrimitives(expr.keys)},"values":${this.serializeExpressions(expr.values)}}`;
}

public visitPrimitiveLiteral(expr: AST.PrimitiveLiteral): string {
return `{"type":"PrimitiveLiteral","value":${serializePrimitive(expr.value)}}`;
}

public visitCallFunction(expr: AST.CallFunction): string {
return `{"type":"CallFunction","func":${expr.func.accept(this)},"args":${this.serializeExpressions(expr.args)}}`;
}

public visitCallMember(expr: AST.CallMember): string {
return `{"type":"CallMember","name":"${expr.name}","object":${expr.object.accept(this)},"args":${this.serializeExpressions(expr.args)}}`;
}

public visitCallScope(expr: AST.CallScope): string {
return `{"type":"CallScope","name":"${expr.name}","ancestor":${expr.ancestor},"args":${this.serializeExpressions(expr.args)}}`;
}

public visitTemplate(expr: AST.Template): string {
return `{"type":"Template","cooked":${serializePrimitives(expr.cooked)},"expressions":${this.serializeExpressions(expr.expressions)}}`;
}

public visitTaggedTemplate(expr: AST.TaggedTemplate): string {
return `{"type":"TaggedTemplate","cooked":${serializePrimitives(expr.cooked)},"raw":${serializePrimitives(expr.cooked.raw)},"expressions":${this.serializeExpressions(expr.expressions)}}`;
}

public visitUnary(expr: AST.Unary): string {
return `{"type":"Unary","operation":"${expr.operation}","expression":${expr.expression.accept(this)}}`;
}

public visitBinary(expr: AST.Binary): string {
return `{"type":"Binary","operation":"${expr.operation}","left":${expr.left.accept(this)},"right":${expr.right.accept(this)}}`;
}

public visitConditional(expr: AST.Conditional): string {
return `{"type":"Conditional","condition":${expr.condition.accept(this)},"yes":${expr.yes.accept(this)},"no":${expr.no.accept(this)}}`;
}

public visitAssign(expr: AST.Assign): string {
return `{"type":"Assign","target":${expr.target.accept(this)},"value":${expr.value.accept(this)}}`;
}

public visitValueConverter(expr: AST.ValueConverter): string {
return `{"type":"ValueConverter","name":"${expr.name}","expression":${expr.expression.accept(this)},"args":${this.serializeExpressions(expr.args)}}`;
}

public visitBindingBehavior(expr: AST.BindingBehavior): string {
return `{"type":"BindingBehavior","name":"${expr.name}","expression":${expr.expression.accept(this)},"args":${this.serializeExpressions(expr.args)}}`;
}

public visitArrayBindingPattern(expr: AST.ArrayBindingPattern): string { throw new Error('visitArrayBindingPattern'); }

public visitObjectBindingPattern(expr: AST.ObjectBindingPattern): string { throw new Error('visitObjectBindingPattern'); }

public visitBindingIdentifier(expr: AST.BindingIdentifier): string { throw new Error('visitBindingIdentifier'); }

public visitHtmlLiteral(expr: AST.HtmlLiteral): string { throw new Error('visitHtmlLiteral'); }

public visitForOfStatement(expr: AST.ForOfStatement): string { throw new Error('visitForOfStatement'); }

public visitInterpolation(expr: AST.Interpolation): string { throw new Error('visitInterpolation'); }

// tslint:disable-next-line:no-any
private serializeExpressions(args: ReadonlyArray<AST.IExpression>): string {
let text = '[';
for (let i = 0, ii = args.length; i < ii; ++i) {
if (i !== 0) {
text += ',';
}
text += args[i].accept(this);
}
text += ']';
return text;
}
}

// tslint:disable-next-line:no-any
function serializePrimitives(values: ReadonlyArray<any>): string {
let text = '[';
for (let i = 0, ii = values.length; i < ii; ++i) {
if (i !== 0) {
text += ',';
}
text += serializePrimitive(values[i]);
}
text += ']';
return text;
}

// tslint:disable-next-line:no-any
function serializePrimitive(value: any): string {
if (typeof value === 'string') {
return `"\\"${escapeString(value)}\\""`;
} else if (value === null || value === undefined) {
return `"${value}"`;
} else {
return `${value}`;
}
}

function escapeString(str: string): string {
let ret = '';
for (let i = 0, ii = str.length; i < ii; ++i) {
ret += escape(str.charAt(i));
}
return ret;
}

function escape(ch: string): string {
switch (ch) {
case '\b': return '\\b';
case '\t': return '\\t';
case '\n': return '\\n';
case '\v': return '\\v';
case '\f': return '\\f';
case '\r': return '\\r';
case '\"': return '\\"';
case '\'': return '\\\'';
case '\\': return '\\\\';
default: return ch;
}
}
72 changes: 64 additions & 8 deletions packages/debug/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ const enum MessageType {

interface IMessageInfo {
message: string;
// tslint:disable-next-line:no-reserved-keywords
type: MessageType;
}

export const Reporter: typeof RuntimeReporter = Object.assign(RuntimeReporter, {
export const Reporter: typeof RuntimeReporter = {...RuntimeReporter,
write(code: number, ...params: any[]): void {
let info = getMessageInfoForCode(code);
const info = getMessageInfoForCode(code);

switch (info.type) {
case MessageType.debug:
Expand All @@ -31,23 +32,22 @@ export const Reporter: typeof RuntimeReporter = Object.assign(RuntimeReporter, {
}
},
error(code: number, ...params: any[]): Error {
let info = getMessageInfoForCode(code);
let error = new Error(info.message);
const info = getMessageInfoForCode(code);
const error = new Error(info.message);
(<any>error).data = params;
return error;
}
});
}};

function getMessageInfoForCode(code: number): IMessageInfo {
return codeLookup[code] || createInvalidCodeMessageInfo(code);
}

function createInvalidCodeMessageInfo(code: number) {
function createInvalidCodeMessageInfo(code: number): IMessageInfo {
return {
type: MessageType.error,
message: `Attempted to report with unknown code ${code}.`
};
};
}

const codeLookup: Record<string, IMessageInfo> = {
0: {
Expand Down Expand Up @@ -145,5 +145,61 @@ const codeLookup: Record<string, IMessageInfo> = {
31: {
type: MessageType.error,
message: 'There are more target instructions than there are targets.'
},
100: {
type: MessageType.error,
message: 'Invalid start of expression.'
},
101: {
type: MessageType.error,
message: 'Unconsumed token.'
},
102: {
type: MessageType.error,
message: 'Double dot and spread operators are not supported.'
},
103: {
type: MessageType.error,
message: 'Invalid member expression.'
},
104: {
type: MessageType.error,
message: 'Unexpected end of expression.'
},
105: {
type: MessageType.error,
message: 'Expected identifier.'
},
106: {
type: MessageType.error,
message: 'Invalid BindingIdentifier at left hand side of "of".'
},
107: {
type: MessageType.error,
message: 'Invalid or unsupported property definition in object literal.'
},
108: {
type: MessageType.error,
message: 'Unterminated quote in string literal.'
},
109: {
type: MessageType.error,
message: 'Unterminated template string.'
},
110: {
type: MessageType.error,
message: 'Missing expected token.'
},
111: {
type: MessageType.error,
message: 'Unexpected character.'
},
150: {
type: MessageType.error,
message: 'Left hand side of expression is not assignable.'
},
151: {
type: MessageType.error,
message: 'Unexpected keyword "of"'
}
};
Loading

0 comments on commit a0c9b0e

Please sign in to comment.