Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Computed expressions respect left-to-right associativity and operator precedence #1090

Merged
merged 13 commits into from
Jun 24, 2020
Merged
Prev Previous commit
Next Next commit
WIP: actually actually associative operators
  • Loading branch information
sc1f committed Jun 23, 2020
commit 353938ae06705b686ccd7f3a316242641112bf2f
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ class PerspectiveComputedExpressionParser {
*/
SuperExpression(ctx) {
let computed_columns = [];
console.log(ctx);

this.visit(ctx.Expression, computed_columns);

Expand All @@ -366,14 +367,15 @@ class PerspectiveComputedExpressionParser {
}

/**
* All operator-type computed columns share the same parsing
* logic.
* Common logic for parsing through a computed column in operator
* syntax, with `get_operator` returning an operator of the
* correct type, which is important for associativity.
*
* @param {*} ctx
* @param {Array[Object]} computed_columns an array of computed
* column definitions that are generated from this expression.
* @param {*} computed_columns
* @param {*} get_operator
*/
_VisitOperatorComputedColumn(ctx, computed_columns) {
_VisitOperatorComputedColumn(ctx, computed_columns, get_operator) {
let left = this.visit(ctx.left, computed_columns);

let final_column_name;
Expand All @@ -382,7 +384,7 @@ class PerspectiveComputedExpressionParser {
let previous;

ctx.right.forEach((rhs, idx) => {
let operator = this.visit(ctx.Operator[idx]);
let operator = get_operator(ctx, idx);

if (!operator) {
return;
Expand Down Expand Up @@ -426,15 +428,18 @@ class PerspectiveComputedExpressionParser {
* @param {*} ctx
*/
OperatorComputedColumn(ctx, computed_columns) {
return this._VisitOperatorComputedColumn(ctx, computed_columns);
const get_operator = (ctx, idx) => this.visit(ctx.Operator[idx]);
return this._VisitOperatorComputedColumn(ctx, computed_columns, get_operator);
}

AdditionOperatorComputedColumn(ctx, computed_columns) {
return this._VisitOperatorComputedColumn(ctx, computed_columns);
const get_operator = (ctx, idx) => this.visit(ctx.AdditionOperator[idx]);
return this._VisitOperatorComputedColumn(ctx, computed_columns, get_operator);
}

MultiplicationOperatorComputedColumn(ctx, computed_columns) {
return this._VisitOperatorComputedColumn(ctx, computed_columns);
const get_operator = (ctx, idx) => this.visit(ctx.MultiplicationOperator[idx]);
return this._VisitOperatorComputedColumn(ctx, computed_columns, get_operator);
}

/**
Expand Down Expand Up @@ -504,20 +509,28 @@ class PerspectiveComputedExpressionParser {
return ctx.columnName[0].payload;
}

/**
* Parse a single mathematical operator (+, -, *, /, %).
* @param {*} ctx
*/
Operator(ctx) {
AdditionOperator(ctx) {
if (ctx.add) {
return ctx.add[0].image;
} else if (ctx.subtract) {
return ctx.subtract[0].image;
} else if (ctx.multiply) {
}
}

MultiplicationOperator(ctx) {
if (ctx.multiply) {
return ctx.multiply[0].image;
} else if (ctx.divide) {
return ctx.divide[0].image;
} else if (ctx.pow) {
}
}

/**
* Parse a single mathematical operator (+, -, *, /, %).
* @param {*} ctx
*/
Operator(ctx) {
if (ctx.pow) {
return ctx.pow[0].image;
} else if (ctx.percent_of) {
return ctx.percent_of[0].image;
Expand Down
38 changes: 23 additions & 15 deletions packages/perspective-viewer/src/js/computed_expressions/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class ComputedExpressionColumnParser extends CstParser {
* it has lower precedence compared to the rules that are to follow.
*/
this.RULE("OperatorComputedColumn", () => {
this.SUBRULE(this.ColumnName, {LABEL: "left"});
this.SUBRULE(this.AdditionOperatorComputedColumn, {LABEL: "left"});

// 0...n operators and right-hand expressions are available here.
// Though a single column name is syntactically valid, it does
Expand All @@ -66,9 +66,9 @@ export class ComputedExpressionColumnParser extends CstParser {
* evaluator logic is the same.
*/
this.RULE("AdditionOperatorComputedColumn", () => {
this.SUBRULE(this.ColumnName, {LABEL: "left"});
this.SUBRULE(this.MultiplicationOperatorComputedColumn, {LABEL: "left"});
this.MANY(() => {
this.SUBRULE(this.Operator);
this.SUBRULE(this.AdditionOperator);
this.SUBRULE2(this.MultiplicationOperatorComputedColumn, {LABEL: "right"});
this.OPTION(() => {
this.SUBRULE(this.As, {LABEL: "as"});
Expand All @@ -84,14 +84,25 @@ export class ComputedExpressionColumnParser extends CstParser {
this.RULE("MultiplicationOperatorComputedColumn", () => {
this.SUBRULE(this.ColumnName, {LABEL: "left"});
this.MANY(() => {
this.SUBRULE(this.Operator);
this.SUBRULE(this.MultiplicationOperator);
this.SUBRULE2(this.ColumnName, {LABEL: "right"});
this.OPTION(() => {
this.SUBRULE(this.As, {LABEL: "as"});
});
});
});

/**
* A column name, which can evaluate to a parenthetical expression,
* a functional column, or a literal column name - a string
* wrapped in double or single quotes.
*/
this.RULE("ColumnName", () => {
this.OR([{ALT: () => this.SUBRULE(this.ParentheticalExpression)}, {ALT: () => this.SUBRULE(this.FunctionComputedColumn)}, {ALT: () => this.CONSUME(vocabulary["columnName"])}], {
ERR_MSG: "Expected a column name (wrapped in double quotes) or a parenthesis-wrapped expression."
});
});

/**
* A computed column in `f(x)` notation. It is evaluated before all
* operator computed columns.
Expand All @@ -114,17 +125,6 @@ export class ComputedExpressionColumnParser extends CstParser {
});
});

/**
* A column name, which can evalue to a parenthetical expression,
* a functional column, or a literal column name - a string
* wrapped in double or single quotes.
*/
this.RULE("ColumnName", () => {
this.OR([{ALT: () => this.SUBRULE(this.ParentheticalExpression)}, {ALT: () => this.SUBRULE(this.FunctionComputedColumn)}, {ALT: () => this.CONSUME(vocabulary["columnName"])}], {
ERR_MSG: "Expected a column name (wrapped in double quotes) or a parenthesis-wrapped expression."
});
});

/**
* A special rule for column names used as alias after `as` to prevent
* further evaluation of possible expressions.
Expand Down Expand Up @@ -173,6 +173,14 @@ export class ComputedExpressionColumnParser extends CstParser {
]);
});

this.RULE("AdditionOperator", () => {
this.OR([{ALT: () => this.CONSUME(vocabulary["add"])}, {ALT: () => this.CONSUME(vocabulary["subtract"])}]);
});

this.RULE("MultiplicationOperator", () => {
this.OR([{ALT: () => this.CONSUME(vocabulary["multiply"])}, {ALT: () => this.CONSUME(vocabulary["divide"])}]);
});

this.RULE("Operator", () => {
this.OR([
{ALT: () => this.CONSUME(vocabulary["add"])},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ class ComputedExpressionWidget extends HTMLElement {
return;
}

console.log("Parsed", this._parsed_expression);

// Take the parsed expression and type check it on the viewer,
// which will call `_type_check_expression()` with a computed_schema.
const event = new CustomEvent("perspective-computed-expression-type-check", {
Expand Down