mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
513 lines
16 KiB
C#
513 lines
16 KiB
C#
using System;
|
|
using Ink.Parsed;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Ink
|
|
{
|
|
public partial class InkParser
|
|
{
|
|
protected class InfixOperator
|
|
{
|
|
public string type;
|
|
public int precedence;
|
|
public bool requireWhitespace;
|
|
|
|
public InfixOperator(string type, int precedence, bool requireWhitespace) {
|
|
this.type = type;
|
|
this.precedence = precedence;
|
|
this.requireWhitespace = requireWhitespace;
|
|
}
|
|
|
|
public override string ToString ()
|
|
{
|
|
return type;
|
|
}
|
|
}
|
|
|
|
protected Parsed.Object TempDeclarationOrAssignment()
|
|
{
|
|
Whitespace ();
|
|
|
|
bool isNewDeclaration = ParseTempKeyword();
|
|
|
|
Whitespace ();
|
|
|
|
Identifier varIdentifier = null;
|
|
if (isNewDeclaration) {
|
|
varIdentifier = (Identifier)Expect (IdentifierWithMetadata, "variable name");
|
|
} else {
|
|
varIdentifier = Parse(IdentifierWithMetadata);
|
|
}
|
|
|
|
if (varIdentifier == null) {
|
|
return null;
|
|
}
|
|
|
|
Whitespace();
|
|
|
|
// += -=
|
|
bool isIncrement = ParseString ("+") != null;
|
|
bool isDecrement = ParseString ("-") != null;
|
|
if (isIncrement && isDecrement) Error ("Unexpected sequence '+-'");
|
|
|
|
if (ParseString ("=") == null) {
|
|
// Definitely in an assignment expression?
|
|
if (isNewDeclaration) Error ("Expected '='");
|
|
return null;
|
|
}
|
|
|
|
Expression assignedExpression = (Expression)Expect (Expression, "value expression to be assigned");
|
|
|
|
if (isIncrement || isDecrement) {
|
|
var result = new IncDecExpression (varIdentifier, assignedExpression, isIncrement);
|
|
return result;
|
|
} else {
|
|
var result = new VariableAssignment (varIdentifier, assignedExpression);
|
|
result.isNewTemporaryDeclaration = isNewDeclaration;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
protected void DisallowIncrement (Parsed.Object expr)
|
|
{
|
|
if (expr is Parsed.IncDecExpression)
|
|
Error ("Can't use increment/decrement here. It can only be used on a ~ line");
|
|
}
|
|
|
|
protected bool ParseTempKeyword()
|
|
{
|
|
var ruleId = BeginRule ();
|
|
|
|
if (Parse (Identifier) == "temp") {
|
|
SucceedRule (ruleId);
|
|
return true;
|
|
} else {
|
|
FailRule (ruleId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected Parsed.Return ReturnStatement()
|
|
{
|
|
Whitespace ();
|
|
|
|
var returnOrDone = Parse(Identifier);
|
|
if (returnOrDone != "return") {
|
|
return null;
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
var expr = Parse(Expression);
|
|
|
|
var returnObj = new Return (expr);
|
|
return returnObj;
|
|
}
|
|
|
|
protected Expression Expression() {
|
|
return Expression(minimumPrecedence:0);
|
|
}
|
|
|
|
// Pratt Parser
|
|
// aka "Top down operator precedence parser"
|
|
// http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
|
|
// Algorithm overview:
|
|
// The two types of precedence are handled in two different ways:
|
|
// ((((a . b) . c) . d) . e) #1
|
|
// (a . (b . (c . (d . e)))) #2
|
|
// Where #1 is automatically handled by successive loops within the main 'while' in this function,
|
|
// so long as continuing operators have lower (or equal) precedence (e.g. imagine some series of "*"s then "+" above.
|
|
// ...and #2 is handled by recursion of the right hand term in the binary expression parser.
|
|
// (see link for advice on how to extend for postfix and mixfix operators)
|
|
protected Expression Expression(int minimumPrecedence)
|
|
{
|
|
Whitespace ();
|
|
|
|
// First parse a unary expression e.g. "-a" or parethensised "(1 + 2)"
|
|
var expr = ExpressionUnary ();
|
|
if (expr == null) {
|
|
return null;
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
// Attempt to parse (possibly multiple) continuing infix expressions (e.g. 1 + 2 + 3)
|
|
while(true) {
|
|
var ruleId = BeginRule ();
|
|
|
|
// Operator
|
|
var infixOp = ParseInfixOperator ();
|
|
if (infixOp != null && infixOp.precedence > minimumPrecedence) {
|
|
|
|
// Expect right hand side of operator
|
|
var expectationMessage = string.Format("right side of '{0}' expression", infixOp.type);
|
|
var multiaryExpr = Expect (() => ExpressionInfixRight (left: expr, op: infixOp), expectationMessage);
|
|
if (multiaryExpr == null) {
|
|
|
|
// Fail for operator and right-hand side of multiary expression
|
|
FailRule (ruleId);
|
|
|
|
return null;
|
|
}
|
|
|
|
expr = SucceedRule(ruleId, multiaryExpr) as Parsed.Expression;
|
|
|
|
continue;
|
|
}
|
|
|
|
FailRule (ruleId);
|
|
break;
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
return expr;
|
|
}
|
|
|
|
protected Expression ExpressionUnary()
|
|
{
|
|
// Divert target is a special case - it can't have any other operators
|
|
// applied to it, and we also want to check for it first so that we don't
|
|
// confuse "->" for subtraction.
|
|
var divertTarget = Parse (ExpressionDivertTarget);
|
|
if (divertTarget != null) {
|
|
return divertTarget;
|
|
}
|
|
|
|
var prefixOp = (string) OneOf (String ("-"), String ("!"));
|
|
|
|
// Don't parse like the string rules above, in case its actually
|
|
// a variable that simply starts with "not", e.g. "notable".
|
|
// This rule uses the Identifier rule, which will scan as much text
|
|
// as possible before returning.
|
|
if (prefixOp == null) {
|
|
prefixOp = Parse(ExpressionNot);
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
// - Since we allow numbers at the start of variable names, variable names are checked before literals
|
|
// - Function calls before variable names in case we see parentheses
|
|
var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression;
|
|
|
|
// Only recurse immediately if we have one of the (usually optional) unary ops
|
|
if (expr == null && prefixOp != null) {
|
|
expr = ExpressionUnary ();
|
|
}
|
|
|
|
if (expr == null)
|
|
return null;
|
|
|
|
if (prefixOp != null) {
|
|
expr = UnaryExpression.WithInner(expr, prefixOp);
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
var postfixOp = (string) OneOf (String ("++"), String ("--"));
|
|
if (postfixOp != null) {
|
|
bool isInc = postfixOp == "++";
|
|
|
|
if (!(expr is VariableReference)) {
|
|
Error ("can only increment and decrement variables, but saw '" + expr + "'");
|
|
|
|
// Drop down and succeed without the increment after reporting error
|
|
} else {
|
|
// TODO: Language Server - (Identifier combined into one vs. list of Identifiers)
|
|
var varRef = (VariableReference)expr;
|
|
expr = new IncDecExpression(varRef.identifier, isInc);
|
|
}
|
|
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
protected string ExpressionNot()
|
|
{
|
|
var id = Identifier ();
|
|
if (id == "not") {
|
|
return id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected Expression ExpressionLiteral()
|
|
{
|
|
return (Expression) OneOf (ExpressionFloat, ExpressionInt, ExpressionBool, ExpressionString);
|
|
}
|
|
|
|
protected Expression ExpressionDivertTarget()
|
|
{
|
|
Whitespace ();
|
|
|
|
var divert = Parse(SingleDivert);
|
|
if (divert == null)
|
|
return null;
|
|
|
|
if (divert.isThread)
|
|
return null;
|
|
|
|
Whitespace ();
|
|
|
|
return new DivertTarget (divert);
|
|
}
|
|
|
|
protected Number ExpressionInt()
|
|
{
|
|
int? intOrNull = ParseInt ();
|
|
if (intOrNull == null) {
|
|
return null;
|
|
} else {
|
|
return new Number (intOrNull.Value);
|
|
}
|
|
}
|
|
|
|
protected Number ExpressionFloat()
|
|
{
|
|
float? floatOrNull = ParseFloat ();
|
|
if (floatOrNull == null) {
|
|
return null;
|
|
} else {
|
|
return new Number (floatOrNull.Value);
|
|
}
|
|
}
|
|
|
|
protected StringExpression ExpressionString()
|
|
{
|
|
var openQuote = ParseString ("\"");
|
|
if (openQuote == null)
|
|
return null;
|
|
|
|
// Set custom parser state flag so that within the text parser,
|
|
// it knows to treat the quote character (") as an end character
|
|
parsingStringExpression = true;
|
|
|
|
List<Parsed.Object> textAndLogic = Parse (MixedTextAndLogic);
|
|
|
|
Expect (String ("\""), "close quote for string expression");
|
|
|
|
parsingStringExpression = false;
|
|
|
|
if (textAndLogic == null) {
|
|
textAndLogic = new List<Ink.Parsed.Object> ();
|
|
textAndLogic.Add (new Parsed.Text (""));
|
|
}
|
|
|
|
else if (textAndLogic.Exists (c => c is Divert))
|
|
Error ("String expressions cannot contain diverts (->)");
|
|
|
|
return new StringExpression (textAndLogic);
|
|
}
|
|
|
|
protected Number ExpressionBool()
|
|
{
|
|
var id = Parse(Identifier);
|
|
if (id == "true") {
|
|
return new Number (true);
|
|
} else if (id == "false") {
|
|
return new Number (false);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected Expression ExpressionFunctionCall()
|
|
{
|
|
var iden = Parse(IdentifierWithMetadata);
|
|
if (iden == null)
|
|
return null;
|
|
|
|
Whitespace ();
|
|
|
|
var arguments = Parse(ExpressionFunctionCallArguments);
|
|
if (arguments == null) {
|
|
return null;
|
|
}
|
|
|
|
return new FunctionCall(iden, arguments);
|
|
}
|
|
|
|
protected List<Expression> ExpressionFunctionCallArguments()
|
|
{
|
|
if (ParseString ("(") == null)
|
|
return null;
|
|
|
|
// "Exclude" requires the rule to succeed, but causes actual comma string to be excluded from the list of results
|
|
ParseRule commas = Exclude (String (","));
|
|
var arguments = Interleave<Expression>(Expression, commas);
|
|
if (arguments == null) {
|
|
arguments = new List<Expression> ();
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
Expect (String (")"), "closing ')' for function call");
|
|
|
|
return arguments;
|
|
}
|
|
|
|
protected Expression ExpressionVariableName()
|
|
{
|
|
List<Identifier> path = Interleave<Identifier> (IdentifierWithMetadata, Exclude (Spaced (String ("."))));
|
|
|
|
if (path == null || Story.IsReservedKeyword (path[0].name) )
|
|
return null;
|
|
|
|
return new VariableReference (path);
|
|
}
|
|
|
|
protected Expression ExpressionParen()
|
|
{
|
|
if (ParseString ("(") == null)
|
|
return null;
|
|
|
|
var innerExpr = Parse(Expression);
|
|
if (innerExpr == null)
|
|
return null;
|
|
|
|
Whitespace ();
|
|
|
|
Expect (String(")"), "closing parenthesis ')' for expression");
|
|
|
|
return innerExpr;
|
|
}
|
|
|
|
protected Expression ExpressionInfixRight(Parsed.Expression left, InfixOperator op)
|
|
{
|
|
Whitespace ();
|
|
|
|
var right = Parse(() => Expression (op.precedence));
|
|
if (right) {
|
|
|
|
// We assume that the character we use for the operator's type is the same
|
|
// as that used internally by e.g. Runtime.Expression.Add, Runtime.Expression.Multiply etc
|
|
var expr = new BinaryExpression (left, right, op.type);
|
|
return expr;
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
private InfixOperator ParseInfixOperator()
|
|
{
|
|
foreach (var op in _binaryOperators) {
|
|
|
|
int ruleId = BeginRule ();
|
|
|
|
if (ParseString (op.type) != null) {
|
|
|
|
if (op.requireWhitespace) {
|
|
if (Whitespace () == null) {
|
|
FailRule (ruleId);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return (InfixOperator) SucceedRule(ruleId, op);
|
|
}
|
|
|
|
FailRule (ruleId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected Parsed.List ExpressionList ()
|
|
{
|
|
Whitespace ();
|
|
|
|
if (ParseString ("(") == null)
|
|
return null;
|
|
|
|
Whitespace ();
|
|
|
|
// When list has:
|
|
// - 0 elements (null list) - this is okay, it's an empty list: "()"
|
|
// - 1 element - it could be confused for a single non-list related
|
|
// identifier expression in brackets, but this is a useless thing
|
|
// to do, so we reserve that syntax for a list with one item.
|
|
// - 2 or more elements - normal!
|
|
List<Identifier> memberNames = SeparatedList (ListMember, Spaced (String (",")));
|
|
|
|
Whitespace ();
|
|
|
|
// May have failed to parse the inner list - the parentheses may
|
|
// be for a normal expression
|
|
if (ParseString (")") == null)
|
|
return null;
|
|
|
|
return new List (memberNames);
|
|
}
|
|
|
|
protected Identifier ListMember ()
|
|
{
|
|
Whitespace ();
|
|
|
|
Identifier identifier = Parse (IdentifierWithMetadata);
|
|
if (identifier == null)
|
|
return null;
|
|
|
|
var dot = ParseString (".");
|
|
if (dot != null) {
|
|
Identifier identifier2 = Expect (IdentifierWithMetadata, "element name within the set " + identifier) as Identifier;
|
|
identifier.name = identifier.name + "." + identifier2?.name;
|
|
}
|
|
|
|
Whitespace ();
|
|
|
|
return identifier;
|
|
}
|
|
|
|
void RegisterExpressionOperators()
|
|
{
|
|
_maxBinaryOpLength = 0;
|
|
_binaryOperators = new List<InfixOperator> ();
|
|
|
|
// These will be tried in order, so we need "<=" before "<"
|
|
// for correctness
|
|
|
|
RegisterBinaryOperator ("&&", precedence:1);
|
|
RegisterBinaryOperator ("||", precedence:1);
|
|
RegisterBinaryOperator ("and", precedence:1, requireWhitespace: true);
|
|
RegisterBinaryOperator ("or", precedence:1, requireWhitespace: true);
|
|
|
|
RegisterBinaryOperator ("==", precedence:2);
|
|
RegisterBinaryOperator (">=", precedence:2);
|
|
RegisterBinaryOperator ("<=", precedence:2);
|
|
RegisterBinaryOperator ("<", precedence:2);
|
|
RegisterBinaryOperator (">", precedence:2);
|
|
RegisterBinaryOperator ("!=", precedence:2);
|
|
|
|
// (apples, oranges) + cabbages has (oranges, cabbages) == true
|
|
RegisterBinaryOperator ("?", precedence: 3);
|
|
RegisterBinaryOperator ("has", precedence: 3, requireWhitespace:true);
|
|
RegisterBinaryOperator ("!?", precedence: 3);
|
|
RegisterBinaryOperator ("hasnt", precedence: 3, requireWhitespace: true);
|
|
RegisterBinaryOperator ("^", precedence: 3);
|
|
|
|
RegisterBinaryOperator ("+", precedence:4);
|
|
RegisterBinaryOperator ("-", precedence:5);
|
|
RegisterBinaryOperator ("*", precedence:6);
|
|
RegisterBinaryOperator ("/", precedence:7);
|
|
|
|
RegisterBinaryOperator ("%", precedence:8);
|
|
RegisterBinaryOperator ("mod", precedence:8, requireWhitespace:true);
|
|
|
|
|
|
}
|
|
|
|
void RegisterBinaryOperator(string op, int precedence, bool requireWhitespace = false)
|
|
{
|
|
_binaryOperators.Add(new InfixOperator (op, precedence, requireWhitespace));
|
|
_maxBinaryOpLength = Math.Max (_maxBinaryOpLength, op.Length);
|
|
}
|
|
|
|
List<InfixOperator> _binaryOperators;
|
|
int _maxBinaryOpLength;
|
|
}
|
|
}
|
|
|