Skip to content

Commit

Permalink
Merge pull request #259 from MarkusAmshove/natparse/parse-input
Browse files Browse the repository at this point in the history
Partially parse INPUT statements
  • Loading branch information
MarkusAmshove authored May 21, 2023
2 parents ab9fedd + af62dc5 commit 60b43ad
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 13 deletions.
6 changes: 3 additions & 3 deletions docs/implemented-statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ This document tracks the implementation status of Natural statements.

Legend:

:x: - not implemented (57)
:x: - not implemented (56)

:white_check_mark: - implemented or reporting (55)

partial - partially implemented to prevent false positives (10)
partial - partially implemented to prevent false positives (11)

| Statement | Status |
| --- |--------------------|
Expand Down Expand Up @@ -68,7 +68,7 @@ partial - partially implemented to prevent false positives (10)
| IF SELECTION | :white_check_mark: |
| IGNORE | :white_check_mark: |
| INCLUDE | :white_check_mark: |
| INPUT | :x: |
| INPUT | :partial: |
| INSERT (SQL) | partial |
| INTERFACE | :x: |
| LIMIT | :white_check_mark: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.amshove.natparse.natural;

import org.amshove.natparse.ReadOnlyList;

public interface IInputStatementNode extends IStatementNode
{
ReadOnlyList<IOperandNode> operands();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.amshove.natparse.parsing;

import org.amshove.natparse.ReadOnlyList;
import org.amshove.natparse.natural.IInputStatementNode;
import org.amshove.natparse.natural.IOperandNode;

import java.util.ArrayList;
import java.util.List;

class InputStatementNode extends StatementNode implements IInputStatementNode
{
private final List<IOperandNode> operands = new ArrayList<>();

@Override
public ReadOnlyList<IOperandNode> operands()
{
return ReadOnlyList.from(operands);
}

void addOperand(IOperandNode operand)
{
if (operand == null)
{
// stuff like tab setting, line skip etc.
return;
}

operands.add(operand);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ private StatementListNode statementList(Set<SyntaxKind> endTokenKinds)
case HISTOGRAM:
statementList.addStatement(histogram());
break;
case INPUT:
statementList.addStatement(inputStatement());
break;
case SELECT:
statementList.addStatement(select());
break;
Expand Down Expand Up @@ -1722,6 +1725,125 @@ private StatementNode display() throws ParseError
return display;
}

private StatementNode inputStatement() throws ParseError
{
var input = new InputStatementNode();
consumeMandatory(input, SyntaxKind.INPUT);

if (consumeOptionally(input, SyntaxKind.WINDOW))
{
consumeMandatory(input, SyntaxKind.EQUALS_SIGN);
consumeLiteralNode(input, SyntaxKind.STRING_LITERAL);
}

if (consumeOptionally(input, SyntaxKind.NO))
{
consumeMandatory(input, SyntaxKind.ERASE);
}

if (consumeOptionally(input, SyntaxKind.LPAREN))
{
// statement attributes?
while (!isAtEnd() && !peekKind(SyntaxKind.RPAREN))
{
consume(input);
}
consumeMandatory(input, SyntaxKind.RPAREN);
}

if (peekKind(SyntaxKind.WITH) || peekKind(SyntaxKind.TEXT))
{
consumeOptionally(input, SyntaxKind.WITH);
consumeMandatory(input, SyntaxKind.TEXT);

consumeOptionally(input, SyntaxKind.ASTERISK);
consumeOperandNode(input);

if (consumeOptionally(input, SyntaxKind.LPAREN))
{
// statement attributes?
while (!isAtEnd() && !peekKind(SyntaxKind.RPAREN))
{
consume(input);
}
consumeMandatory(input, SyntaxKind.RPAREN);
}

while (consumeOptionally(input, SyntaxKind.COMMA))
{
consumeOperandNode(input);
}
}

if (consumeOptionally(input, SyntaxKind.MARK))
{
if (consumeOptionally(input, SyntaxKind.POSITION))
{
consumeOperandNode(input);
consumeOptionally(input, SyntaxKind.IN);
}

consumeOptionally(input, SyntaxKind.FIELD);
consumeOptionally(input, SyntaxKind.ASTERISK);
consumeOperandNode(input);
}

if (peekKind(SyntaxKind.AND) || peekKind(SyntaxKind.SOUND) || peekKind(SyntaxKind.ALARM))
{
consumeOptionally(input, SyntaxKind.AND);
consumeOptionally(input, SyntaxKind.SOUND);
consumeMandatory(input, SyntaxKind.ALARM);
}

if (peekKind(SyntaxKind.USING) || peekKind(SyntaxKind.MAP))
{
consumeOptionally(input, SyntaxKind.USING);
consumeMandatory(input, SyntaxKind.MAP);
consumeLiteralNode(input, SyntaxKind.STRING_LITERAL);
// TODO: Side load map. Create new type for Input that uses map, so that it can be IModuleReferencingNode ?
if (peekKind(SyntaxKind.NO) && peekKind(1, SyntaxKind.ERASE))
{
consumeMandatory(input, SyntaxKind.NO);
consumeMandatory(input, SyntaxKind.ERASE);
}

if (consumeOptionally(input, SyntaxKind.NO))
{
consumeMandatory(input, SyntaxKind.PARAMETER);
return input;
}
}

while (!isAtEnd() && !isStatementStart())
{
if (peekKind(SyntaxKind.LPAREN) && getKind(1).isAttribute())
{
consumeAttributeDefinition(input);
}
else
{
// coordinates in form of x/y
if (peekKind(SyntaxKind.NUMBER_LITERAL) && peekKind(1, SyntaxKind.SLASH))
{
consumeLiteralNode(input, SyntaxKind.NUMBER_LITERAL);
consumeMandatory(input, SyntaxKind.SLASH);
consumeLiteralNode(input, SyntaxKind.NUMBER_LITERAL);
continue;
}

if ((consumeOptionally(input, SyntaxKind.NO) && consumeOptionally(input, SyntaxKind.PARAMETER))
|| !isOperand() && !peekKind(SyntaxKind.TAB_SETTING) && !peekKind(SyntaxKind.SLASH) && !peekKind(SyntaxKind.OPERAND_SKIP))
{
break;
}

input.addOperand(consumeWriteOperand(input));
}
}

return input;
}

private static final Set<SyntaxKind> OPTIONAL_WRITE_FLAGS = Set.of(SyntaxKind.NOTITLE, SyntaxKind.NOHDR, SyntaxKind.USING, SyntaxKind.MAP, SyntaxKind.FORM, SyntaxKind.TITLE, SyntaxKind.LEFT, SyntaxKind.JUSTIFIED, SyntaxKind.UNDERLINED);

private StatementNode write() throws ParseError
Expand All @@ -1738,7 +1860,7 @@ private StatementNode write() throws ParseError
else
{
// currently consume everything until closing parenthesis to consume things like attribute definition etc.
while (!peekKind(SyntaxKind.RPAREN))
while (!isAtEnd() && !peekKind(SyntaxKind.RPAREN))
{
consume(write);
}
Expand Down Expand Up @@ -1771,31 +1893,32 @@ private StatementNode write() throws ParseError
return write;
}

private void consumeWriteOperand(WriteNode write) throws ParseError
private IOperandNode consumeWriteOperand(BaseSyntaxNode writeLikeNode) throws ParseError
{
if (consumeOptionally(write, SyntaxKind.TAB_SETTING)
|| consumeOptionally(write, SyntaxKind.SLASH)
|| consumeOptionally(write, SyntaxKind.OPERAND_SKIP))
if (consumeOptionally(writeLikeNode, SyntaxKind.TAB_SETTING)
|| consumeOptionally(writeLikeNode, SyntaxKind.SLASH)
|| consumeOptionally(writeLikeNode, SyntaxKind.OPERAND_SKIP))
{
return;
return null;
}

if (peekKind().isLiteralOrConst())
{
consumeLiteralNode(write, SyntaxKind.STRING_LITERAL);
var literal = consumeLiteralNode(writeLikeNode, SyntaxKind.STRING_LITERAL);
if (peekKind(SyntaxKind.LPAREN))
{
// currently consume everything until closing parenthesis to consume attribute definition shorthands
while (!isAtEnd() && !peekKind(SyntaxKind.RPAREN))
{
consume(write);
consume(writeLikeNode);
}
consumeMandatory(write, SyntaxKind.RPAREN);
consumeMandatory(writeLikeNode, SyntaxKind.RPAREN);
}
return literal;
}
else
{
consumeOperandNode(write);
return consumeOperandNode(writeLikeNode);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.amshove.natparse.parsing.statements;

import org.amshove.natparse.natural.IInputStatementNode;
import org.amshove.natparse.natural.ILiteralNode;
import org.amshove.natparse.natural.ITokenNode;
import org.amshove.natparse.natural.IVariableReferenceNode;
import org.amshove.natparse.parsing.StatementParseTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

class InputStatementParsingShould extends StatementParseTest
{
@Test
void parseASimpleInput()
{
var input = assertParsesSingleStatement("INPUT #VAR", IInputStatementNode.class);
assertNodeOperand(input, 0, IVariableReferenceNode.class, "#VAR");
}

@Test
void parseASimpleInputWithMultipleOperands()
{
var input = assertParsesSingleStatement("INPUT #VAR 'Hi' #VAR2", IInputStatementNode.class);
assertNodeOperand(input, 0, IVariableReferenceNode.class, "#VAR");
assertNodeOperand(input, 1, ILiteralNode.class, "'Hi'");
assertNodeOperand(input, 2, IVariableReferenceNode.class, "#VAR2");
}

@Test
void parseAInputWithWindow()
{
assertParsesSingleStatement("INPUT WINDOW='window' 'Hi'", IInputStatementNode.class);
}

@Test
void parseInputWithNoErase()
{
assertParsesSingleStatement("INPUT WINDOW='window' NO ERASE 'Hi'", IInputStatementNode.class);
}

@Test
void consumeStatementAttributes()
{
assertParsesSingleStatement("INPUT (AD=IO) 'Hi'", IInputStatementNode.class);
}

@ParameterizedTest
@ValueSource(strings =
{
"WITH TEXT", "TEXT"
})
void consumeWithText(String permutation)
{
var input = assertParsesSingleStatement("INPUT %s *MSG-INFO.##MSG-NR,#VAR2 'HI'".formatted(permutation), IInputStatementNode.class);
assertThat(input.operands()).hasSize(1);
assertNodeOperand(input, 0, ILiteralNode.class, "'HI'");
}

@ParameterizedTest
@ValueSource(strings =
{
"MARK POSITION 3 IN FIELD #NUMBER", "MARK 3", "MARK #NUMBER", "MARK *#NUMBER"
})
void consumeMark(String permutation)
{
var input = assertParsesSingleStatement("INPUT %s 'HI'".formatted(permutation), IInputStatementNode.class);
assertThat(input.operands()).hasSize(1);
assertNodeOperand(input, 0, ILiteralNode.class, "'HI'");
}

@ParameterizedTest
@ValueSource(strings =
{
"AND SOUND ALARM", "AND ALARM", "SOUND ALARM", "ALARM"
})
void consumeAlarm(String permutation)
{
var input = assertParsesSingleStatement("INPUT %s 'HI'".formatted(permutation), IInputStatementNode.class);
assertThat(input.operands()).hasSize(1);
assertNodeOperand(input, 0, ILiteralNode.class, "'HI'");
}

@ParameterizedTest
@ValueSource(strings =
{
"USING MAP", "MAP"
})
void consumeUsingMap(String permutation)
{
var input = assertParsesSingleStatement("INPUT %s 'Map' 'HI'".formatted(permutation), IInputStatementNode.class);
assertThat(input.operands()).hasSize(1);
assertNodeOperand(input, 0, ILiteralNode.class, "'HI'");
}

@Test
void consumeUsingMapWithoutParameter()
{
var input = assertParsesSingleStatement("INPUT USING MAP 'Map' NO PARAMETER", IInputStatementNode.class);
assertThat(input.operands()).isEmpty();
}

@Test
void consumeInputsWithPositions()
{
var input = assertParsesSingleStatement("INPUT 'Hi' 10/15 'Ho' 20/20 #VAR", IInputStatementNode.class);
assertThat(input.operands()).hasSize(3);
assertNodeOperand(input, 0, ILiteralNode.class, "'Hi'");
assertNodeOperand(input, 1, ILiteralNode.class, "'Ho'");
assertNodeOperand(input, 2, IVariableReferenceNode.class, "#VAR");
}

@Test
void consumeTabsAndSkips()
{
var input = assertParsesSingleStatement("INPUT 'Hi' / 'Ho' 5T #VAR", IInputStatementNode.class);
assertThat(input.operands()).hasSize(3);
assertNodeOperand(input, 0, ILiteralNode.class, "'Hi'");
assertNodeOperand(input, 1, ILiteralNode.class, "'Ho'");
assertNodeOperand(input, 2, IVariableReferenceNode.class, "#VAR");
}

private void assertNodeOperand(IInputStatementNode input, int index, Class<? extends ITokenNode> operandType, String source)
{
assertThat(
assertNodeType(input.operands().get(index), operandType)
.token().source()
).isEqualTo(source);
}
}

0 comments on commit 60b43ad

Please sign in to comment.