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
Next Next commit
WIP: associative operators without parens
  • Loading branch information
sc1f committed Jun 23, 2020
commit 241259df5225e399678c6e0243634ac267d390ae
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class PerspectiveComputedExpressionParser {
throw new Error(message);
}

console.log(cst);

return this._visitor.visit(cst);
}

Expand Down Expand Up @@ -343,13 +345,7 @@ class PerspectiveComputedExpressionParser {
}

Expression(ctx, computed_columns) {
if (ctx.OperatorComputedColumn) {
this.visit(ctx.OperatorComputedColumn, computed_columns);
} else if (ctx.FunctionComputedColumn) {
this.visit(ctx.FunctionComputedColumn, computed_columns);
} else {
return;
}
return this.visit(ctx.OperatorComputedColumn, computed_columns);
}

/**
Expand All @@ -359,38 +355,151 @@ class PerspectiveComputedExpressionParser {
* @param {*} ctx
*/
OperatorComputedColumn(ctx, computed_columns) {
console.log("operator", ctx);
// TODO: collapse body
let left = this.visit(ctx.left, computed_columns);

if (typeof left === "undefined") {
left = computed_columns[computed_columns.length - 1].column;
}
let final_column_name;

let operator = this.visit(ctx.Operator);
if (ctx.right) {
let previous;

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

if (!operator) {
return;
}

let right = this.visit(rhs, computed_columns);

// If there is a previous value, use it, otherwise use
// the leftmost value. This enables expressions such as
// a + b / c * d + e ... ad infinitum
const left_hand = previous ? previous : left;

console.log(left_hand, operator, right);

// Use custom name if provided through `AS/as/As`
const as = this.visit(ctx.as);
const column_name = as ? as : COMPUTED_FUNCTION_FORMATTERS[operator](left_hand, right);

computed_columns.push({
column: column_name,
computed_function_name: operator,
inputs: [left_hand, right]
});

previous = column_name;
});

final_column_name = previous;
} else {
// If there are no more right-hand tokens, return the
// column name so it can be used as the tree traversal
// goes upwards.
final_column_name = left;
}

let right = this.visit(ctx.right, computed_columns);
return final_column_name;
}

AdditionOperatorComputedColumn(ctx, computed_columns) {
console.log("addition", ctx);
let left = this.visit(ctx.left, computed_columns);

let final_column_name;

if (ctx.right) {
let previous;

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

if (!operator) {
return;
}

let right = this.visit(rhs, computed_columns);

// If there is a previous value, use it, otherwise use
// the leftmost value. This enables expressions such as
// a + b / c * d + e ... ad infinitum
const left_hand = previous ? previous : left;

if (typeof right === "undefined") {
right = computed_columns[computed_columns.length - 1].column;
console.log(left_hand, operator, right);

// Use custom name if provided through `AS/as/As`
const as = this.visit(ctx.as);
const column_name = as ? as : COMPUTED_FUNCTION_FORMATTERS[operator](left_hand, right);

computed_columns.push({
column: column_name,
computed_function_name: operator,
inputs: [left_hand, right]
});

previous = column_name;
});

final_column_name = previous;
} else {
// If there are no more right-hand tokens, return the
// column name so it can be used as the tree traversal
// goes upwards.
final_column_name = left;
}

let as = this.visit(ctx.as);
return final_column_name;
}

MultiplicationOperatorComputedColumn(ctx, computed_columns) {
console.log("multiply", ctx);
let left = this.visit(ctx.left, computed_columns);

let column_name = COMPUTED_FUNCTION_FORMATTERS[operator](left, right);
let final_column_name;

// Use custom name if provided through `AS/as/As`
if (as) {
column_name = as;
if (ctx.right) {
let previous;

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

if (!operator) {
return;
}

let right = this.visit(rhs, computed_columns);

// If there is a previous value, use it, otherwise use
// the leftmost value. This enables expressions such as
// a + b / c * d + e ... ad infinitum
const left_hand = previous ? previous : left;

console.log(left_hand, operator, right);

// Use custom name if provided through `AS/as/As`
const as = this.visit(ctx.as);
const column_name = as ? as : COMPUTED_FUNCTION_FORMATTERS[operator](left_hand, right);

computed_columns.push({
column: column_name,
computed_function_name: operator,
inputs: [left_hand, right]
});

previous = column_name;
});

final_column_name = previous;
} else {
// If there are no more right-hand tokens, return the
// column name so it can be used as the tree traversal
// goes upwards.
final_column_name = left;
}

computed_columns.push({
column: column_name,
computed_function_name: operator,
inputs: [left, right]
});
return final_column_name;
}

/**
Expand Down Expand Up @@ -419,11 +528,13 @@ class PerspectiveComputedExpressionParser {

const as = this.visit(ctx.as);

let column_name = COMPUTED_FUNCTION_FORMATTERS[fn](...input_columns);
let column_name;

// Use custom name if provided through `AS/as/As`
if (as) {
column_name = as;
} else {
column_name = COMPUTED_FUNCTION_FORMATTERS[fn](...input_columns);
}

const computed = {
Expand All @@ -433,6 +544,9 @@ class PerspectiveComputedExpressionParser {
};

computed_columns.push(computed);

// Return the column name so it can be used up the chain
return column_name;
}

/**
Expand Down
47 changes: 27 additions & 20 deletions packages/perspective-viewer/src/js/computed_expressions/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,24 @@ export class ComputedExpressionColumnParser extends CstParser {
});

this.RULE("Expression", () => {
this.OR(
[
{
ALT: () => {
this.SUBRULE(this.OperatorComputedColumn);
}
},
{
ALT: () => {
this.SUBRULE(this.FunctionComputedColumn);
}
}
],
{
ERR_MSG: "Expected an expression of the form `x + y` or `func(x)`."
}
);
// Preserve operator precedence amongst mathematical operations
this.SUBRULE(this.AdditionOperatorComputedColumn);
});

this.RULE("AdditionOperatorComputedColumn", () => {
this.SUBRULE(this.ColumnName, {LABEL: "left"});
this.MANY(() => {
this.SUBRULE(this.Operator);
this.SUBRULE2(this.MultiplicationOperatorComputedColumn, {LABEL: "right"});
});
this.OPTION(() => {
this.SUBRULE(this.As, {LABEL: "as"});
});
});

this.RULE("OperatorComputedColumn", () => {
this.RULE("MultiplicationOperatorComputedColumn", () => {
this.SUBRULE(this.ColumnName, {LABEL: "left"});
this.AT_LEAST_ONE(() => {
this.MANY(() => {
this.SUBRULE(this.Operator);
this.SUBRULE2(this.ColumnName, {LABEL: "right"});
});
Expand All @@ -50,6 +46,17 @@ export class ComputedExpressionColumnParser extends CstParser {
});
});

// this.RULE("OperatorComputedColumn", () => {
// this.SUBRULE(this.ColumnName, {LABEL: "left"});
// this.AT_LEAST_ONE(() => {
// this.SUBRULE(this.Operator);
// this.SUBRULE2(this.ColumnName, {LABEL: "right"});
// });
// this.OPTION(() => {
// this.SUBRULE(this.As, {LABEL: "as"});
// });
// });

this.RULE("FunctionComputedColumn", () => {
this.SUBRULE(this.Function);
this.CONSUME(vocabulary["leftParen"]);
Expand All @@ -66,7 +73,7 @@ export class ComputedExpressionColumnParser extends CstParser {
});

this.RULE("ColumnName", () => {
this.OR([{ALT: () => this.SUBRULE(this.ParentheticalExpression)}, {ALT: () => this.CONSUME(vocabulary["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."
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,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
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe("Computed Expression Parser", function() {
expect(parsed).toEqual(expected);
});

it.skip("Should parse an operator notation expression with associativity", function() {
it("Should parse an operator notation expression with associativity", function() {
const expected = [
{
column: "(w + x)",
Expand All @@ -58,6 +58,28 @@ describe("Computed Expression Parser", function() {
expect(parsed).toEqual(expected);
});

it("Should parse an operator notation expression with associativity and operator precedence", function() {
const expected = [
{
column: "(w * x)",
computed_function_name: "*",
inputs: ["w", "x"]
},
{
column: "(z / abc)",
computed_function_name: "/",
inputs: ["w", "abc"]
},
{
column: "((w * x) + (z / abc))",
computed_function_name: "+",
inputs: ["(w + x)", "(z / abc)"]
}
];
const parsed = COMPUTED_EXPRESSION_PARSER.parse('"w" * "x" + "z" / "abc"');
expect(parsed).toEqual(expected);
});

it("Should parse an operator notation expression named with 'AS'", function() {
const expected = [
{
Expand Down