using Elsa.Dsl.ElsaScript.Ast;
using Elsa.Dsl.ElsaScript.Contracts;
using Parlot;
using Parlot.Fluent;
namespace Elsa.Dsl.ElsaScript.Parser;
///
/// ElsaScript parser using Parlot for robust parsing.
///
public class ElsaScriptParser : IElsaScriptParser
{
private static readonly Parser ProgramParser;
static ElsaScriptParser()
{
// Keywords
var useKeyword = Terms.Text("use");
var workflowKeyword = Terms.Text("workflow");
var expressionsKeyword = Terms.Text("expressions");
var listenKeyword = Terms.Text("listen");
var varKeyword = Terms.Text("var");
var constKeyword = Terms.Text("const");
var forKeyword = Terms.Text("for");
var foreachKeyword = Terms.Text("foreach");
var inKeyword = Terms.Text("in");
var toKeyword = Terms.Text("to");
var throughKeyword = Terms.Text("through");
var stepKeyword = Terms.Text("step");
var flowchartKeyword = Terms.Text("flowchart");
var entryKeyword = Terms.Text("entry");
// Basic tokens
var identifier = Terms.Identifier();
var stringLiteral = Terms.String();
var integerLiteral = Terms.Integer();
var decimalLiteral = Terms.Decimal();
// Punctuation
var semicolon = Terms.Char(';');
var comma = Terms.Char(',');
var colon = Terms.Char(':');
var leftParen = Terms.Char('(');
var rightParen = Terms.Char(')');
var leftBrace = Terms.Char('{');
var rightBrace = Terms.Char('}');
var leftBracket = Terms.Char('[');
var rightBracket = Terms.Char(']');
var dot = Terms.Char('.');
var arrow = Terms.Text("=>");
var rightArrow = Terms.Text("->");
var equals = Terms.Char('=');
// Deferred parsers for recursive structures
var expression = Deferred();
var statement = Deferred();
// Expression parsers
var booleanLiteral = Terms.Text("true").Or(Terms.Text("false"))
.Then(x => new LiteralNode { Value = x.ToString() == "true" });
var numberLiteral = decimalLiteral
.Then(x => new LiteralNode { Value = x });
var intLiteral = integerLiteral
.Then(x => new LiteralNode { Value = (long)x });
var stringExpr = stringLiteral
.Then(x => new LiteralNode { Value = x.ToString() });
var identifierExpr = identifier
.Then(x => new IdentifierNode { Name = x.ToString() });
// Array literal: [expr, expr, ...]
var commaSeparatedExpression = expression.And(ZeroOrOne(comma)).Then(x => x.Item1);
var arrayLiteral = Between(leftBracket, ZeroOrMany(commaSeparatedExpression), rightBracket)
.Then(elements => new ArrayLiteralNode { Elements = elements.ToList() });
// Elsa expression: lang =>
// We need to capture raw text after => up to the closing parenthesis
// This supports nested parentheses by counting depth
// Use a custom scanner-based parser wrapped in RawExpressionParser
var rawExpressionText = new RawExpressionParser();
var elsaExpressionWithLang = identifier
.And(arrow)
.And(rawExpressionText)
.Then(x => new ElsaExpressionNode
{
Language = x.Item1.ToString(),
Expression = x.Item3.ToString().Trim()
});
var elsaExpressionWithoutLang = arrow
.And(rawExpressionText)
.Then(x => new ElsaExpressionNode
{
Language = null,
Expression = x.Item2.ToString().Trim()
});
var elsaExpression = elsaExpressionWithLang.Or(elsaExpressionWithoutLang);
// Expression priority: try most specific first
expression.Parser = elsaExpression
.Or(arrayLiteral)
.Or(booleanLiteral)
.Or(numberLiteral)
.Or(intLiteral)
.Or(stringExpr)
.Or(identifierExpr);
// Argument parser: name: value or just value
var namedArgument = identifier
.And(colon)
.And(expression)
.Then(x => new ArgumentNode { Name = x.Item1.ToString(), Value = x.Item3 });
var positionalArgument = expression
.Then(x => new ArgumentNode { Value = x });
var argument = namedArgument.Or(positionalArgument);
var commaSeparatedArgument = argument.And(ZeroOrOne(comma)).Then(x => x.Item1);
var arguments = ZeroOrMany(commaSeparatedArgument);
// Activity invocation: ActivityName(args) - with or without arguments
var activityInvocationWithArgs = identifier
.And(leftParen)
.And(arguments)
.And(rightParen)
.Then(x => new ActivityInvocationNode
{
ActivityName = x.Item1.ToString(),
Arguments = x.Item3.ToList()
});
var activityInvocationNoArgs = identifier
.And(leftParen)
.And(rightParen)
.Then(x => new ActivityInvocationNode
{
ActivityName = x.Item1.ToString(),
Arguments = []
});
var activityInvocation = activityInvocationWithArgs.Or(activityInvocationNoArgs);
// Variable declaration: var/const name = expr
var variableKindParser = varKeyword.Or(constKeyword);
var variableDeclaration = variableKindParser
.And(identifier)
.And(equals)
.And(expression)
.Then(x =>
{
// x is a flat tuple (kind, identifier, equals, expression)
var kind = x.Item1.ToString() switch
{
"var" => VariableKind.Var,
"const" => VariableKind.Const,
_ => VariableKind.Var
};
return new VariableDeclarationNode
{
Kind = kind,
Name = x.Item2.ToString(),
Value = x.Item4
};
});
// Listen statement: listen ActivityName(args)
var listenStatement = listenKeyword
.And(activityInvocation)
.Then(x => new ListenNode { Activity = x.Item2 });
// Statement: variable declaration, listen, or activity invocation
var activityStatement = activityInvocation
.Then(x => x);
// Declare deferred for loop, foreach, and flowchart parsers
var forStatement = Deferred();
var foreachStatement = Deferred();
var flowchartStatement = Deferred();
statement.Parser = variableDeclaration
.Or(listenStatement)
.Or(forStatement)
.Or(foreachStatement)
.Or(flowchartStatement)
.Or(activityStatement);
// Statement with optional semicolon
var statementWithSemicolon = statement.And(ZeroOrOne(semicolon)).Then(x => x.Item1);
// For loop statement: for (var i = 0 to 10 step 1) { body } or for (i = 0 to 10) statement
// Must be defined after statementWithSemicolon
var rangeOperator = toKeyword.Or(throughKeyword);
// For body can be either a block or a single statement
var forBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
.Then(statements => (StatementNode)(statements.Count == 1
? statements.First()
: new BlockNode { Statements = statements.ToList() }));
var forSingleStatementBody = statement;
var forBody = forBlockBody.Or(forSingleStatementBody);
// For header with optional var: (var i = start to/through end step stepValue)
// or (i = start to/through end step stepValue)
// Step clause is optional
var optionalVarKeyword = ZeroOrOne(varKeyword);
var optionalStepClause = ZeroOrOne(stepKeyword.And(expression).Then(x => x.Item2));
var forHeader = Between(leftParen,
optionalVarKeyword
.And(identifier)
.And(equals)
.And(expression)
.And(rangeOperator)
.And(expression)
.And(optionalStepClause)
.Then(x => (
HasVar: x.Item1 != null,
VarName: x.Item2.ToString(),
Start: x.Item4,
RangeOp: x.Item5.ToString(),
End: x.Item6,
Step: x.Item7
)),
rightParen);
var forStatementParser = forKeyword
.And(forHeader)
.And(forBody)
.Then(result =>
{
var header = result.Item2;
var body = result.Item3;
// Default step to 1 if not specified
var stepExpr = header.Step ?? new LiteralNode { Value = 1 };
return new ForNode
{
DeclaresVariable = header.HasVar,
VariableName = header.VarName,
Start = header.Start,
End = header.End,
Step = stepExpr,
IsInclusive = header.RangeOp == "through",
Body = body
};
});
forStatement.Parser = forStatementParser;
// ForEach statement: foreach (var item in collection) { body } or foreach (item in collection) statement
// Must be defined after statementWithSemicolon
// ForEach body can be either a block or a single statement
var foreachBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
.Then(statements => (StatementNode)(statements.Count == 1
? statements.First()
: new BlockNode { Statements = statements.ToList() }));
var foreachSingleStatementBody = statement;
var foreachBody = foreachBlockBody.Or(foreachSingleStatementBody);
// ForEach header with optional var: (var item in collection) or (item in collection)
var foreachOptionalVarKeyword = ZeroOrOne(varKeyword);
var foreachHeader = Between(leftParen,
foreachOptionalVarKeyword
.And(identifier)
.And(inKeyword)
.And(expression)
.Then(x => (
HasVar: x.Item1 != null,
VarName: x.Item2.ToString(),
Collection: x.Item4
)),
rightParen);
var foreachStatementParser = foreachKeyword
.And(foreachHeader)
.And(foreachBody)
.Then(result =>
{
var header = result.Item2;
var body = result.Item3;
return new ForEachNode
{
DeclaresVariable = header.HasVar,
VariableName = header.VarName,
Collection = header.Collection,
Body = body
};
});
foreachStatement.Parser = foreachStatementParser;
// Flowchart statement: flowchart { [variables] [nodes] [connections] [entry] }
// Node declaration: label: statement;
// Entry declaration: entry label;
// Connection declaration: source -> target; or source.Outcome -> target;
// Flowchart body element can be:
// 1. Variable declaration
// 2. Node declaration (label: statement)
// 3. Entry declaration (entry label)
// 4. Connection declaration (source -> target or source.Outcome -> target)
// Node declaration: label: activityInvocation; or label: { block }
// Note: We use activityInvocation directly (not statement) to avoid circular dependency
// since statement includes flowchart which would include node declarations
var nodeBlock = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
.Then(statements => statements.Count == 1
? statements.First()
: new BlockNode { Statements = statements.ToList() });
var nodeActivityStatement = activityInvocation.Then(s => s);
var nodeDeclaration = identifier
.And(colon)
.And(nodeBlock.Or(nodeActivityStatement))
.And(ZeroOrOne(semicolon))
.Then(x => new LabeledActivityNode
{
Label = x.Item1.ToString(),
Activity = x.Item3
});
// Entry declaration: entry label;
var entryDeclaration = entryKeyword
.And(identifier)
.And(ZeroOrOne(semicolon))
.Then(x => x.Item2.ToString());
// Connection declaration: source -> target; or source.Outcome -> target;
// Source can be: identifier or identifier.identifier (with outcome)
var optionalOutcome = ZeroOrOne(dot.And(identifier).Then(x => x.Item2.ToString()));
var connectionSource = identifier
.And(optionalOutcome)
.Then(x => (
SourceLabel: x.Item1.ToString(),
Outcome: x.Item2
));
var connectionTarget = identifier;
var connectionDeclaration = connectionSource
.And(rightArrow)
.And(connectionTarget)
.And(ZeroOrOne(semicolon))
.Then(x => new ConnectionNode
{
Source = x.Item1.SourceLabel,
Outcome = x.Item1.Outcome,
Target = x.Item3.ToString()
});
// Flowchart body element type - try each parser in order
var flowchartBodyElement = variableDeclaration.Then