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