name:
src/main/js/execution/TranspilationVisitor.ts
-rw-r--r--
17039
1import { isNotNull, isNotUndefined } from "../utils/Other";
2import { f7ExceptionFromString } from "../errors/ExceptionHelpers";
3import { ParseException } from "../errors/ParseException";
4import { RefException } from "../errors/RefException";
5import { ColumnRowKey } from "../models/common/ColumnRowKey";
6import { BinaryOperationNode } from "../models/nodes/BinaryOperationNode";
7import { CellQuery } from "../models/nodes/CellQuery";
8import { ErrorNode } from "../models/nodes/ErrorNode";
9import { FormulaNode } from "../models/nodes/FormulaNode";
10import { ListNode } from "../models/nodes/ListNode";
11import { LogicalNode } from "../models/nodes/LogicalNode";
12import { MultiRangeNode } from "../models/nodes/MultiRangeNode";
13import { Node } from "../models/nodes/Node";
14import { NumberNode } from "../models/nodes/NumberNode";
15import { RangeNode } from "../models/nodes/RangeNode";
16import { TextNode } from "../models/nodes/TextNode";
17import { UnaryMinusOperationNode } from "../models/nodes/UnaryMinusOperationNode";
18import { UnaryPercentOperationNode } from "../models/nodes/UnaryPercentOperationNode";
19import { UnaryPlusOperationNode } from "../models/nodes/UnaryPlusOperationNode";
20import { VariableNode } from "../models/nodes/VariableNode";
21import { AlphaUtils } from "../utils/AlphaUtils";
22import { Compare } from "../utils/Compare";
23import { Numbers } from "../utils/Numbers";
24import { F7Visitor } from "../antlr/F7Visitor";
25
26/**
27 * The node visitor transpiles all valid F7 code, by recursively visiting each node.
28 */
29export class TranspilationVisitor extends F7Visitor {
30 /**
31 * Variable names (grid names, named ranges, etc.) have a maximum length of 255 characters. This type of limit is
32 * difficult and messy to enforce in G4 grammars, so we enforce it here.
33 */
34 private static MAX_VARIABLE_NAME_LENGTH = 255;
35
36 /**
37 * Visit parenthetical expression by passing the wrapped expression to the generic visitor.
38 *
39 * @param ctx - context node containing expression.
40 * @return Atom expression once fully executed or "resolved".
41 */
42 visitParentheticalAtom(ctx: any) {
43 return this.visit(ctx.expression());
44 }
45
46 /**
47 * Visit the unary minus expression.
48 *
49 * @param ctx - holding the expression that we will apply the unary operator to.
50 * @return UnaryMinusOperationNode
51 */
52 visitUnaryMinusExpression(ctx: any) {
53 const value = this.visit(ctx.expression());
54 return new UnaryMinusOperationNode(value);
55 }
56
57 /**
58 * Visit the unary plus expression.
59 *
60 * @param ctx - holding the expression that we will apply the unary operator to.
61 * @return UnaryPlusOperationNode
62 */
63 visitUnaryPlusExpression(ctx: any) {
64 const value = this.visit(ctx.expression());
65 return new UnaryPlusOperationNode(value);
66 }
67
68 /**
69 * Visit the unary percent expression, but ONLY if we have a single percent.
70 *
71 * @param ctx - holding the expression that will be percent-ed.
72 * @return UnaryPercentOperationNode
73 */
74 visitUnaryPercentExpression(ctx: any) {
75 if (ctx.children.length > 2) {
76 throw new ParseException();
77 }
78 const value = this.visit(ctx.expression());
79 return new UnaryPercentOperationNode(value);
80 }
81
82 /**
83 * Visit number atom not to convert to double.
84 *
85 * @param ctx - holding the raw string that should conform to what java can parse as a double. Although many
86 * spreadsheets allow for the parsing numbers with commas and currency, it should be handled in formulas,
87 * not in the executor.
88 * @return NumberAtom.
89 */
90 visitNumberAtom(ctx: any) {
91 return new NumberNode(Numbers.toNumberOrNull(ctx.getText()));
92 }
93
94 /**
95 * Finding an error literal returns it instead of throwing it. In most formulas, finding an error will throw it,
96 * stopping the execution of a formula. But not all formulas. Some formulas check the type to serve some other
97 * purpose. For example they check to see if it's NA or to execute something else.
98 *
99 * @param ctx - context node that is an error atom.
100 * @return Error
101 */
102 visitErrorAtom(ctx: any) {
103 return new ErrorNode(f7ExceptionFromString(ctx.getText().toString().toUpperCase()));
104 }
105
106 /**
107 * Visit the concatenation expression.
108 *
109 * @param ctx - holding the values we're concatenating.
110 * @return BinaryOperationNode
111 */
112 visitConcatExpression(ctx: any) {
113 return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
114 }
115
116 /**
117 * Visit additive expression node by executing addition/subtraction operation on left and right variables.
118 *
119 * @param ctx - context containing the operator and left and right operands.
120 * @return Double resulting from the addition/subtraction operation.
121 */
122 visitAdditiveExpression(ctx: any) {
123 return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
124 }
125
126 /**
127 * Visit multiplication expression node by executing multiplication operation on left and right variables.
128 *
129 * @param ctx - context containing the operator and left and right operands.
130 * @return NumberAtom resulting from the multiplication operation.
131 */
132 visitMultiplicationExpression(ctx: any) {
133 return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
134 }
135
136 /**
137 * Visit relational expression node by executing relational checking operation on left and right variables.
138 *
139 * @param ctx - context containing the operator and left and right operands.
140 * @return Boolean resulting from the relational operation.
141 */
142 visitRelationalExpression(ctx: any) {
143 return new BinaryOperationNode(this.visit(ctx.left), ctx.op.getText(), this.visit(ctx.right));
144 }
145
146 /**
147 * Visit power expression by executing the exponential function on the left and right variables.
148 *
149 * @param ctx - holding the left and right nodes.
150 * @return NumberAtom that is the result of the power operation.
151 */
152 visitPowerExpression(ctx: any) {
153 return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
154 }
155
156 /**
157 * Visit string atom node by parsing it into a string value that we would expect. Instead of ""This is my string"", it
158 * should be "This is my string" for example.
159 *
160 * @param ctx - context for this atom.
161 * @return String.
162 */
163 visitStringAtom(ctx: any) {
164 let stringInProgress: string = ctx.getText();
165 // Remove the first and last character, which are quote characters, and strip quotes listAtomFrom remaining string.
166 stringInProgress = stringInProgress
167 .substring(1, stringInProgress.length - 1)
168 .replace('""', '"');
169 return new TextNode(stringInProgress);
170 }
171
172 /**
173 * Visit the list atom by creating a new list, and visiting all children to build the list. A list can have two types
174 * of separators: the comma (,) and the semi-colon (;).
175 * COMMA: This indicates that we're adding another value to the existing list.
176 * SEMI-COLON: THis indicates that we're terminating the existing list, and starting a new one.
177 *
178 * @param ctx - context containing all children we need to visit.
179 * @return ListAtom.
180 */
181 visitListAtom(ctx: any) {
182 const master = new ListNode();
183 let next = new ListNode();
184 for (let i = 0; i < ctx.children.length; i++) {
185 const child = ctx.children[i];
186 const childResult = child.accept(this);
187 if (isNotNull(childResult) && isNotUndefined(childResult)) {
188 if (childResult instanceof ListNode) {
189 next.grid.addGridToRight((childResult as ListNode).grid);
190 } else {
191 next.grid.addOneToRight(childResult);
192 }
193 }
194 if (child.getText() === ";") {
195 master.grid.addGridToBottom(next.grid);
196 next = new ListNode();
197 }
198 }
199 if (!next.grid.isEmpty()) {
200 if (master.isEmpty()) {
201 return next;
202 }
203 master.grid.addGridToBottom(next.grid);
204 }
205
206 if (master.grid.getColumns() === 0 && master.grid.getRows() === 0) {
207 return new ErrorNode(new RefException("Range does not exist."));
208 }
209 return master;
210 }
211
212 /**
213 * An expression that is just a variable is a valid expression, but it just resolves the variable.
214 *
215 * @param ctx - context node holding just the atom.
216 * @return - Atom once visited.
217 */
218 visitAtomExpression(ctx: any) {
219 return this.visit(ctx.atom());
220 }
221
222 /**
223 * Visit named atom node by pulling the identifier text and creating a variable node.
224 *
225 * @param ctx - context for this atom holding the identifier.
226 * @return variable node
227 */
228 visitNamedAtom(ctx: any) {
229 const identifier: string = ctx.identifier().getText();
230 if (identifier.length > TranspilationVisitor.MAX_VARIABLE_NAME_LENGTH) {
231 return new ErrorNode(new ParseException());
232 }
233 // If it's case insensitively true or false, return logical node.
234 const upper = identifier.toUpperCase();
235 if (upper === "TRUE" || upper === "FALSE") {
236 return new LogicalNode(upper === "TRUE");
237 }
238 return new VariableNode(identifier);
239 }
240
241 /**
242 * Visit a formula atom node by treating the arguments in the same way that we would a list, finally calling the
243 * formula with the arguments.
244 *
245 * @param ctx - context node for the formula.
246 * @return node representing executed formula.
247 */
248 visitFormulaAtom(ctx: any): Node {
249 const formulaString: string = ctx.name.getText();
250 const args: Array<Node> = [];
251 ctx
252 .arguments()
253 .expression()
254 .forEach((child: any) => {
255 const childResult = child.accept(this);
256 args.push(childResult);
257 });
258 return new FormulaNode(formulaString, args);
259 }
260
261 /**
262 * Visit a range expression by visiting all children, and gather them into a list to pass to the range. Node validity
263 * will be check in executor (you can have nodes that are named ranges, and regular ranges for example, but not
264 * numbers, or errors, etc).
265 * @param ctx - context node for range.
266 */
267 visitRangeExpression(ctx: any) {
268 const nodes: Array<Node> = [];
269 ctx.children.forEach((child: any) => {
270 const childResult = child.accept(this);
271 if (isNotNull(childResult) && isNotUndefined(childResult)) {
272 nodes.push(childResult);
273 }
274 });
275 return new MultiRangeNode(nodes);
276 }
277
278 /**
279 * Eg: Grid!A1:D1 or A1:D1.
280 *
281 * @param ctx - holds start and end rows and columns, and maybe grid name.
282 * @return RangeNode.
283 */
284 visitBiRange(ctx: any): Node {
285 let first = new ColumnRowKey(
286 AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase()),
287 AlphaUtils.rowToInt(ctx.firstRow.text)
288 );
289 let second = new ColumnRowKey(
290 AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase()),
291 AlphaUtils.rowToInt(ctx.lastRow.text)
292 );
293
294 if (first.compareTo(second) === 1) {
295 const swap = first;
296 first = second;
297 second = swap;
298 }
299
300 const builder = CellQuery.builder()
301 .columnsBetween(first.column, second.column)
302 .rowsBetween(first.row, second.row);
303 if (isNotNull(ctx.grid)) {
304 builder.setSheet(ctx.grid.getText());
305 }
306 return new RangeNode(builder.build());
307 }
308
309 /**
310 * Eg: Grid!A1 or A1.
311 *
312 * @param ctx - holds single row and column, and maybe grid name.
313 * @return RangeNode
314 */
315 visitUniRange(ctx: any): Node {
316 const builder = CellQuery.builder()
317 .columnsBetween(
318 AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase()),
319 AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase())
320 )
321 .rowsBetween(AlphaUtils.rowToInt(ctx.firstRow.text), AlphaUtils.rowToInt(ctx.firstRow.text));
322 if (isNotNull(ctx.grid)) {
323 builder.setSheet(ctx.grid.getText());
324 }
325 return new RangeNode(builder.build());
326 }
327
328 /**
329 * Eg: Grid!A:D or A:D.
330 *
331 * @param ctx - holds start column and end column, and maybe grid name.
332 * @return RangeNode
333 */
334 visitColumnWiseBiRange(ctx: any): Node {
335 let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
336 let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
337 if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
338 const swap = firstColumn;
339 firstColumn = secondColumn;
340 secondColumn = swap;
341 }
342 const builder = CellQuery.builder()
343 .openRowsStartingAtZero()
344 .columnsBetween(firstColumn, secondColumn);
345 if (isNotNull(ctx.grid)) {
346 builder.setSheet(ctx.grid.getText());
347 }
348 return new RangeNode(builder.build());
349 }
350
351 /**
352 * Eg: Grid!A2:D or A2:D.
353 *
354 * @param ctx - holds start column and end column, and an offset row, and maybe grid name.
355 * @return RangeNode
356 */
357 visitColumnWiseWithRowOffsetFirstBiRange(ctx: any): Node {
358 let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
359 let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
360 if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
361 const swap = firstColumn;
362 firstColumn = secondColumn;
363 secondColumn = swap;
364 }
365 const builder = CellQuery.builder()
366 .openRowsStartingAt(ctx.firstRow.text)
367 .columnsBetween(firstColumn, secondColumn);
368 if (isNotNull(ctx.grid)) {
369 builder.setSheet(ctx.grid.getText());
370 }
371 return new RangeNode(builder.build());
372 }
373
374 /**
375 * Eg: Grid!A:D2 or A:D2.
376 *
377 * @param ctx - holds start and end columns, row offset, and maybe grid name.
378 * @return RangeNode.
379 */
380 visitColumnWiseWithRowOffsetLastBiRange(ctx: any): Node {
381 let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
382 let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
383 if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
384 const swap = firstColumn;
385 firstColumn = secondColumn;
386 secondColumn = swap;
387 }
388 const builder = CellQuery.builder()
389 .openRowsStartingAt(ctx.lastRow.text)
390 .columnsBetween(firstColumn, secondColumn);
391 if (isNotNull(ctx.grid)) {
392 builder.setSheet(ctx.grid.getText());
393 }
394 return new RangeNode(builder.build());
395 }
396
397 /**
398 * Eg: Grid!2:4 or 2:4.
399 *
400 * @param ctx - holds start and end rows, maybe a grid name.
401 * @return RangeNode
402 */
403 visitRowWiseBiRange(ctx: any): Node {
404 let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
405 let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
406 if (Compare.numberComparison(firstRow, secondRow) >= 1) {
407 const swap = firstRow;
408 firstRow = secondRow;
409 secondRow = swap;
410 }
411 const builder = CellQuery.builder()
412 .openColumnsStartingAtZero()
413 .rowsBetween(firstRow, secondRow);
414 if (isNotNull(ctx.grid)) {
415 builder.setSheet(ctx.grid.getText());
416 }
417 return new RangeNode(builder.build());
418 }
419
420 /**
421 * Eg: Grid!A2:4 or A2:4.
422 *
423 * @param ctx - holds start end end rows, and a column offset, and maybe a grid name.
424 * @return RangeNode
425 */
426 visitRowWiseWithColumnOffsetFirstBiRange(ctx: any): Node {
427 let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
428 let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
429 if (Compare.numberComparison(firstRow, secondRow) >= 1) {
430 const swap = firstRow;
431 firstRow = secondRow;
432 secondRow = swap;
433 }
434 const builder = CellQuery.builder()
435 .openColumnsStartingAt(ctx.firstColumn.text.toUpperCase())
436 .rowsBetween(firstRow, secondRow);
437 if (isNotNull(ctx.grid)) {
438 builder.setSheet(ctx.grid.getText());
439 }
440 return new RangeNode(builder.build());
441 }
442
443 /**
444 * Eg: Grid!2:D4 or 2:D4.
445 *
446 * @param ctx - holds start and end row, column offset, and maybe a grid name.
447 * @return RangeNode
448 */
449 visitRowWiseWithColumnOffsetLastBiRange(ctx: any): Node {
450 let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
451 let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
452 if (Compare.numberComparison(firstRow, secondRow) >= 1) {
453 const swap = firstRow;
454 firstRow = secondRow;
455 secondRow = swap;
456 }
457 const builder = CellQuery.builder()
458 .openColumnsStartingAt(ctx.lastColumn.text.toUpperCase())
459 .rowsBetween(firstRow, secondRow);
460 if (isNotNull(ctx.grid)) {
461 builder.setSheet(ctx.grid.getText());
462 }
463 return new RangeNode(builder.build());
464 }
465
466 /**
467 * Recursively visit children.
468 * @param ctx - context node containing any and all children.
469 */
470 visitChildren(ctx: any) {
471 if (!ctx) {
472 return;
473 }
474
475 if (ctx.children && ctx.children.length > 0) {
476 let result = this.defaultResult();
477 for (let i = 0; i < ctx.children.length; i++) {
478 const childResult = ctx.children[i].accept(this);
479 result = this.aggregateResult(result, childResult);
480 }
481 return result;
482 }
483 }
484
485 visit(tree: any) {
486 return tree.accept(this);
487 }
488
489 protected aggregateResult(aggregate: Node, nextResult: Node): Node {
490 return nextResult;
491 }
492
493 protected defaultResult(): Node {
494 return undefined;
495 }
496}